Skip to content

[Bug]: 流式工具调用续片带空串 id/name 时累加器丢弃参数片段,导致 "Arguments are invalid JSON" #3225

Description

@Priestess-ai-0

简述

部分按 OpenAI 流式规范分片发送工具调用参数、且续片把 id/name 写成空字符串 "" 的模型(如 glm-5.2),其工具调用会被 tool_call_accumulator 判定为 Arguments are invalid JSON. 而整轮失败重试。根因不在模型:模型实际发出了合法 JSON,但累加器只保留了每个工具调用的第一片 {,后续参数片段被丢弃。

环境

  • OpenBitFun Desktop: 1.0.1(Windows)
  • 模型: glm-5.2(provider: sensenova,OpenAI 兼容流式)
  • 出现会话: 20260923T213930、20260924T163057(前者参数解析失败 9 次,后者 8 次)

复现

让 glm-5.2 在一轮里并行发起两个工具调用(如 ExecCommand + Glob)。app.log 出现:

[ERROR][openbitfun_agent_stream::tool_call_accumulator] Tool call arguments parsing failed at boundary=finish_reason: tool_id=call_9072c7daa3af466782cee2ea, tool_name=Glob, raw_len=1, error=EOF while parsing an object at line 1 column 1, parse_error_category=eof, parse_error_is_eof=true, completion=Unknown
[ERROR][openbitfun_agent_stream] SSE Error: Has invalid tool calls
[ERROR][openbitfun_core::agentic::tools::pipeline::tool_pipeline] Tool execution failed: error=Validation error: Arguments are invalid JSON.
[WARN][openbitfun_core::agentic::execution::round_executor] Retrying stream because provider returned only invalid tool arguments: ... local_retry=1/10

关键:raw_len=1 —— 累加器里每个工具调用只剩 1 个字符 {。

根因(SSE 证据)

模型把同一工具调用的 arguments 拆成多个 SSE chunk 发送,首片带 id/name,续片把它们写成空串:

17: tool_calls[0] id="call_b45..." name="ExecCommand" arguments="{"
18: tool_calls[0] id=""            name=""            arguments="\"cmd\": "
19: tool_calls[0] id=""            name=""            arguments="\"Get-"
...(续片累积命令字符串)
28: tool_calls[0] id=""            name=""            arguments="\"}"
29: tool_calls[1] id="call_907..." name="Glob"       arguments="{"
30: tool_calls[1] id=""            name=""            arguments="\"pattern\": "
33: tool_calls[1] id=""            name=""            arguments="}"
34: finish_reason="tool_calls"

把续片 arguments 顺序拼接,得到的是合法 JSON:

  • ExecCommand → {"cmd": "Get-Date; Write-Output \"PowerShell 工具运行正常"}
  • Glob → {"pattern": "*.md"}

但累加器只保留了首片的 {,续片全部丢失 → 解析到 EOF while parsing an object at line 1 column 1。

为什么"有些模型"出问题(对比)

同一台机器上 qwen3.8-27b-uncensored 基本正常。其续片是把 id/name 字段省略(而非空串):

"tool_calls":[{"index":0,"type":"function","function":{"arguments":"{"}}]
"tool_calls":[{"index":0,"type":"function","function":{"arguments":"\"file_path\":"}}]

glm-5.2 的续片则是空串:

"tool_calls":[{"index":0,"id":"","type":"function","function":{"name":"","arguments":"\"cmd\": "}}]

两种写法都符合 OpenAI 流式语义,但实现细节不同:省略字段 → serde 反序列化为 None,累加器走续片追加分支(正常);空串 "" → 累加器按 id 查归属工具调用,查不到(真 id 是 call_xxx),于是丢弃该参数片段,缓冲区永远停在 {。

(注:9/21 那次 qwen 的唯一失败是另一类:boundary=graceful_shutdown、raw_len=67、completion=Interrupted,是流被中途打断导致参数真的截断,且已累积到 67 字符,反证 qwen 的累加本身是工作的。)

期望行为

续片无论 id/name 是省略还是空串,都应视为"无 id/无 name"的续片,按 index 追加到对应工具调用的参数缓冲区。两种写法都应正确合并出完整 JSON。

修复建议(openbitfun_agent_stream::tool_call_accumulator)

  1. 续片归属应按 index 匹配,而不是按 id;或
  2. 当 id 为空串 / name 为空串时,等价于 None,走续片追加分支,而非当成新 id 去查表。

影响范围

凡按 OpenAI 规范分片发送 arguments、且续片带空串 id/name 的模型都会 100% 失败并触发整轮重试(local_retry 最多 10 次)。续片字段被省略的模型不受影响。已用本机真实 SSE 流量验证(上述日志片段为原始截取,仅脱敏用户路径)。

Activity

self-assigned this
on Oct 8, 2026

wsp1911 commented on Oct 8, 2026

@wsp1911
Collaborator

感谢提供详细的问题描述。我们核对了 1.0.1 和当前版本的代码:工具调用续片已经按 index 定位,空字符串 id 会被视为没有提供 ID,随后仍会追加 arguments;空字符串 name 也不会阻止参数累积。

我们还使用实际的 OpenAI 解析器和流处理器做了分片回放:续片省略 id/name 或将它们写为空串,两个工具依次发送或交错发送,都能完整重建参数。因此,目前还无法确认问题原因,需要查看同一次失败请求的原始 SSE 数据。

方便在一个简单的示例项目中重新测试,并提供原始日志吗?建议:

  1. 新建一个仅包含简单示例 README.md 的目录,在 OpenBitFun 中打开,并新建测试对话。

  2. 使用出现问题时相同的模型,例如发送:

    请在同一轮回复中调用 Glob 查找当前目录下的 *.md 文件,并调用 ExecCommand 执行 Get-Date。

  3. 如果复现出失败,从设置中显示的本次启动日志目录找到 app.log,提供参数解析错误、完整的 SSE history 段落,以及附近的重试日志。

工具调用参数 JSON 无效时,程序会自动以 ERROR 级别将 SSE 历史写入 app.log。可以搜索:

SSE Error: Has invalid tool calls

Windows 默认日志位置是:

%APPDATA%\openbitfun\logs\<启动时间>\app.log

使用示例项目和全新对话,可以尽量避免脱敏影响分析。请保留每个 SSE 分片的原始 JSON 和顺序,不要重新整理成摘要。分享前确认相关日志没有密钥或无关的私人内容即可。

raw_len=1 能说明最终缓冲区只有一个字节,但还不足以判断为什么续片没有进入该缓冲区;原始 SSE 会帮助我们继续定位。

wsp1911 commented on Oct 8, 2026

@wsp1911
Collaborator

日志示例:

[2026-10-08T16:04:46.379][tid:39][ERROR][openbitfun_agent_stream::tool_call_accumulator] Tool call arguments parsing failed at boundary=stream_end: tool_id=call_1, tool_name=tool_one, raw_len=6, error=EOF while parsing an object at line 1 column 6, parse_error_category=eof, parse_error_is_eof=true, completion=Unknown
[2026-10-08T16:04:46.482][tid:27][ERROR][openbitfun_agent_stream] SSE Error: Has invalid tool calls
[2026-10-08T16:04:46.483][tid:27][ERROR][openbitfun_agent_stream] SSE history (3 events):
     0: {"id":"chatcmpl_test","object":"chat.completion.chunk","created":800,"model":"gpt-test","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_1","type":"function","function":{"name":"tool_one"}}]},"finish_reason":null}],"usage":null}
     1: {"id":"chatcmpl_test","object":"chat.completion.chunk","created":802,"model":"gpt-test","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"type":"function","function":{"arguments":"{\"x\":1"}}]},"finish_reason":null}],"usage":null}
     2: [DONE]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions