qwen-code 工具响应预算(Final Tool Response Budget)机制深度解析:聚合截断、持久化元数据与共享终结器
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
导读
本篇文章围绕 qwen-code(终端内运行的开源 AI 编程代理)中"工具响应预算"(Final Tool Response Budget)这一核心机制展开,系统讲解它如何解决"多个截断层各自为政、缺乏共享状态,导致聚合后工具输出仍超预算"的经典问题。读完本文,你将掌握truncateToolOutputThreshold与toolOutputBatchBudget两个配置项的真实作用、生产者级预览与共享终结器(shared finalizer)的分工、max-min water-fill 预算分配算法,以及它在 Core 调度器、Headless、ACP、Agent 运行时等不同聚合边界上的落点——既有可直接落地的参数说明,也有源码级原理支撑。
问题背景:多层截断之间的"信息孤岛"
在设计 final-tool-response-budget.md 之前,工具输出在多个相互独立的层上被截断:
- Shell 输出默认在约 30K 字符处被缩短并标记为"已截断";若显式配置了
truncateToolOutputThreshold,则用它覆盖生产者触发阈值; - 通用工具输出默认在约 2K 字符处被缩短;
- Core 调度器批处理在聚合输出超过配置的批预算时执行卸载(offload)。
这些层之间不共享结构化状态:调度器把已有的截断标记视为"无需再做任何工作"的证据,于是多个各自被缩短的 Shell 结果累加起来,仍然可能超过聚合预算。文档还指出,Headless 模式会放大这一缺口——它为每个工具调用创建一个调度器,并在这些调度器之外拼接响应;交互模式同样在调度器终结之后追加重复与合成响应;ACP、Agent 与推测执行(speculative execution)则各自拥有独立的聚合边界。
该设计的目标边界非常明确:模型请求、可恢复的对话记录(resumable transcript)与工具结果记录(tool-result recording)必须包含同一个有界响应;而面向用户展示的富工具结果显示(rich user-facing tool display)明确不在范围内,可继续沿用现有结果展示。
核心不变量(Invariants)
该设计通过七条不变量定义正确性边界:
- 每个工具响应批次必须在发送给模型前的最后一个聚合边界处被终结(finalize);
- 该批次中序列化的工具输出文本,在预算为有限正值时不得超过配置的聚合字符预算;
enter_plan_mode生命周期提醒属于策略输入而非工具输出,内联保留且不计入此预算; - 若生产者已持久化输出工件(artifact),后续层复用这些路径而不是再次写入同样的生产者输出;
- 聚合终结只依据结构化内部元数据判断持久化工件能否复用,绝不从人类可读文本推断该决策;生产者本地的哨兵处理仅是既有截断器的兼容细节;
- 终结保留响应顺序与非文本部分,只允许缩短
functionResponse.response.output、functionResponse.response.error以及属于工具响应批次的顶层文本 part; - 终结后的 parts 也是回放(replay)与恢复(resume)所记录的 parts;
- 工具展示与模型响应相互独立。
持久化元数据:persistedOutputFiles
ToolResult与ToolCallResponseInfo新增一个内部可选字段persistedOutputFiles,其取值语义为:
undefined:生产者尚未做出持久化决策;[]:已做出决策,但没有可复用的文件;- 非空数组:生产者持久化的输出工件在这些路径上可用。
该字段不参与hook 序列化、ACP 负载、JSON 输出、遥测属性或持久化的 UI 元数据。由 hook 重建的响应不会继承元数据,除非运行时显式拷贝。
生产者级预览(Producer-level Preview)
在最终聚合之前,各生产者仍负责"控制正常模型预览 + 一次性持久化完整输出":
- Shell:默认 30K 字符触发(shell.ts 中
DEFAULT_SHELL_OUTPUT_THRESHOLD = 30_000);显式配置truncateToolOutputThreshold时优先(getShellOutputThreshold见 shell.ts);返回约4K的头尾预览,使退出/错误信息保持可见(previewChars: Math.min(4000, outputThreshold),keep: 'both',见 shell.ts); - MCP:保留当前的大输出触发阈值(
DiscoveredMCPTool.maxOutputChars为 500_000,约全局预算的 10 倍,见 mcp-tool.ts),完整转换结果保留给用户端展示,模型预览约2K(threshold: 500_000, previewChars: 2000, lines: Infinity,见 mcp-tool.ts); - 通用持久化:主写入器与回退写入器均返回实际写入的路径。
这些预览不是聚合约束——一个已被缩短的响应在终结时仍可能被再次缩短。
共享终结器(Shared Finalizer)的设计与源码实现
入口与总体流程
共享终结器接收"保持原始顺序的响应列表 + 配置的聚合预算",其核心实现在 tool-response-finalizer.ts 的finalizeToolResponses(L324-L488):
- 读取预算
config.getToolOutputBatchBudget?.()(默认见下文配置章节); - 预算为无限或非正时直接返回(no-op,但仍做边界观测);
- 用
collectTextSlots收集所有可计费的文本槽位(顶层text、output、error三类字段),累加总量;若总量 ≤ 预算则直接返回; - 用
allocateTextBudget做确定性分配; - 对需要缩短的条目执行持久化(复用已有路径,或调用
persistAndTruncateToolResult至多一次); - 用
replaceTextSlots应用截断结果,并通过toolResponseTextLength重新计算contentLength(调度器侧见 coreToolScheduler.ts)。
max-min water-fill 预算分配
allocateTextBudget(L157-L187)实现了一个确定性的 max-min 水填充算法:在多个模型面向文本字段之间均分预算,同时允许小字段保留完整内容(长度 ≤ 均分份额的字段先被"固定"),剩余预算继续在较大字段间均分,余数按序逐个分配。这保证了大响应与小响应共存时,小字段不被无谓截断,大字段以可控方式共享剩余预览容量。
头尾预览与持久化路径引用
fitText(L210-L250)在字段必须缩短时,生成形如:
Tool output truncated. Persisted tool-output artifact: <path>(多文件时逐行列出)的头部,随后按head 1/5 + tail 4/5的结构输出预览,中间以\n...\n分隔——这一比例与truncation.ts中keep: 'both'的默认行为一致(truncation.ts)。若预算连头部都放不下,则只输出截断的头部。
Unicode 代理对保护
所有切片操作都经过sliceStartWithoutBrokenSurrogate与sliceEndWithoutBrokenSurrogate(L189-L208):截断点若落在高位代理(0xD800–0xDBFF)上则回退一位,若落在低位代理(0xDC00–0xDFFF)上则前进一位,确保永不劈开代理对,避免产生畸形 UTF-16 序列。
硬上限兜底(no-I/O 安全帽)
终结流程的最后一步是不做任何 I/O 的硬上限(hard-cap)传递,它只对文本做内存内缩短,因此持久化失败也不可能破坏"请求大小不变量"。在聊天发送边界,enforceFunctionResponseBudget(L305-L322)对工具响应字段应用同样的 no-I/O 安全帽,作为对遗漏外层聚合边界的未来调用方的保护,正常情况下应为 no-op(实现于 llm-chat.ts)。
enter_plan_mode语义例外
enter_plan_mode是唯一的语义例外:其成功的 function-response 输出会安装活动规划策略,截断它会改变执行规则而非缩短诊断输出。终结器与发送边界守卫通过工具名识别该输出并将其排除在预算分配之外;同批次中的失败文本与所有普通输出仍保持有界(实现见getPlanModeLifecyclePrefix在 tool-response-finalizer.ts 的使用)。
运行时聚合边界(Runtime Boundaries)
不同执行模式在何处调用终结器,决定了预算何时生效:
- Core 调度器:在
PostToolBatchhook 之前终结一次(约束 hook 输入),hook 之后再终结一次(约束 hook 输出)——见 coreToolScheduler.ts 与applyBatchOutputBudget(L6671-L6711); - 交互模式:按原始序号合并可执行、重复与合成响应,然后在记录与提交前执行外层终结;
- Headless 模式:收集整个回合(含重复、跳过、取消与已执行调用),提交前统一终结一次;
- ACP:收集完整工具调用回合,在转录记录前终结,并向后继消息返回同一组 parts;规范与模型面向 parts 保持不变,而即时 ACP 展示事件中的合格文本字段可在传输边界投影到固定 JSON UTF-8 字节预算;
- Agent 运行时与推测式后续:在发出模型面向结果或追加历史前终结其聚合;
- 聊天发送边界:仅对工具响应字段应用 no-I/O 安全帽。
配置参数详解
truncateToolOutputThreshold
类型为number(见 config.ts)。Shell 生产者的显式截断阈值,覆盖默认 30K 触发。注意getTruncateToolOutputThreshold在配置值 ≤ 0 时返回Infinity(即禁用截断),见 config.ts;isTruncateToolOutputThresholdExplicit()用于区分"显式配置"与"使用内置默认",Shell 侧只有在显式配置时才使用该值,否则回落DEFAULT_SHELL_OUTPUT_THRESHOLD = 30_000(shell.ts)。
toolOutputBatchBudget
类型为number,默认值DEFAULT_TOOL_OUTPUT_BATCH_BUDGET = 200_000(字符),定义于 config.ts,用于getToolOutputBatchBudget()(L9052-L9057,≤ 0 时同样返回无限)。它是共享终结器的聚合字符预算,也是测试中常用的关键参数:例如 coreToolScheduler.test.ts 中大量使用toolOutputBatchBudget: 10_000验证多响应聚合截断,exec-context-budget.test.ts 使用200_000验证执行上下文预算边界。注意该预算只约束文本字段,媒体 parts 不参与计数。
持久化写入的工程细节
persistAndTruncateToolResult(truncation.ts)揭示了持久化的多层防护:
- 单文件硬上限:
MAX_FILE_SIZE_BYTES = 50MB(L21),超限跳过磁盘持久化并返回(file too large to persist)桩文本; - 会话预算:
MAX_SESSION_BYTES = 500MB(L22),且在异步 I/O 前同步预留字节数,防止并行工具调用同时通过检查;失败时回滚预留(trackToolResultBytes(-byteSize)); - 安全文件名:经
normalizeToolResultCallId规范化;无效 callId 返回(invalid callId)桩; - 原子写入:写入到
storage.getToolResultsDir()下${safeCallId}.txt,使用atomicWriteFile,权限0o600、强制模式、不跟随符号链接。
失败处理
持久化失败通过既有日志上报,且绝不阻止最终截断:返回的模型响应依然满足预算,只是可能因完整输出未成功持久化而缺少文件引用。媒体 parts 保持不动,不计入字符预算。取消(cancellation)与 hook-stop 响应与成功/失败响应一样被终结;空输出与错误字段是合法状态。单个响应超过整批预算时被独立缩减;多个大响应则通过确定性分配共享剩余预览容量(见前文 water-fill 算法)。
兼容性与非目标(Non-goals)
- 公共模型面向的 function response schema不变;既有截断文本仍然可读,但聚合终结不再依赖它;
- 既有会话仍可回放;只有新记录的工具结果获得更严格的不变量;
- 该变更不引入线上字节哈希、精确 token 核算、媒体预算、存储生命周期变更、转录迁移或新的临时文件布局——这些被明确列为独立后续工作。
测试与验证路径
仓库为这一机制提供了成体系的测试支撑,可作深入阅读入口:
- tool-response-finalizer.test.ts:终结器单元测试(分配、头尾预览、Unicode 保护、enter_plan_mode 例外等);
- tool-response-finalizer.integration.test.ts:与调度器/录制集成的端到端行为;
- coreToolScheduler.test.ts:以
toolOutputBatchBudget为参数的批聚合截断测试; - truncation.test.ts:持久化与截断工具的边界用例;
- mcp-tool.test.ts 与 shell.test.ts:各生产者预览行为的回归保护。
小结
Final Tool Response Budget 通过"生产者级预览 + 共享终结器 + 结构化持久化元数据"三层协作,把散落在 Shell、MCP、调度器、Headless、ACP 等多个边界的截断行为统一到同一个确定性预算框架内:模型请求、转录与记录拿到同一份有界文本,而用户展示层保持独立演进。对使用者而言,只需理解两个核心旋钮——生产者触发阈值truncateToolOutputThreshold与聚合预算toolOutputBatchBudget(默认 200_000 字符);对二次开发者而言,tool-response-finalizer.ts 是理解整套机制的最佳起点。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考