Skip to content

Fix/truncate oversized tool args #200

Open
lordquest wants to merge 5 commits into
itmisx:mainfrom
lordquest:fix/truncate-oversized-tool-args
Open

Fix/truncate oversized tool args #200
lordquest wants to merge 5 commits into
itmisx:mainfrom
lordquest:fix/truncate-oversized-tool-args

Conversation

@lordquest

Copy link
Copy Markdown
Contributor

🐛 fix(agent): 发送前截断超大工具参数,规避 Novita 400 与 llama.cpp 500

背景与问题

deepx 在把模型生成的工具调用(tool_calls)发回 chat/completions 请求时,会原样序列化 function.arguments 字符串,没有任何体积限制。当模型把整篇大文件内容内联进工具参数时,会触发两类不同后端的同类故障:

  1. OpenRouter(tencent/hy3:free,Novita 独供后端) 直接拒单:HTTP 400 ... invalid_request_error

  2. 本地 llama.cpp(如 Agents-A1-MTP-APEX)HTTP 500:parse error at line 1, column 27456 ... missing closing quote

两者根因相同:单个工具调用的 arguments 过大(实测约 27KB 的一整页 HTML 被作为 Write 参数内联)。

修复方案

agent/llm.go 新增一个轻量护栏,在每次发请求前对 convo 里的工具参数做体积裁剪:

  • 常量 maxToolArgBytes = 16 * 1024(经验阈值:正常参数远小于此,只有"整篇文件内联"才会触顶,后续易调)。

  • 函数 truncateToolArgs(convo):遍历消息,对任一 tool_calls[*].function.arguments 超过阈值的,替换为一个合法 JSON 占位:{"_deepx_truncated":true,"original_bytes":N}

  • 集成到三处构造请求的入口:CallOnce(非流式文本)、CallWithTools(压缩摘要)、streamAttempt(主循环流式,且顺序为 truncate → sanitize,先截后消毒)。

关键设计点

  • 替换成合法 JSON,而非从字符串中间硬切断。硬切断会产生非法 tool_calls,反而触发另一种 400;占位 JSON 保证请求仍是合法结构。

  • 作用于副本,不改原始对话truncateToolArgs 复制 convo 与 ToolCalls 切片,原始历史不动——工具此前已用完整参数执行过、结果在后续 tool 消息里,截断历史无害。

  • 只裁工具参数,不裁消息内容 / tool 结果,改动面最小、对任务正确性影响最低。

  • 保留 tool_call ID:截断只改 Arguments,不动 ID,因此截断后的 tool_call 仍能通过 sanitizeToolPairs 配对,不会被当作失配剥掉(这是 streamAttempt 里 truncate→sanitize 顺序正确性的前提,已用测试覆盖)。

验证

  • go build ./agent/go vet ./agent/go test ./agent/ 全绿。

  • 复现任务(读取大 HTML → 原样写回)下:修复前某 Write 参数 argLen≈27455;修复后该参数变为约 45 字节的占位 JSON,HTTP 400/500 不再出现,任务仍能正常完成(工具结果消息保留)。

  • 普通小参数任务不受影响(arguments< 16KB,护栏对原始对话为 no-op)。

新增测试 agent/truncate_test.go(6 例)

测试 | 验证点 -- | -- TestTruncateNoOpWhenSmall | 小参数不截断、返回 truncated=false、原始 convo 未被改动(走副本) TestTruncateLargeArg | 单条超大→替换为占位;占位是合法 JSON;original_bytes 等于原字节数;原 convo 不动 TestTruncateBoundary | 边界:恰好等于阈值不截 / 阈值+1 截(防 off-by-one) TestTruncateMixedSizesSameMessage | 同一条 assistant 消息内多个 tool_calls,只截大的、保留小的,且 ID 保留 TestTruncateOnlyTargetsOversizedMessages | 多条消息只改含超大参数的那条,其余原样 TestTruncateSurvivesSanitize | 截断后仍能通过 sanitizeToolPairs 配对,且整条消息可正常序列化(避免 400)

