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

@@ -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)