feat: DeepSeek v4 模型对齐、作业助手脚本与 Agent 对比测试

- 前端 WorkflowEditor/ModelConfigs/NodeTemplates:deepseek-v4-flash、v4-pro,弃用提示
- llm_service 默认 deepseek-v4-flash;workflow_engine 等与模型配置注入
- 作业管理脚本支持 AGENT_NAME 与 v4-pro;新增 compare_homework_agents 脚本
- 文档重命名为 (红头)项目核心文档汇总.md 并更新 DeepSeek 说明

Made-with: Cursor
This commit is contained in:
renjianbo
2026-04-30 00:57:13 +08:00
parent cadeb2dc32
commit 4366312946
12 changed files with 488 additions and 55 deletions

View File

@@ -121,7 +121,7 @@
- **工具节点**: 定时任务、Agent节点
### 5. 内置工具调用
平台提供8个内置工具可在LLM节点中启用
平台提供10个内置工具可在LLM节点中启用
1. 🌐 **http_request** - HTTP请求工具
2. 📖 **file_read** - 文件读取工具
3. ✍️ **file_write** - 文件写入工具
@@ -130,6 +130,8 @@
6. 🔢 **math_calculate** - 数学计算工具
7. 💻 **system_info** - 系统信息工具
8. 📦 **json_process** - JSON处理工具
9. 🗄️ **database_query** - 数据库查询工具(只读查询场景)
10. 📱 **adb_log** - Android/设备日志工具
### 6. 执行管理
- 创建执行任务
@@ -145,14 +147,26 @@
- 数据查询功能
### 8. 模型配置管理
- 模型配置CRUD API
- 多模型支持OpenAI、DeepSeek等
- 模型配置CRUD APIAPI Key 等在服务端加密存储;工作流 LLM 节点可绑定 `model_config_id` 由可信所有者运行时解密注入)
- 多模型支持OpenAI、Anthropic 兼容路径、DeepSeek 等)
- 模型切换和配置
**DeepSeek APIOpenAI 兼容格式)**
与 [DeepSeek API 文档](https://api-docs.deepseek.com/) 对齐的常用参数如下(密钥需在控制台申请,勿写入文档或仓库):
| 参数 | 说明 |
|------|------|
| **base_url** | `https://api.deepseek.com`OpenAI 兼容Anthropic 兼容格式为 `https://api.deepseek.com/anthropic` |
| **主推模型** | `deepseek-v4-flash``deepseek-v4-pro` |
| **兼容旧名(计划弃用)** | `deepseek-chat``deepseek-reasoner` 计划于 **2026/07/24** 弃用;二者分别对应 `deepseek-v4-flash` 的非思考与思考模式 |
本平台侧:**新建 LLM 节点、节点模板及后端未显式指定模型时的 DeepSeek 默认值**为 **`deepseek-v4-flash`**;工作流编辑器与「模型配置」页下拉仍可选择兼容旧模型名并标注弃用时间。
### 9. Agent管理
- Agent CRUD API
- Agent管理页面
- Agent协作功能
- 批量场景Agent脚本教育/企业/政务/媒体)
## 项目结构
@@ -255,7 +269,8 @@ pnpm dev
```
### 服务访问地址
- **前端**: http://localhost:8038
- **前端(本地开发推荐)**: http://localhost:3001
- **前端Docker方案**: http://localhost:8038
- **后端API**: http://localhost:8037
- **API文档**: http://localhost:8037/docs
- **Redis**: localhost:6379
@@ -305,7 +320,7 @@ pnpm dev
### 节点类型说明
- **开始节点**:工作流起始节点
- **输入节点**:数据输入节点,可配置输入参数
- **LLM节点**AI模型处理节点支持OpenAI、DeepSeek等模型
- **LLM节点**AI 模型处理节点,支持 OpenAI、DeepSeekDeepSeek 推荐选用 **`deepseek-v4-flash`** 或 **`deepseek-v4-pro`**(旧名 `deepseek-chat` / `deepseek-reasoner` 仍可兼容使用,计划 **2026/07/24** 弃用)
- **条件节点**:条件判断节点,支持表达式配置
- **转换节点**数据转换节点支持JSONPath、模板等
- **输出节点**:数据输出节点,可配置输出格式
@@ -352,10 +367,9 @@ alembic downgrade -1
```
### 添加新节点类型
1. 后端:在 `backend/app/core/nodes/`加节点执行器
2. 端:在 `backend/app/core/workflow_engine.py` 中注册节点类型
3. 前端:在 `frontend/src/components/nodes/` 中添加节点组件
4. 前端:在 `frontend/src/stores/workflowStore.ts` 中注册节点类型
1. 后端:在 `backend/app/services/workflow_engine.py``execute_node`加节点类型分支;如需校验,可扩展 `workflow_validator.py`
2. 端:在 `frontend/src/components/nodes/`(或工作流编辑器关联组件)中增加节点展示与配置
3. 前端:在 `frontend/src/stores/workflowStore.ts` 等处注册节点类型与默认数据
## 测试指南
@@ -376,7 +390,7 @@ alembic downgrade -1
## 内置工具列表
平台提供8个内置工具,详细功能如下:
平台提供10个内置工具,详细功能如下:
| 工具名称 | 功能描述 | 主要参数 |
|---------|---------|---------|
@@ -388,6 +402,10 @@ alembic downgrade -1
| math_calculate | 执行数学计算 | expression |
| system_info | 获取系统信息 | 无 |
| json_process | 处理JSON数据 | json_string, operation |
| database_query | 只读数据库查询 | datasource/query(按工具schema) |
| adb_log | 设备日志读取 | command/serial/lines(按工具schema) |
> 说明:`http_request` 已加入响应体截断与头部精简机制(支持 `max_body_chars`),用于避免大页面导致 LLM 上下文超限。
详细使用示例请参考:[内置工具列表.md](./内置工具列表.md)
@@ -406,9 +424,11 @@ alembic downgrade -1
3. **工作流执行引擎** - DAG构建、拓扑排序、节点执行
4. **可视化编辑器** - 拖拽节点、连线、配置面板
5. **异步任务处理** - Celery集成支持长时间运行的任务
6. **多模型支持** - OpenAI、DeepSeek集成
7. **内置工具调用** - 8个内置工具支持
6. **多模型支持** - OpenAI、DeepSeekDeepSeek 主推 `deepseek-v4-flash` / `deepseek-v4-pro`
7. **内置工具调用** - 10 个内置工具支持
8. **实时状态推送** - WebSocket实时推送执行状态
9. **批量Agent场景生成** - 教育与政务/媒体场景批量创建脚本
10. **Windows运维文档统一** - 启停/重启流程合并为单一权威文档
### 近期开发重点(高优先级)
1. **监控和告警前端界面** - 系统监控面板、告警规则管理
@@ -451,12 +471,21 @@ alembic downgrade -1
## 联系和支持
- **API文档**http://localhost:8037/docs
- **前端服务**http://localhost:8038
- **前端服务(本地开发)**http://localhost:3001
- **前端服务Docker**http://localhost:8038
- **问题反馈**:查看项目文档或联系开发团队
## 关键文档索引(建议)
- Windows 启停唯一文档:[`(红头)Windows服务器启动与重启唯一指南.md`](./(红头)Windows服务器启动与重启唯一指南.md)
- 上传图片与OCR实现[`(红头)上传图片和识别的实现文档.md`](./(红头)上传图片和识别的实现文档.md)
- 教育行业批量Agent脚本`backend/scripts/create_education_agents_batch.py`
- 政务/媒体批量Agent脚本`backend/scripts/create_gov_media_agents_batch.py`
- 企业场景批量Agent脚本`backend/scripts/create_enterprise_scenario_agents.py`
---
**最后更新**: 2026-04-06
**文档版本**: 1.0
**最后更新**: 2026-04-30
**文档版本**: 1.2
*本文档基于项目现有文档整理生成,涵盖项目核心信息。详细技术方案请参考[方案-优化版.md](./方案-优化版.md)。*
*本文档基于项目现有文档整理生成,涵盖项目核心信息。详细技术方案请参考[方案-优化版.md](./方案-优化版.md)。DeepSeek 模型名与 Base URL 以官方文档为准,变更时请同步修订本节。*

View File

@@ -27,7 +27,7 @@ class NodeTemplateCreate(BaseModel):
prompt: str = Field(..., min_length=1, description="提示词模板")
variables: Optional[List[Dict[str, Any]]] = Field(None, description="变量定义列表")
provider: Optional[str] = Field("deepseek", description="默认LLM提供商")
model: Optional[str] = Field("deepseek-chat", description="默认模型")
model: Optional[str] = Field("deepseek-v4-flash", description="默认模型")
temperature: Optional[str] = Field("0.7", description="默认温度参数")
max_tokens: Optional[int] = Field(1500, description="默认最大token数")
is_public: Optional[bool] = Field(False, description="是否公开")
@@ -176,7 +176,7 @@ async def create_node_template(
prompt=template_data.prompt,
variables=template_data.variables or [],
provider=template_data.provider or "deepseek",
model=template_data.model or "deepseek-chat",
model=template_data.model or "deepseek-v4-flash",
temperature=template_data.temperature or "0.7",
max_tokens=template_data.max_tokens or 1500,
is_public=template_data.is_public or False,

View File

@@ -60,7 +60,12 @@ async def test_node(
"edges": []
}
engine = WorkflowEngine("test-node", workflow_data, db=db)
engine = WorkflowEngine(
"test-node",
workflow_data,
db=db,
trusted_model_config_user_id=str(current_user.id),
)
# 执行节点
result = await engine.execute_node(node, input_data)

View File

@@ -25,7 +25,7 @@ class NodeTemplate(Base):
# 默认配置
provider = Column(String(50), default="deepseek", comment="默认LLM提供商")
model = Column(String(100), default="deepseek-chat", comment="默认模型")
model = Column(String(100), default="deepseek-v4-flash", comment="默认模型")
temperature = Column(String(10), default="0.7", comment="默认温度参数")
max_tokens = Column(Integer, default=1500, comment="默认最大token数")

View File

@@ -480,7 +480,7 @@ class LLMService:
async def call_deepseek(
self,
prompt: str,
model: str = "deepseek-chat",
model: str = "deepseek-v4-flash",
temperature: float = 0.7,
max_tokens: Optional[int] = None,
api_key: Optional[str] = None,
@@ -492,7 +492,7 @@ class LLMService:
Args:
prompt: 提示词
model: 模型名称,默认deepseek-chat
model: 模型名称,默认 deepseek-v4-flashdeepseek-chat / deepseek-reasoner 将于 2026/07/24 弃用)
temperature: 温度参数默认0.7
max_tokens: 最大token数
api_key: API密钥可选如果不提供则使用默认配置
@@ -610,7 +610,7 @@ class LLMService:
elif provider == "deepseek":
# 默认模型
if not model:
model = "deepseek-chat"
model = "deepseek-v4-flash"
return await self.call_deepseek(
prompt=prompt,
model=model,
@@ -852,7 +852,7 @@ class LLMService:
self,
prompt: str,
tools: List[Dict[str, Any]],
model: str = "deepseek-chat",
model: str = "deepseek-v4-flash",
temperature: float = 0.7,
max_tokens: Optional[int] = None,
api_key: Optional[str] = None,
@@ -928,7 +928,7 @@ class LLMService:
)
elif provider == "deepseek":
if not model:
model = "deepseek-chat"
model = "deepseek-v4-flash"
return await self.call_deepseek_with_tools(
prompt=prompt,
tools=tools,

View File

@@ -92,6 +92,7 @@ class WorkflowEngine:
logger=None,
db=None,
budget_limits: Optional[Dict[str, Any]] = None,
trusted_model_config_user_id: Optional[str] = None,
):
"""
初始化工作流引擎
@@ -101,6 +102,7 @@ class WorkflowEngine:
workflow_data: 工作流数据包含nodes和edges
logger: 执行日志记录器(可选)
db: 数据库会话可选用于Agent节点加载Agent配置
trusted_model_config_user_id: 允许加载「模型配置」解密密钥的用户 ID通常为当前执行所属 Workflow/Agent 的 owner
"""
self.workflow_id = workflow_id
self.nodes = {node['id']: node for node in workflow_data.get('nodes', [])}
@@ -115,6 +117,7 @@ class WorkflowEngine:
self._llm_invocations: int = 0
self._tool_calls_used: int = 0
self.budget_limits: Dict[str, Any] = dict(budget_limits or {})
self.trusted_model_config_user_id: Optional[str] = trusted_model_config_user_id
self._cap_steps: int = max(
1, int(getattr(settings, "WORKFLOW_MAX_STEPS_PER_RUN", 2000) or 2000)
)
@@ -178,6 +181,71 @@ class WorkflowEngine:
detail=f"已超过工具调用预算({self._cap_tool} 次)",
)
def _resolve_llm_credentials_from_model_config(
self, node_data: Dict[str, Any]
) -> Optional[Dict[str, Any]]:
"""
若节点 data 含 model_config_id则从数据库加载加密密钥并校验归属
返回 {"api_key","base_url","provider","model"},否则返回 None。
"""
raw = node_data.get("model_config_id") or node_data.get("modelConfigId")
if raw is None or raw == "":
return None
cfg_id = str(raw).strip()
if not cfg_id:
return None
if not self.trusted_model_config_user_id:
logger.warning(
"LLM 节点配置了 model_config_id=%s,但未绑定 trusted_model_config_user_id"
"将跳过模型配置密钥注入(仍使用节点与环境变量)。",
cfg_id,
)
return None
from app.models.model_config import ModelConfig
from app.services.encryption_service import EncryptionService
db = self.db or SessionLocal()
own_db = self.db is None
try:
cfg = db.query(ModelConfig).filter(ModelConfig.id == cfg_id).first()
if not cfg:
raise ValueError(f"模型配置不存在: {cfg_id}")
if cfg.user_id != self.trusted_model_config_user_id:
raise ValueError("无权使用该模型配置(仅创建者可调用)")
api_key_plain = EncryptionService.decrypt(cfg.api_key)
if not (api_key_plain or "").strip():
raise ValueError("模型配置中的 API Key 无效")
base_url = (cfg.base_url or "").strip() or None
raw_prov = (cfg.provider or "").strip().lower()
if raw_prov == "local":
llm_prov = "openai"
elif raw_prov in ("openai", "deepseek"):
llm_prov = raw_prov
elif raw_prov == "anthropic":
raise ValueError(
"当前 LLM 节点暂不支持从模型配置加载 Anthropic请改用 OpenAI 或 DeepSeek 兼容配置"
)
else:
raise ValueError(f"不支持的模型配置提供商: {cfg.provider}")
model_name = (cfg.model_name or "").strip()
if not model_name:
raise ValueError("模型配置中模型名称为空")
return {
"api_key": api_key_plain.strip(),
"base_url": base_url,
"provider": llm_prov,
"model": model_name,
}
finally:
if own_db:
db.close()
def _get_persist_scope(self) -> Tuple[Optional[str], Optional[str]]:
"""(scope_kind, scope_id) 或 (None, None),用于持久化 user_memory_*。"""
if self._persist_scope_cache is None:
@@ -1625,9 +1693,33 @@ class WorkflowEngine:
max_tokens = int(max_tokens_raw) if max_tokens_raw is not None else None
else:
max_tokens = None
# 不传递 api_key 和 base_url让 LLM 服务使用系统默认配置(与节点测试保持一致)
api_key = None
base_url = None
# 默认使用环境变量中的 Key若节点绑定 model_config_id 且执行上下文可信,则注入用户保存的密钥与 endpoint
api_key: Optional[str] = None
base_url: Optional[str] = None
mc_cred: Optional[Dict[str, Any]] = None
try:
mc_cred = self._resolve_llm_credentials_from_model_config(node_data)
if mc_cred:
api_key = mc_cred.get("api_key")
base_url = mc_cred.get("base_url")
provider = mc_cred.get("provider", provider)
model = mc_cred.get("model", model)
except Exception as mc_err:
if self.logger:
duration = int((time.time() - start_time) * 1000)
self.logger.log_node_error(node_id, node_type, mc_err, duration)
logger.error(f"[rjb] LLM 模型配置解析失败: {mc_err}", exc_info=True)
return {
'output': None,
'status': 'failed',
'error': str(mc_err),
}
llm_extra_kw: Dict[str, Any] = {}
if api_key is not None:
llm_extra_kw["api_key"] = api_key
if base_url is not None:
llm_extra_kw["base_url"] = base_url
# 记录实际发送给LLM的prompt
logger.info(f"[rjb] 准备调用LLM: node_id={node_id}, provider={provider}, model={model}, prompt前200字符='{prompt[:200] if len(prompt) > 200 else prompt}'")
@@ -1660,7 +1752,10 @@ class WorkflowEngine:
# 调用LLM服务
try:
if self.logger:
logger.debug(f"[rjb] LLM节点配置: provider={provider}, model={model}, 使用系统默认API Key配置, 工具调用: {'启用' if tools else '禁用'}")
key_src = "模型配置(model_config_id)" if mc_cred else "环境变量默认"
logger.debug(
f"[rjb] LLM节点配置: provider={provider}, model={model}, API密钥来源={key_src}, 工具调用: {'启用' if tools else '禁用'}"
)
self.logger.info(f"调用LLM服务: {provider}/{model}", node_id=node_id, node_type=node_type)
# 根据是否启用工具选择不同的调用方式
@@ -1684,6 +1779,8 @@ class WorkflowEngine:
_tool_extra["request_timeout"] = max(10.0, float(_rt))
except (TypeError, ValueError):
pass
_merged_tool_kw = dict(llm_extra_kw)
_merged_tool_kw.update(_tool_extra)
result = await llm_service.call_llm_with_tools(
prompt=prompt,
tools=tools,
@@ -1694,7 +1791,7 @@ class WorkflowEngine:
execution_logger=self.logger,
tool_choice=_tool_choice,
on_tool_executed=self._on_tool_executed_budget,
**_tool_extra,
**_merged_tool_kw,
)
result = self._enrich_llm_json_user_profile(result, input_data)
else:
@@ -1703,8 +1800,8 @@ class WorkflowEngine:
provider=provider,
model=model,
temperature=temperature,
max_tokens=max_tokens
# 不传递 api_key 和 base_url使用系统默认配置
max_tokens=max_tokens,
**llm_extra_kw,
)
result = self._enrich_llm_json_user_profile(result, input_data)
@@ -4786,6 +4883,7 @@ class WorkflowEngine:
logger=child_logger,
db=self.db,
budget_limits=child_budget,
trusted_model_config_user_id=self.trusted_model_config_user_id,
)
try:
child_result = await child_engine.execute(sub_input)