取舍与局限(请评审时重点关注)

  1. 历史可读性:被裁的参数在上下文中变成占位,模型日后"回忆刚才写了什么"会看不到原参数(但工具结果消息仍在,任务正确性不受影响)。这是为修 400/500 换来的取舍。

  2. 对 OpenRouter 是直接修复;对 llama.cpp 是缓解而非绝对根治。本地模型的 500 发生在"模型生成"侧(deepx 收不到合法 JSON 也就无从裁剪),本修复通过把上下文里那段 27KB 历史参数缩掉,降低模型再次尝试生成超大参数的概率与上下文压力,从而减少 500,但不能 100% 保证。要更彻底,需进一步限制消息内容 / tool 结果体积(更侵入,留作后续)。

  3. schema 校验风险:占位 JSON 不遵守该工具的参数 schema。主流提供商会接受历史 assistant 消息里的 tool_calls(不按 schema 校验历史),但若有端点严格校验历史 tool_calls,可能另报 400——目前未观察到,列为已知风险。

备选方案(已考虑未采用)

  • provider_routing 忽略 Novita:对 hy3:free 无效(独供→404),且没碰根因。

  • 直接丢弃超大工具调用:过于激进,会丢失工具执行本身的上下文。

  • 在工具层(如 Write)限制入参:能治本但改动分散在各工具,不如在发送侧统一拦截通用。

改动文件

  • agent/llm.go:新增 maxToolArgBytes 常量与 truncateToolArgs 函数;在 CallOnce / CallWithTools / streamAttempt 三处发请求前接入。

  • agent/truncate_test.go:新增上述 6 个单元测试。

模型把整页约 27KB 文件内容内联进 Write/Edit 的 arguments 时,会触发
OpenRouter/Novita 的 400 (invalid_request_error) 与本地 llama.cpp 生成
超大参数时 JSON 断尾的 500。

新增 maxToolArgBytes 常量与 truncateToolArgs 函数:对超过阈值的
tool_calls[*].function.arguments 替换为合法 JSON 占位
{"_deepx_truncated":true,"original_bytes":N},作用于副本、不改原始对话。
接入 CallOnce / CallWithTools / streamAttempt 三处发请求入口。

占位保证请求体仍是合法 JSON(避免硬截断产生非法 tool_calls 被再拒),
并保留 tool_call ID 以便后续 sanitizeToolPairs 仍能配对。

新增 agent/truncate_test.go 覆盖:小参数 no-op、单条超大、边界(恰好
阈值不截/阈值+1 截)、同消息混合大小、多消息定向、截断后过 sanitize
配对、占位可序列化。
@lordquest

Copy link
Copy Markdown
Contributor Author

这二天开发hf-webui-downloader 发现的问题, 这个改动验证过了, 但是未必是最优的改法. 可能需要你好好审核一下.

@itmisx

itmisx commented Jul 21, 2026

Copy link
Copy Markdown
Owner

@lordquest 我没太理解,你的修复方案和问题好像有点不匹配吧

@lordquest

Copy link
Copy Markdown
Contributor Author

@lordquest 我没太理解,你的修复方案和问题好像有点不匹配吧

问题应该是工具调用(tool_calls)一次塞进去太多东西 要求翻译(一个27kb)大小的网页, 导至openrouter API 返回 400. 我改用本地部署大模型 一样不过 (返回500错误) . 其实就应该限制 工具调用(tool_calls) 在一次发送请求中不能太大就行了. 这 个地方我不熟, 要怎么改完全丢给AI去分析的.

@itmisx

itmisx commented Jul 21, 2026

Copy link
Copy Markdown
Owner

@lordquest 下次出错,可以截个图,包括状态栏的上下文占用等信息,我看下

@lordquest

Copy link
Copy Markdown
Contributor Author

@lordquest 下次出错,可以截个图,包括状态栏的上下文占用等信息,我看下

昨天忘了截图了, 和上下文的占用量没有关系, 我很清楚 的记得那个时时候上下文占用量大约在131k左右, 一开始我也怀疑这一点, 但是手动压缩之后也是一样. 因为不好分析openrouter服务器的日志, 只是个翻译任务, 用本地AI也是一样, 但是本地AI也报500错误, 把日志丢AI分析结论和openrouter的问题是一样的.
在那次对话中是必现的, 一定会卡400, 无法进行下去. 后面用已经规避的版本, 对话已通过并完成了. 不知道还能不能用那个会话复现, 我晚上回去试试.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants