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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user