fix: v0.6.2 修复工具调用不稳定与会话停止 — 纯 tool_calls 轮丢失 assistant 消息导致 API 400
【根因(main.log 实证)】 19:04 / 19:05 / 19:06 三次会话终止均为同一报错: DeepSeek 400 "Messages with role 'tool' must be a response to a preceding message with 'tool_calls'" 缺陷链:engine 主循环仅在 step.thought 存在(该轮有文本或思考内容)时才 将 assistant 消息加入请求历史。当模型发起纯工具调用(零文本零思考 — DeepSeek 高频行为)时: - assistant(tool_calls) 消息不进 messages - 但 tool 结果消息照常 push → 下一轮请求出现孤立 tool 消息 → 协议 400(不可重试)→ 会话 ERROR 终止 "不稳定" = 模型每轮是否附带文本是概率性行为:带文本正常,纯调用必崩。 DB 持久化侧同源缺陷(if (!step.thought) continue)导致这些步骤的 assistant 与 tool 结果全部不落库 — 重启后工具上下文丢失,模型重复调用。 【修复】 - engine.ts: 有 toolCalls 的轮次必 push assistant(content=null,C-6 规范) - agent.ts: 持久化条件同步修复(无 thought 但有 toolCalls 的步骤落库) - 回归测试: 纯 tool_calls 轮后第二次请求中 tool 消息前必须是带 tool_calls 的 assistant(请求契约断言,engine-toolchain.test.ts) 【纵深防御 — 孤立 tool 消息过滤】 - openai-format.ts(DeepSeek/Agnes/MiMo/OpenAI 四家共享): 构建请求时 按 tool_call_id 配对过滤孤立 tool 消息(任何来源的历史污染不再 400 死锁) - anthropic.adapter.ts: tool_use/tool_result 同策略配对过滤 - 单测 ×6: 正常配对保留 / 孤立丢弃 / id 不匹配丢弃 / 多轮配对 / includeImages 原位转换 / 非 vision 静默丢弃 【多模态索引对齐收敛】 4 家 adapter 的 images 处理循环原按未过滤的 nonSystemMsgs[i-1] 对齐索引, 孤立 tool 过滤引入后会错位 — 统一收进 buildOpenAICompatibleMessages (includeImages 参数,基于 sanitized 序列原位转换),4 家 adapter 删除 各自的索引对齐循环(DeepSeek vision 判断 / OpenAI 推理模型拒绝保留在 adapter)。 【终止原因可见化】 MAX_ITERATIONS / TIMEOUT 终止此前无任何提示(用户感知"会话直接停止")— 前端 DONE 事件非 completed 终止原因显示为 system 消息。 【v0.6.1 回归缓解】 web_fetch timeoutMs 120s → 240s:浏览器回退串行化后并发 3 个排队最坏 ~127.5s,旧值让排队末位抓取被工具超时杀掉(表现为抓取不稳定)。 【验证】 lint 0/0;typecheck 双工程 0 错误;test:electron 259/259(+7); electron-vite build 成功
This commit is contained in:
+12
-5
@@ -549,8 +549,13 @@ export function registerAgentHandlers(ctx: IPCContext): void {
|
||||
}
|
||||
|
||||
// 保存每轮迭代的 assistant 消息到数据库(含思考内容和工具调用)
|
||||
// 崩溃修复(会话停止根因的持久化侧): 原 `if (!step.thought) continue;` 把
|
||||
// 纯工具调用轮(零文本零思考)整个跳过 — assistant 与 tool 结果都不落库。
|
||||
// 后果:① 重启后历史缺失工具上下文(模型"忘记"做过什么,重复调用 →
|
||||
// 表现为工具调用不稳定);② 与 engine 运行时缺陷同源。现改为:
|
||||
// 有 thought 或有 toolCalls 的步骤都保存。
|
||||
for (const step of output.iterations) {
|
||||
if (!step.thought) continue;
|
||||
if (!step.thought && !(step.toolCalls && step.toolCalls.length > 0)) continue;
|
||||
|
||||
const toolCallsWithResults = step.toolCalls?.map((tc) => {
|
||||
const result = step.toolResults?.find((r) => r.toolCallId === tc.id);
|
||||
@@ -567,18 +572,20 @@ export function registerAgentHandlers(ctx: IPCContext): void {
|
||||
|
||||
// 只有当有内容、思考内容或工具调用时才保存
|
||||
if (
|
||||
step.thought.content ||
|
||||
step.thought.reasoningContent ||
|
||||
step.thought?.content ||
|
||||
step.thought?.reasoningContent ||
|
||||
toolCallsWithResults?.length
|
||||
) {
|
||||
// C-6 修复: assistant 消息仅有 tool_calls 时 content 必须为 null(而非空字符串)
|
||||
const assistantContent =
|
||||
toolCallsWithResults?.length && !step.thought.content ? null : step.thought.content;
|
||||
toolCallsWithResults?.length && !step.thought?.content
|
||||
? null
|
||||
: (step.thought?.content ?? null);
|
||||
sessionService.saveMessage({
|
||||
sessionId,
|
||||
role: 'assistant',
|
||||
content: assistantContent,
|
||||
reasoningContent: step.thought.reasoningContent || undefined,
|
||||
reasoningContent: step.thought?.reasoningContent || undefined,
|
||||
toolCalls: toolCallsWithResults,
|
||||
iteration: step.iteration,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user