fix: 修复 Agent 流式 tool_call arguments 拼接错误及并行工具调用串扰问题#1355
fix: 修复 Agent 流式 tool_call arguments 拼接错误及并行工具调用串扰问题#1355CodFrm merged 7 commits intorelease/v1.4-agentfrom
Conversation
There was a problem hiding this comment.
Pull request overview
该 PR 聚焦于 Agent 流式输出中 tool_call 的两个核心问题:首个 chunk 同时携带 name/arguments 时导致参数字符串被错误拼接(出现多余 {} 前缀),以及并发多个 tool call 时 tool_call_delta 被错误写入“最后一个工具”造成串扰。通过在流式事件中引入 index 并让各 consumer 按 id/index 精确路由,提升多 provider(OpenAI/Anthropic)下的稳定性。
Changes:
- 扩展流式事件类型:
tool_call_delta增加index?: number,用于并发 tool call 的精确匹配。 - 修复 OpenAI/Anthropic stream parser:避免首 chunk 的
arguments污染 start 事件,并透传 index/id 以支持并发路由。 - 更新多个 consumer 的 delta 归属逻辑与相关单测,覆盖并发/交错 delta 的场景。
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/pages/options/routes/AgentChat/ChatArea.tsx | 子代理分支的 tool_call_delta 改为按 id/index/running 回退匹配 |
| src/app/service/agent/service_worker/sub_agent_service.ts | 子代理 service 内同样改为三级匹配,避免 length-1 串扰 |
| src/app/service/agent/service_worker/background_session_manager.ts | 后台会话流状态更新逻辑支持按 id/index 路由 delta |
| src/app/service/agent/service_worker/background.test.ts | 新增并发 tool_call 交错 delta 按 index 正确分派测试 |
| src/app/service/agent/core/types.ts | LLMStreamEvent.tool_call_delta 增加可选 index 字段 |
| src/app/service/agent/core/providers/openai.ts | 修复首 chunk name/arguments 互斥逻辑并透传 index |
| src/app/service/agent/core/providers/openai.test.ts | 调整既有断言 + 新增并发/index 相关测试 |
| src/app/service/agent/core/providers/anthropic.ts | 通过 index→id 映射为 input_json_delta 补齐 id/index |
| src/app/service/agent/core/providers/anthropic.test.ts | 新增 input_json_delta 应携带正确 id/index 的测试 |
| // 并发 tool call 时(OpenAI 用 index 区分、Anthropic 的多个 tool_use block)length-1 会把 delta 写错工具。 | ||
| if (rc.streamingState.toolCalls.length === 0) break; | ||
|
|
||
| let target; |
There was a problem hiding this comment.
这里的 let target; 在 strict: true(tsconfig.json)下会触发隐式 any 的编译错误(Variable implicitly has an 'any' type)。建议显式标注类型(例如 ToolCall | undefined)或用 let target: ToolCall | undefined = undefined;。
| let target; | |
| let target: ToolCall | undefined = undefined; |
| case "tool_call_delta": { | ||
| if (!sa.currentToolCalls.length) break; | ||
| let t = event.id ? sa.currentToolCalls.find((x) => x.id === event.id) : undefined; | ||
| if (!t && event.index !== undefined) t = sa.currentToolCalls[event.index]; | ||
| if (!t) { | ||
| for (let i = sa.currentToolCalls.length - 1; i >= 0; i--) { | ||
| if (sa.currentToolCalls[i].status === "running") { | ||
| t = sa.currentToolCalls[i]; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| if (t) t.arguments += event.delta; | ||
| break; |
There was a problem hiding this comment.
这个分支已对「子代理」的 tool_call_delta 做了按 id/index 匹配,但同一函数后面「主消息」的 case "tool_call_delta" 仍然是 toolCalls[toolCalls.length - 1] 追加(见同文件后续 switch),并发 tool_call 仍会串扰。建议把主消息分支也改成同样的三级匹配逻辑(id → index → 最新 running)。
| // 拼接后应等同 LLM 真正要发的(就算首 chunk 有 "{}",也应被后续覆盖式语义接受) | ||
| // 注意:如果模型真的先发 "{}" 再发别的 JSON,整体不是合法 JSON —— 这是模型问题, | ||
| // 但至少我们不在 start 事件里把 "{}" 当成 args 的 prefix。 | ||
| expect(joined.startsWith("{}")).toBe(true); // 原样透传 | ||
| }); |
There was a problem hiding this comment.
这段测试的注释/用例名写的是“arguments='{}' 不应污染后续 args”,但断言却是 expect(joined.startsWith("{}")) 为 true(原样透传)。建议要么改用例名/注释以匹配当前预期,要么补充断言验证真正想保证的行为(例如 start 事件不携带占位 {}、或最终可解析 JSON 等)。
| const toolUseByIndex = new Map<number, { id: string }>(); | ||
|
|
There was a problem hiding this comment.
这里新增了 toolUseByIndex 用于 index→id 映射,但当前实现只在 content_block_start 里 set,在 content_block_stop / message_stop 等结束事件里没有清理对应 index。这会导致 map 在长会话里持续增长,并且如果 index 在同一连接生命周期内被复用可能会拿到过期 id。建议在收到 content_block_stop(可用 json.index)时执行 toolUseByIndex.delete(json.index),并在 message_stop 时清空 map。
Code reviewFound 2 issues:
scriptcat/src/pages/options/routes/AgentChat/ChatArea.tsx Lines 283 to 289 in 2f5b1b5
scriptcat/src/app/service/agent/service_worker/llm_client.ts Lines 159 to 170 in 2f5b1b5 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
- background_session_manager.ts: target 添加 ToolCall | undefined 类型,避免 strict 模式下隐式 any
- ChatArea.tsx: 主消息分支 tool_call_delta 同步改为 id/index/running 三级匹配
- openai.test.ts: 重写 "{}" 用例的断言与名称,明确 start 事件 args 为空、首 chunk args 作为 delta
- anthropic.ts: content_block_stop 清理对应 index,message_stop 清空 toolUseByIndex,顺带去掉 diff-annotation 注释
- llm_client.ts: 移除单指针 currentToolCall 模型,改为 push-then-match 三级匹配,修复执行路径在并发 tool_call 下的参数串扰
问题描述
在 Agent Chat 界面中,工具调用的参数(arguments)显示异常,开头出现多余的
{}前缀,例如:根本原因有两处:
问题一:
parseOpenAIStream的if/else if互斥逻辑(providers/openai.ts)部分 API 网关(OpenRouter、Azure 某些部署、本地 vllm)在首个 chunk 中会同时携带
name和arguments: "{}"(占位符)。tool_call_start携带"{}"后,后续真实 JSON 通过tool_call_delta追加,最终"{}" + '{"description":...}' = '{}{...}',与截图完全吻合。问题二:
tool_call_delta按length-1匹配(多处 consumer)background_session_manager.ts、ChatArea.tsx、sub_agent_service.ts中处理tool_call_delta时,均使用toolCalls[toolCalls.length - 1]来找目标工具,而非按id配对。OpenAI 并行返回多个 tool_call 时(用index区分),交错到达的 delta 会被写入错误的工具。另外,Anthropic parser 的
tool_call_delta固定 emitid: "",导致按 id 配对完全失效。变更内容
src/app/service/agent/core/types.tsLLMStreamEvent中tool_call_delta新增可选字段index?: number,用于并行工具调用的精确匹配src/app/service/agent/core/providers/openai.tsif/else if改为两个独立的if,tool_call_start的arguments永远为空字符串arguments同样作为tool_call_delta发出tool_call_delta透传tc.index,供 consumer 精确匹配src/app/service/agent/core/providers/anthropic.tstoolUseByIndex: Map<number, { id: string }>追踪每个content_block的 index 与 id 对应关系input_json_delta发出的tool_call_delta现在携带正确的id和index(原来id固定为"")content_block_stop时清理对应 index 条目src/app/service/agent/service_worker/background_session_manager.tstool_call_delta匹配逻辑改为:按id精确匹配 → 按index匹配 → fallback 最近status=running的工具src/pages/options/routes/AgentChat/ChatArea.tsxtool_call_delta分支采用相同的三级匹配逻辑,替换原length-1写法src/app/service/agent/service_worker/sub_agent_service.tsrunSubAgentCore中tool_call_delta分支同上,采用三级匹配逻辑测试更新
openai.test.ts:新增并行 tool_call 按 index 分派测试;更新两个因 parser 行为变更而需调整断言的旧测试(event 数量 +1,tool_call_start.arguments改为断言空字符串)anthropic.test.ts:新增input_json_delta携带正确 id 和 index 的测试background.test.ts:新增并行 tool_call 交错 delta 按 index 正确分派的测试不受影响的文件
src/types/scriptcat.d.ts/scriptcat.zh-CN.d.ts:公开给 userscript 的StreamChunk接口不含tool_call_delta,无需修改src/app/service/content/gm_api/cat_agent.ts:processStream不处理tool_call_delta事件,无需修改ToolCall类型)不变