Qwen Code Tool-Use Summary 深度解析:用快模型为并行工具批次生成 Git 提交式摘要
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
本文基于仓库设计文档 docs/design/tool-use-summary/tool-use-summary-design.md 与用户手册 docs/users/features/tool-use-summaries.md,并结合
packages/core与packages/cli的真实实现展开。当模型一次并行发起多个Read、Grep、Bash调用时,Qwen Code 会在批次结束后异步调用配置的快模型,返回一条 git-commit-subject 风格的短标签(如Read 4 text files、Fixed NPE in UserService),用于替代默认的Tool × N头部。读完本文,你将掌握该功能的触发链路、三种开关语义、输出清洗规则、<Static>append-only 约束下的渲染设计,以及成本与隐私边界。
1. 功能概览:一条标签解决"并行工具看不懂"问题
Qwen Code 是一个运行在终端里的开源 AI 编程 Agent。当主模型在一个回合内扇出多个并行工具调用(例如同时read_file四个文件、再跑一轮grep)时,用户面对折叠后的Tool × N分组,往往需要逐个展开才能知道这批调用到底做了什么。
Tool-Use Summary 正是为此设计的 UX 增强:每个工具批次完成后,Qwen Code 向配置的快模型(fastModel)发起一次极短的调用,返回一条 git-commit-subject 风格的过去式标签,例如:
Searched in auth/Fixed NPE in UserServiceCreated signup endpointRead config.jsonRan failing tests
该调用是 fire-and-forget(即发即忘)的,与下一轮主模型的 API 流式输出并行执行,约 1 秒的延迟被主模型 5–30 秒的流式响应完全掩盖,用户感知不到任何额外等待。
设计文档在 Executive Summary 中用一张对比表系统梳理了与 Claude Code 同名能力的差异,下表摘录了最关键的几行:
| 维度 | Claude Code | Qwen Code |
|---|---|---|
| 触发点 | query.ts— 工具批次最终化之后 | use-llm-stream.ts→handleCompletedTools(同一生命周期点) |
| 生成模型 | Haiku(queryHaiku) | 配置的fastModel(经runSideQuery) |
| 子代理行为 | !toolUseContext.agentId— 仅主会话 | 隐式排除 — 子代理走agents/runtime/ |
| 输出形态 | SDK 流中的ToolUseSummaryMessage | UI 历史中的HistoryItemToolUseSummary+ 导出工厂函数供未来 SDK 使用 |
| 开关 | CLAUDE_CODE_EMIT_TOOL_USE_SUMMARIES,默认off | experimental.emitToolUseSummaries(默认on)+ 环境变量覆盖 |
| 标签后处理 | 模型原文 | cleanSummary(剥离 Markdown、引号、错误前缀;100 字符上限) |
| 会话持久化 | 仅流式,每次会话重新生成 | 仅 UI 历史;ChatRecordingService不持久化tool_use_summary |
该功能的实现并非简单照搬:Qwen Code 将默认值设为on、增加了settings.json持久化开关、加入了专门的cleanSummary清洗层,并针对 CLI 的渲染模型做了双路径展示设计。以下各节逐一深入。
2. Claude Code 参考实现:流的边界产物
设计文档首先剖析了 Claude Code 的做法,因为 Qwen Code 的实现是"行为对齐的移植"(源码注释明确写有Ported from Claude Code (services/toolUseSummary/toolUseSummaryGenerator.ts))。
2.1 参考流程
tool_batch_complete → fork queryHaiku (fire-and-forget) ↓ next_turn_stream_starts ↓ ← summary Promise resolves during streaming → ↓ await pendingToolUseSummary → yield ToolUseSummaryMessage ↓ continue with next turn关键设计决策包括:
- 无论紧凑/详情模式都生成。摘要属于流级产物,由 UI 决定是否渲染;
- 作为一等消息类型发射。
tool_use_summary与user、assistant、tool_result并列,通过precedingToolUseIds字段让消费方关联到具体批次; - 排除子代理。子代理的输出在上游聚合,单独的批次标签只会产生主界面永远不展示的噪声;
- 默认关闭。仅环境变量门控,保证下游 SDK 未显式选择时零成本;
- 每字段 300 字符截断。覆盖最大的成本风险——单个大工具结果撑爆 prompt——同时保留足够信号。
3. Qwen Code 实现:从服务到双路径渲染
3.1 端到端数据流
Qwen Code 挂在相同的生命周期点,但渲染覆盖了ui.compactMode的两侧,让纯 CLI 用户无需任何 SDK 管道即可受益:
tool_batch_complete (handleCompletedTools) ↓ config.getEmitToolUseSummaries()? ↓ fork generateToolUseSummary (fire-and-forget) ↓ submitQuery() for next turn (streaming starts) ↓ ← summary Promise resolves during streaming → ↓ addItem({type:'tool_use_summary', summary, precedingToolUseIds}) ↓ HistoryItemDisplay renders: compactMode=false → ● <label> 独立行 compactMode=true → 隐藏;MainContent 查找注入 CompactToolGroupDisplay 头部3.2 核心源码地图
| 组件 | 文件 | 关键逻辑 |
|---|---|---|
| 服务 | packages/core/src/services/toolUseSummary.ts | generateToolUseSummary、truncateJson、cleanSummary、消息工厂 |
| 配置开关 | packages/core/src/config/config.ts#L8384-L8389 | getEmitToolUseSummaries:环境变量 → 设置 → 默认 true |
| 触发 | packages/cli/src/ui/hooks/use-llm-stream.ts#L5637-L5683 | handleCompletedTools内 fire-and-forget,resolve 后addItem |
| 全量模式渲染 | packages/cli/src/ui/components/HistoryItemDisplay.tsx#L551-L558 | !compactMode时渲染● <label>行 |
| 紧凑模式查找 | packages/cli/src/ui/components/MainContent.tsx | summaryByCallId映射 → 每个 tool_group 的compactLabel |
| 紧凑头部 | packages/cli/src/ui/components/messages/CompactToolGroupDisplay.tsx | 有标签时用<Summary> · N tools替换默认Tool × N |
| 合并处理 | packages/cli/src/ui/utils/mergeCompactToolGroups.ts | 将tool_use_summary视为紧凑模式下隐藏项以保持相邻性 |
| UI 类型 | packages/cli/src/ui/types.ts→HistoryItemToolUseSummary | { type: 'tool_use_summary', summary, precedingToolUseIds } |
说明:设计文档中记录的旧路径
useGeminiStream.ts在当前仓库中已演化为 use-llm-stream.ts,触发逻辑位于该文件的handleCompletedTools回调内。阅读源码时以当前路径为准。
3.3<Static>append-only 约束:为什么标签是独立历史项
这是该 PR 的核心架构决策——为什么全量模式下标签是独立的 history item,而不是 tool_group 上的装饰。
Qwen Code 通过 Ink 的<Static>渲染转录。<Static>是append-only的:一旦某个条目提交到终端缓冲区,Ink 不会重绘该区域,除非调用refreshStatic()清空并整体重渲染整个转录。这正是 CLI 依赖的性能模型——静态条目不会在每次按键时重绘。
现在考虑快模型调用的时序:
T0 工具批次完成,tool_group 推入历史 T0+ε tool_group 经 <Static> 渲染并提交到缓冲区 T0+1s 快模型调用返回标签在 T0+1s 时,我们无法把标签"追溯"塞进已经提交的 tool_group。两条路:
- 更新 tool_group 的 props + 调用
refreshStatic()。可行,但每个批次都会触发整个转录重绘——这是应用中最昂贵的 UI 操作之一,且肉眼可见闪烁。为一个装饰性标签不可接受。 - 把摘要渲染成自己的新历史项,追加在 tool_group 之后。
<Static>原生支持新条目干净地追加,无需重绘。
该 PR 在全量模式下选择方案 2:tool_use_summary是真实的历史条目,由HistoryItemDisplay渲染为一行暗色● <label>。
紧凑模式则不同:mergeCompactToolGroups合并连续 tool_group 时,MainContent本就会调用refreshStatic()——这是既有代码路径,重渲染合并组时可以从历史中查到标签。因此紧凑模式以头部替换的方式获得标签。为避免同一标签渲染两次(一次作为紧凑头部、一次作为尾部● <label>行),HistoryItemDisplay在compactMode为 true 时隐藏独立行:
Full mode Compact mode (with merge) ─────────── ───────────────────────── [tool_group] [merged tool_group — header replaced via lookup] ● <label> (● <label> line is hidden)3.4 开关语义:三层优先级
三个层级,按优先级解析(见 config.ts#L8384-L8389 的getEmitToolUseSummaries):
QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=0|1|true|false—— 环境变量覆盖,最高优先级;experimental.emitToolUseSummaries(settings.json)—— 默认true;- 隐式跳过—— 若
config.getFastModel()返回undefined,无论开关如何都跳过生成。无报错,无可见变化。
此外在触发端还有一层运行时保护(use-llm-stream.ts#L5643-L5667):resolve 时会做"陈旧摘要检查"——只有当前批次对应的 tool_group 仍是历史中最新一个时才addItem。若快模型调用在途期间对话已经推进到更新的批次,摘要会被丢弃,避免● <label>行落在后续内容之后(全量模式)或归属到错误的组(紧凑模式)。
3.5 输出清洗:cleanSummary的六道工序
cleanSummary对每次模型响应在写入历史前执行清洗(完整实现见 toolUseSummary.ts#L264-L302):
- 只取第一行——丢弃模型推理前导段落;
- 剥离项目符号前缀(
-、*、•)——模型有时会把标签当列表项返回; - 剥离首尾引号/反引号——通过有界
{1,10}正则(CodeQL 安全;真实标签的包裹引号不会超过几个)。字符类覆盖 ASCII 及常见 Unicode 引号对:"'\‘’“”「」『』`,兼容中文指令模型的弯引号与日式角括号输出; - 剥离前缀标签(
Label:、Summary:、Result:、Output:)——部分模型会预置这类词; - 拒绝错误消息形态——
API error: ...、Error: ...、I cannot ...、I can't ...、Unable to ...等英文,以及我无法、我不能、抱歉、无法等中文拒绝语,命中即返回空串,不产生历史条目; - 硬性 100 字符上限——移动端 UI 约在 30 字符处截断;余量用于覆盖 CJK 短语。
3.6 遥测与成本归属
生成调用设置promptId: 'tool_use_summary_generation'(在runSideQuery中以purpose: 'tool-use-summary'传递),其 token 用量在/stats中单独核算。用户可以精确看到该功能带来的增量成本,而不会与 prompt suggestions 或主会话的用量混淆。
4. 与 Claude Code 的偏差及原因
| 偏差 | 原因 |
|---|---|
| 在环境变量之外增加设置层 | Qwen Code 在 CLI 中渲染标签,用户需要持久化开关,而非每次 shell 导出环境变量 |
| 默认on而非 off | 两种显示模式下标签都立即可见;配置了fastModel的用户本就在使用快模型特性 |
专门的cleanSummary后处理 | Qwen Code 支持的 provider 比 CC 更多样,部分模型会预置Label:或加引号;在边界处归一化以保持 UI 一致 |
存储HistoryItemToolUseSummary而非发射流消息 | CLI 优先的实现;SDK 流路径是后续 PR,ToolUseSummaryMessage工厂已导出备用 |
| 尚未接入 prompt caching | 未单独配置快模型的用户,快模型常与主模型相同;共享缓存需要经forkedAgent.ts路由,作为跟进项 |
| 双渲染路径(全量内联 + 紧凑头部) | Qwen Code 默认ui.compactMode: false;没有内联全量渲染,该功能对大多数用户不可见 |
5. 用户侧配置与使用
5.1 配置快模型
标签由快模型生成——与 prompt suggestions、投机执行共用同一个fastModel。两种配置方式:
通过命令:
/model --fast qwen3-coder-flash通过settings.json:
{ "fastModel": "qwen3-coder-flash" }未配置fastModel时,摘要生成整体跳过——该功能在你配置之前不起任何作用(设计文档明确:回退到主模型是被刻意禁止的,以保证成本曲线有界)。
5.2 开关设置
| 设置 | 类型 | 默认值 | 说明 |
|---|---|---|---|
experimental.emitToolUseSummaries | boolean | true | 摘要生成总开关。关闭以禁用额外的快模型调用 |
fastModel | string | "" | 用于摘要生成的快模型(与 prompt suggestions 共享)。必填;为空则无效果 |
环境变量覆盖:QWEN_CODE_EMIT_TOOL_USE_SUMMARIES对当前会话覆盖上述设置:
QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=0或=false—— 强制关闭;QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=1或=true—— 强制开启;- 未设置 —— 使用
experimental.emitToolUseSummaries设置值。
完整示例:
{ "fastModel": "qwen3-coder-flash", "experimental": { "emitToolUseSummaries": true } }5.3 出现与不出现的条件
摘要生成需要全部满足以下条件:
experimental.emitToolUseSummaries为true(默认);- 已配置
fastModel(settings 或/model --fast); - 批次中至少有一个工具完成;
- 工具完成前回合未被中止;
- 快模型返回了非空、非错误响应。
以下情况静默跳过(无报错、无 UI 变化):
- 未配置快模型;
- 快模型调用失败、超时或返回空;
- 模型返回了明显错误形态字符串(如
Error: ...、I cannot ...)——由客户端过滤,避免展示误导性标签; - 回合在模型完成前被中止(
Ctrl+C)。
所有跳过场景下,工具组都按原有方式渲染。
5.4 子代理边界
触发点位于主会话的回合循环(use-llm-stream.ts),因此:
- ✅ Shell、MCP、文件操作,以及
Task/子代理工具调用本身(以主批次成员出现时)会被摘要; - ❌ 子代理内部的工具批次(经
packages/core/src/agents/runtime/运行)不会摘要。
包含Task工具的外层批次仍会获得标签,但快模型只看到子代理工具调用及其聚合输出——看不到子代理内部的单个工具调用。预期标签形态是Ran research-agent、Delegated file search,而不是Searched 14 files。这是有意为之:摘要子代理内部会成倍增加快模型成本,并制造主界面永不展示的噪声。
5.5 生命周期要点
三个容易误读的细节:
- 每批次只生成一次,两种显示模式共享。快模型调用在工具批次最终化时(
handleCompletedTools)恰好发生一次。之后切换Ctrl+O展开详情不会触发新调用——折叠与展开渲染都读取首次捕获的同一tool_use_summary历史条目; - 切换或恢复会话时不回填。在功能启用(或你打开开关)之前完成的 tool_group,以及在恢复的会话中(
ChatRecordingService不持久化摘要条目)不会获得标签。没有"扫描既有历史"的通道。会话中途开启该设置,只有之后的批次会显示标签; - 仅主 Agent 批次。触发点在主会话的回合循环中。
5.6 显示行为
主视图已把完成的可折叠批次折叠为单行(✓ Read 4 text files)——摘要承担了旧版逐工具列表的工作。要看完整逐工具详情,按Ctrl+O切换展开详情模式,每个工具单独渲染,摘要以尾部● <label>行出现在组下方:
╭──────────────────────────────────────────────╮ │ ✓ ReadFile a.txt │ │ ✓ ReadFile b.txt │ │ ✓ ReadFile c.txt │ │ ✓ ReadFile d.txt │ ╰──────────────────────────────────────────────╯ ● Read 4 text files对于小规模同类型批次(如Read × 3),展开态的● <label>行可能与可见工具行语义重复;若这正是你的常规工作流,可经experimental.emitToolUseSummaries: false整体关闭。
6. 数据流与隐私边界
摘要调用向快模型发送每个成功工具的名称、截断后的args与截断后的结果(每字段上限 300 字符),外加助手最近文本的前200 字符作为意图前缀。
- 若快模型与主会话模型配置在同一 provider/auth 下,数据沿主会话已使用的同一边界流动,信任范围不变;
- 若快模型来自不同 provider,工具输入与输出(可能包含
read_file读到的文件内容、shell 命令输出、MCP 工具暴露的值)会随摘要 prompt 发送到该 provider——这是严格大于主会话的数据共享范围。
两个干净的应对选项:
- 将
fastModel配置为与主会话同 provider 的模型,使摘要调用不跨越新的认证/数据边界; - 以
experimental.emitToolUseSummaries: false(或QWEN_CODE_EMIT_TOOL_USE_SUMMARIES=0)整体关闭。
300 字符/字段上限限制了暴露面但并未消除——截断窗口内工具输出中发现的密钥仍可能被发送。请以对待主模型数据边界的方式对待快模型。
7. 成本模型
每个达标的工具批次产生一次快模型调用。输入为小型固定系统 prompt 加上截断的工具输入/输出(每字段 300 字符上限);输出为单行短标签(100 字符上限,通常不超过 20 个 token)。generateToolUseSummary中runSideQuery的采样参数为maxOutputTokens: 60、temperature: 0.3,且maxAttempts: 1——标签是尽力而为的装饰品,每回合一次,瞬时故障时 7 次重试只会徒增流量而毫无收益(toolUseSummary.ts#L138-L151)。
当前仓库已知限制包括:
- 无会话持久化。
tool_use_summary不写入聊天记录 JSONL。恢复会话会丢失标签,工具组回退到通用头部渲染。优先级低:继续会话时标签会自然重新生成; - 尚无 SDK 流发射。消息工厂已导出,但 CLI 尚未将
tool_use_summary接入 SDK bridge; - 无 prompt caching。每个批次都产生一次全新输入 token 成本。绝对值可忽略(约 300 token),但每回合跑几十个批次时是可测的;
- 合并紧凑组的摘要取首个批次的标签。连续 10 个不相似批次(紧循环,非典型场景)时,合并头部只显示领头批次的意图。这是接受的权衡:合并视图中展开每批次标签比只取首个更嘈杂;
- 必须有快模型。未配置
fastModel则跳过生成。
8. 未来工作
设计文档列出了四项后续规划:
- 将
ToolUseSummaryMessage接入 SDK bridge,让已导出的工厂函数在下游真正被使用; - 经
forkedAgent.ts路由生成并启用enablePromptCaching,让重复的工具名前缀命中 provider 缓存; - (可选)将
tool_use_summary条目持久化到ChatRecordingService,并在会话恢复时重放; - (可选)按工具名的标签快捷路径(例如单个
read_file调用固定为Read <filename>),作为 LLM 前的快速通道。
9. 结语与验证入口
Tool-Use Summary 是"用一次便宜的调用换取整体可读性"的典型工程实践:fire-and-forget 的调度让 ~1s 延迟隐身于主模型流式输出之后,<Static>append-only 约束催生了"独立历史项 + 双路径渲染"的优雅解,而cleanSummary在异构 provider 环境下保证了 UI 的一致性。
希望深入验证的读者可以关注以下测试与实现入口:
- 渲染测试:packages/cli/src/ui/components/HistoryItemDisplay.test.tsx(
renders tool_use_summary as a dim badge line in full mode用例断言●字符); - 合并场景测试:packages/cli/src/ui/components/MainContent.test.tsx(验证静态模式下
tool_use_summary作为独立行保留、合并时不被丢弃); - 触发与陈旧检查:packages/cli/src/ui/hooks/use-llm-stream.ts#L5628-L5683;
- 开关解析:packages/core/src/config/config.ts#L8384-L8389;
- 清洗与截断:packages/core/src/services/toolUseSummary.ts。
相关功能联动可参考用户手册中的 Followup Suggestions(共享fastModel的另一快模型 UX 增强)以及 Expanded detail mode(Ctrl+O展开详情)。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考