View File

@@ -41,6 +41,19 @@ def _snapshot_to_jsonable(snapshot: dict) -> dict:
return _json.loads(_json.dumps(snapshot, default=str))
def _trusted_user_for_execution(db, execution: Optional[Execution]) -> Optional[str]:
"""用于校验 LLM 节点引用的 model_configs 归属(与 Workflow / Agent 所有者一致)。"""
if not execution:
return None
if execution.agent_id:
ag = db.query(Agent).filter(Agent.id == execution.agent_id).first()
return ag.user_id if ag else None
if execution.workflow_id:
wf = db.query(Workflow).filter(Workflow.id == execution.workflow_id).first()
return wf.user_id if wf else None
return None
@celery_app.task(bind=True)
def execute_workflow_task(
self,
@@ -80,12 +93,14 @@ def execute_workflow_task(
# 创建工作流引擎传入logger、db、合并后的执行预算
budget = merge_budget_for_execution(db, execution) if execution else {}
trusted_uid = _trusted_user_for_execution(db, execution)
engine = WorkflowEngine(
workflow_id,
workflow_data,
logger=execution_logger,
db=db,
budget_limits=budget,
trusted_model_config_user_id=trusted_uid,
)
max_retries = max(0, int(getattr(settings, "WORKFLOW_TASK_MAX_RETRIES", 0) or 0))
@@ -269,12 +284,14 @@ def resume_workflow_task(
self.update_state(state="PROGRESS", meta={"progress": 0, "status": "running"})
budget = merge_budget_for_execution(db, execution) if execution else {}
trusted_uid = _trusted_user_for_execution(db, execution)
engine = WorkflowEngine(
wf_key,
workflow_data,
logger=execution_logger,
db=db,
budget_limits=budget,
trusted_model_config_user_id=trusted_uid,
)
max_retries = max(0, int(getattr(settings, "WORKFLOW_TASK_MAX_RETRIES", 0) or 0))
result: Optional[dict] = None

View File

@@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""
对比「学生作业管理助手」与「学生作业管理助手2号」在相同输入下的节点测试结果。
调用 POST /api/v1/nodes/test同步需后端运行且 DEEPSEEK_API_KEY 有效。
用法: cd backend && .\\venv\\Scripts\\python.exe scripts/compare_homework_agents.py
"""
from __future__ import annotations
import json
import os
import sys
import time
from typing import Any, Dict, List, Optional
import requests
BACKEND = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/")
USER = os.getenv("PLATFORM_USERNAME", "admin")
PWD = os.getenv("PLATFORM_PASSWORD", "123456")
NAMES = ["学生作业管理助手", "学生作业管理助手2号"]
TEST_QUERY = (
"帮我记一项作业语文摘抄名著段落3处并批注截止周五下午5点前。"
"请只回复一条简短清单科目、要点、截止时间不要超过120字。"
)
def _login() -> str:
r = requests.post(
f"{BACKEND}/api/v1/auth/login",
data={"username": USER, "password": PWD},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=15,
)
r.raise_for_status()
t = r.json().get("access_token")
if not t:
raise RuntimeError("无 access_token")
return t
def _find_llm_node(wf: Dict[str, Any]) -> Optional[Dict[str, Any]]:
for n in wf.get("nodes") or []:
if n.get("type") == "llm":
return n
return None
def _get_agents(h: Dict[str, str]) -> List[Dict[str, Any]]:
r = requests.get(
f"{BACKEND}/api/v1/agents",
params={"search": "学生作业管理", "limit": 50},
headers=h,
timeout=30,
)
r.raise_for_status()
return list(r.json() or [])
def _fetch_agent_detail(h: Dict[str, str], aid: str) -> Dict[str, Any]:
r = requests.get(f"{BACKEND}/api/v1/agents/{aid}", headers=h, timeout=30)
r.raise_for_status()
return r.json()
def _test_node(h: Dict[str, str], node: Dict[str, Any]):
body = {"node": node, "input_data": {"query": TEST_QUERY}}
t0 = time.perf_counter()
r = requests.post(
f"{BACKEND}/api/v1/nodes/test",
headers=h,
json=body,
timeout=240,
)
elapsed_ms = int((time.perf_counter() - t0) * 1000)
if r.status_code != 200:
return elapsed_ms, "", f"HTTP {r.status_code}: {r.text[:800]}", None
data = r.json()
out = data.get("output")
if isinstance(out, dict):
text = out.get("output") or out.get("text") or json.dumps(out, ensure_ascii=False)
else:
text = str(out)
err = data.get("error_message")
status = data.get("status")
return elapsed_ms, text[:4000], err, status
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
try:
token = _login()
except Exception as e:
print("登录失败:", e, file=sys.stderr)
return 1
h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
agents = _get_agents(h)
by_name = {a.get("name"): a for a in agents}
results = []
for name in NAMES:
a = by_name.get(name)
if not a:
results.append(
{"name": name, "error": f"未找到名为「{name}」的 Agent"}
)
continue
detail = _fetch_agent_detail(h, a["id"])
wf = detail.get("workflow_config") or {}
node = _find_llm_node(wf)
if not node:
results.append({"name": name, "id": a["id"], "error": "工作流中无 LLM 节点"})
continue
data = node.get("data") or {}
elapsed_ms, text, err, st = _test_node(h, node)
results.append(
{
"name": name,
"id": a["id"],
"provider": data.get("provider"),
"model": data.get("model"),
"elapsed_ms": elapsed_ms,
"status": st,
"api_error": err,
"output_excerpt": text[:2000],
}
)
print("=== 对比测试(同步节点测试 API===")
print("输入:", TEST_QUERY)
print()
for r in results:
print(f"{r['name']}")
if r.get("error"):
print(" ", r["error"])
print()
continue
print(f" id: {r['id']}")
print(f" provider/model: {r.get('provider')} / {r.get('model')}")
print(f" 耗时: {r['elapsed_ms']} ms status: {r.get('status')}")
if r.get("api_error"):
print(f" error_message: {r['api_error']}")
print(f" 输出节选:\n{r.get('output_excerpt', '')}\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -8,7 +8,7 @@
环境变量:
PLATFORM_BASE_URL, PLATFORM_USERNAME, PLATFORM_PASSWORD
AGENT_NAME默认 学生作业管理助手)
AGENT_NAME默认 学生作业管理助手);示例:`AGENT_NAME=学生作业管理助手2号 HOMEWORK_LLM_MODEL=deepseek-v4-pro`
HOMEWORK_LLM_PROVIDER / HOMEWORK_LLM_MODEL / HOMEWORK_LLM_TIMEOUT可选
"""
from __future__ import annotations
@@ -33,7 +33,7 @@ PROVIDER = os.getenv(
"HOMEWORK_LLM_PROVIDER", os.getenv("ENTERPRISE_LLM_PROVIDER", "deepseek")
)
MODEL = os.getenv(
"HOMEWORK_LLM_MODEL", os.getenv("ENTERPRISE_LLM_MODEL", "deepseek-chat")
"HOMEWORK_LLM_MODEL", os.getenv("ENTERPRISE_LLM_MODEL", "deepseek-v4-flash")
)
REQ_TIMEOUT = max(
30,
@@ -52,7 +52,9 @@ BUDGET_CONFIG = {
HOMEWORK_TOOLS = ["file_read", "text_analyze", "datetime", "json_process"]
HOMEWORK_PROMPT = """你是「学生作业管理助手」,帮助学生**记作业**与**监督完成**,语气友好、具体、可执行。
def _homework_prompt(agent_display_name: str) -> str:
return f"""你是「{agent_display_name}」,帮助学生**记作业**与**监督完成**,语气友好、具体、可执行。
【核心能力】
1. **记作业**:从用户自然语言中提取「科目 / 作业内容 / 截止日期与时间 / 老师要求要点 / 预估耗时」,整理成清单。
@@ -104,7 +106,7 @@ def build_workflow() -> Dict[str, Any]:
"position": {"x": llm_pos[0], "y": llm_pos[1]},
"data": {
"label": "作业管理",
"prompt": HOMEWORK_PROMPT,
"prompt": _homework_prompt(AGENT_NAME),
"provider": PROVIDER,
"model": MODEL,
"temperature": 0.3,
@@ -169,7 +171,7 @@ def main() -> int:
h = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
desc = (
"学生作业管理助手:记作业(科目、内容、截止日)、跟进度、温和督促与周回顾;"
f"{AGENT_NAME}:记作业(科目、内容、截止日)、跟进度、温和督促与周回顾;"
"支持上传文件/照片后用 file_read 提取正文文本、PDF、docx、xlsx、图片 OCR与 json_process 整理;"
f"默认模型 {PROVIDER}/{MODEL},单次执行内工具迭代上限 10。"
)

View File

@@ -645,6 +645,25 @@
<el-option label="DeepSeek" value="deepseek" />
</el-select>
</el-form-item>
<el-form-item label="已配置模型">
<el-select
v-model="selectedNode.data.model_config_id"
placeholder="从模型配置中选择(可选)"
clearable
filterable
@change="handleConfiguredModelChange"
>
<el-option
v-for="cfg in availableConfiguredModels"
:key="cfg.id"
:label="`${cfg.name} (${cfg.provider}/${cfg.model_name})`"
:value="cfg.id"
/>
</el-select>
<div style="margin-top: 5px; color: #909399; font-size: 12px;">
选择后会回填提供商与模型保存工作流后运行时将通过 model_config_id 使用你在模型配置中保存的密钥与 API 地址需与当前 Agent/工作流属主一致
</div>
</el-form-item>
<el-form-item label="提示词">
<div class="prompt-input-wrapper" style="position: relative;">
<el-input
@@ -764,8 +783,11 @@
<el-option label="GPT-4 Turbo" value="gpt-4-turbo-preview" />
</template>
<template v-else-if="selectedNode.data.provider === 'deepseek'">
<el-option label="DeepSeek Chat" value="deepseek-chat" />
<el-option label="DeepSeek V4 Flash推荐" value="deepseek-v4-flash" />
<el-option label="DeepSeek V4 Pro推荐" value="deepseek-v4-pro" />
<el-option label="DeepSeek Coder" value="deepseek-coder" />
<el-option label="DeepSeek Chat兼容计划弃用 2026/07/24" value="deepseek-chat" />
<el-option label="DeepSeek Reasoner兼容计划弃用 2026/07/24" value="deepseek-reasoner" />
</template>
</el-select>
</el-form-item>
@@ -827,6 +849,25 @@
<el-option label="DeepSeek" value="deepseek" />
</el-select>
</el-form-item>
<el-form-item label="已配置模型">
<el-select
v-model="selectedNode.data.model_config_id"
placeholder="从模型配置中选择(可选)"
clearable
filterable
@change="handleConfiguredModelChange"
>
<el-option
v-for="cfg in availableConfiguredModels"
:key="cfg.id"
:label="`${cfg.name} (${cfg.provider}/${cfg.model_name})`"
:value="cfg.id"
/>
</el-select>
<div style="margin-top: 5px; color: #909399; font-size: 12px;">
选择后会回填提供商与模型保存工作流后运行时将通过 model_config_id 使用你在模型配置中保存的密钥与 API 地址需与当前 Agent/工作流属主一致
</div>
</el-form-item>
<el-form-item label="提示词">
<el-input
v-model="selectedNode.data.prompt"
@@ -846,8 +887,11 @@
<el-option label="GPT-4 Turbo" value="gpt-4-turbo-preview" />
</template>
<template v-else-if="selectedNode.data.provider === 'deepseek'">
<el-option label="DeepSeek Chat" value="deepseek-chat" />
<el-option label="DeepSeek V4 Flash推荐" value="deepseek-v4-flash" />
<el-option label="DeepSeek V4 Pro推荐" value="deepseek-v4-pro" />
<el-option label="DeepSeek Coder" value="deepseek-coder" />
<el-option label="DeepSeek Chat兼容计划弃用 2026/07/24" value="deepseek-chat" />
<el-option label="DeepSeek Reasoner兼容计划弃用 2026/07/24" value="deepseek-reasoner" />
</template>
</el-select>
</el-form-item>
@@ -2910,6 +2954,14 @@ import { StartNode, LLMNode, ConditionNode, EndNode, DefaultNode } from './NodeT
import { useCollaboration } from '@/composables/useCollaboration'
import NodeExecutionDetail from './NodeExecutionDetail.vue'
interface ConfiguredModelItem {
id: string
name: string
provider: string
model_name: string
base_url?: string
}
const props = defineProps<{
workflowId?: string
agentId?: string
@@ -2986,6 +3038,50 @@ const hasUpstreamNodes = computed(() => {
// 节点模板相关
const nodeTemplates = ref<any[]>([])
const loadingTemplates = ref(false)
const configuredModels = ref<ConfiguredModelItem[]>([])
const availableConfiguredModels = computed(() => {
const p = selectedNode.value?.data?.provider
if (!p) return configuredModels.value
return configuredModels.value.filter(cfg => cfg.provider === p)
})
const loadConfiguredModels = async () => {
try {
const response = await api.get('/api/v1/model-configs', {
params: {
limit: 100
}
})
configuredModels.value = Array.isArray(response.data) ? response.data : []
} catch (error) {
console.warn('[WorkflowEditor] 加载模型配置失败:', error)
configuredModels.value = []
}
}
const handleConfiguredModelChange = (configId: string) => {
if (!selectedNode.value) return
if (!configId) return
const cfg = configuredModels.value.find(item => item.id === configId)
if (!cfg) return
selectedNode.value.data.provider = cfg.provider
selectedNode.value.data.model = cfg.model_name
}
watch(
() => selectedNode.value?.data?.provider,
(newProv) => {
const sn = selectedNode.value
if (!sn || (sn.type !== 'llm' && sn.type !== 'template')) return
const mid = sn.data?.model_config_id as string | undefined
if (!mid || !newProv) return
const cfg = configuredModels.value.find((c) => c.id === mid)
if (cfg && cfg.provider !== newProv) {
sn.data.model_config_id = ''
}
}
)
// 加载节点模板列表
const loadNodeTemplates = async () => {
@@ -4538,7 +4634,7 @@ const getScenarios = (nodeType: string) => {
],
config: {
provider: 'deepseek',
model: 'deepseek-chat',
model: 'deepseek-v4-flash',
prompt: '请总结以下内容100字以内{{input}}',
temperature: 0.5,
max_tokens: 500
@@ -4558,7 +4654,7 @@ const getScenarios = (nodeType: string) => {
],
config: {
provider: 'deepseek',
model: 'deepseek-chat',
model: 'deepseek-v4-flash',
prompt: '请把下列内容翻译成{{target_lang}}{{input}}',
temperature: 0.3,
max_tokens: 1000
@@ -4571,7 +4667,7 @@ const getScenarios = (nodeType: string) => {
icon: 'DataAnalysis',
config: {
provider: 'deepseek',
model: 'deepseek-chat',
model: 'deepseek-v4-flash',
prompt: '请从以下文本中提取关键信息以JSON格式返回{{input}}',
temperature: 0.2,
max_tokens: 1000
@@ -4584,7 +4680,7 @@ const getScenarios = (nodeType: string) => {
icon: 'Sort',
config: {
provider: 'deepseek',
model: 'deepseek-chat',
model: 'deepseek-v4-flash',
prompt: '请对以下文本进行分类:{{input}}',
temperature: 0.1,
max_tokens: 200
@@ -4701,7 +4797,7 @@ const configTemplates = ref<Array<{
nodeType: 'llm',
config: {
provider: 'deepseek',
model: 'deepseek-chat',
model: 'deepseek-v4-flash',
prompt: '请总结以下内容100字以内{{input}}',
temperature: 0.5,
max_tokens: 500
@@ -4715,7 +4811,7 @@ const configTemplates = ref<Array<{
nodeType: 'llm',
config: {
provider: 'deepseek',
model: 'deepseek-chat',
model: 'deepseek-v4-flash',
prompt: '请把下列内容翻译成英文:{{input}}',
temperature: 0.3,
max_tokens: 1000
@@ -5138,7 +5234,7 @@ const applyTemplate = () => {
if (t === 'llm') {
if (templateSelection.value === 'llm_summary') {
selectedNode.value.data.provider = selectedNode.value.data.provider || 'deepseek'
selectedNode.value.data.model = selectedNode.value.data.model || 'deepseek-chat'
selectedNode.value.data.model = selectedNode.value.data.model || 'deepseek-v4-flash'
selectedNode.value.data.prompt = '请总结以下内容100字以内{text}'
selectedNode.value.data.temperature = 0.5
} else if (templateSelection.value === 'llm_translate') {
@@ -5218,12 +5314,12 @@ const applyTemplate = () => {
if (t === 'llm') {
if (templateSelection.value === 'llm_extract') {
selectedNode.value.data.provider = selectedNode.value.data.provider || 'deepseek'
selectedNode.value.data.model = selectedNode.value.data.model || 'deepseek-chat'
selectedNode.value.data.model = selectedNode.value.data.model || 'deepseek-v4-flash'
selectedNode.value.data.prompt = '请从以下文本中提取关键信息JSON格式{text}'
selectedNode.value.data.temperature = 0.3
} else if (templateSelection.value === 'llm_classify') {
selectedNode.value.data.provider = selectedNode.value.data.provider || 'deepseek'
selectedNode.value.data.model = selectedNode.value.data.model || 'deepseek-chat'
selectedNode.value.data.model = selectedNode.value.data.model || 'deepseek-v4-flash'
selectedNode.value.data.prompt = '请将以下内容分类为:正面/中性/负面。内容:{text}'
selectedNode.value.data.temperature = 0.2
}
@@ -5431,7 +5527,7 @@ const handleDrop = (event: DragEvent) => {
// LLM节点默认配置
...(isLLMNode && !isTemplateNode ? {
provider: 'deepseek',
model: 'deepseek-chat',
model: 'deepseek-v4-flash',
prompt: '请处理用户请求。',
temperature: 0.5,
max_tokens: 1500,
@@ -5441,7 +5537,7 @@ const handleDrop = (event: DragEvent) => {
// 模板节点默认配置
...(isTemplateNode ? {
provider: 'deepseek',
model: 'deepseek-chat',
model: 'deepseek-v4-flash',
prompt: '',
temperature: 0.7,
max_tokens: 1500,
@@ -7757,6 +7853,8 @@ onMounted(async () => {
// 加载节点模板列表
loadNodeTemplates()
// 加载模型配置列表用于LLM节点快速选择已配置模型
loadConfiguredModels()
// 加载测试用例
loadTestCases()
// 加载配置模板

View File

@@ -108,7 +108,21 @@
</el-select>
</el-form-item>
<el-form-item label="模型名称" prop="model_name">
<el-input v-model="form.model_name" placeholder="例如: gpt-3.5-turbo, deepseek-chat" />
<el-select
v-model="form.model_name"
placeholder="选择或输入模型名称"
filterable
allow-create
default-first-option
style="width: 100%"
>
<el-option
v-for="name in providerModelOptions"
:key="name"
:label="name"
:value="name"
/>
</el-select>
</el-form-item>
<el-form-item label="API密钥" prop="api_key">
<el-input
@@ -164,7 +178,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import MainLayout from '@/components/MainLayout.vue'
import {
@@ -207,6 +221,20 @@ const testing = ref(false)
const testResult = ref<any>(null)
const currentTestConfig = ref<ModelConfig | null>(null)
const providerModelOptions = computed(() => {
const provider = form.value.provider
if (provider === 'openai') {
return ['gpt-4o', 'gpt-4-turbo-preview', 'gpt-4', 'gpt-3.5-turbo']
}
if (provider === 'deepseek') {
return ['deepseek-v4-flash', 'deepseek-v4-pro', 'deepseek-chat', 'deepseek-reasoner', 'deepseek-coder']
}
if (provider === 'anthropic') {
return ['claude-3-5-sonnet-latest', 'claude-3-opus-20240229']
}
return []
})
// 表单验证规则
const rules = {
name: [

View File

@@ -337,7 +337,7 @@ const formData = ref({
description: string
}>,
provider: 'deepseek',
model: 'deepseek-chat',
model: 'deepseek-v4-flash',
temperature: 0.7,
max_tokens: 1500,
is_public: false
@@ -414,7 +414,7 @@ const handleCreate = () => {
prompt: '',
variables: [],
provider: 'deepseek',
model: 'deepseek-chat',
model: 'deepseek-v4-flash',
temperature: 0.7,
max_tokens: 1500,
is_public: false
@@ -434,7 +434,7 @@ const handleEdit = (row: any) => {
prompt: row.prompt,
variables: row.variables ? JSON.parse(JSON.stringify(row.variables)) : [],
provider: row.provider || 'deepseek',
model: row.model || 'deepseek-chat',
model: row.model || 'deepseek-v4-flash',
temperature: parseFloat(row.temperature) || 0.7,
max_tokens: row.max_tokens || 1500,
is_public: row.is_public || false