qwen-code /review 的 Token 经济改造:从 93% Prompt 缓存命中到 `match-remote` 确定性下沉
2026/9/13 19:26:37 网站建设 项目流程

qwen-code /review 的 Token 经济改造:从 93% Prompt 缓存命中到match-remote确定性下沉

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

一次高强度的 PR 评审会触发 17~23 次模型调用,按 DESIGN.md 的成本分析折合约 88 万~120 万输入 token,其中绝大部分来自一段约 50K token 的共享前缀(系统提示 + 工具声明 + 启动前奏),在每个评审 agent 上被重复投递一次。本文以 qwen-code 仓库中的设计文档 docs/design/review-cpu-for-tokens.md 为主线,完整还原这次/reviewtoken 经济改造的两条工作流:Workstream A(prompt-cache 命中率基线测量,用测量而非代码改动关闭)与Workstream Bqwen review match-remote子命令,把 SKILL.md 中以散文形式承载的远程匹配规则下沉为经过测试的确定性代码),并深入到 wire 层缓存控制、前缀字节一致性、退出码契约与真实 git 测试的实现细节。读完本文,你将理解一条"先测量、后编码"的 token 优化闭环,以及一套可复用的"把易出 bug 的散文规则搬进子命令"的工程范式。

背景:一次评审为什么吃掉 88 万~120 万 token

/review的高强度流水线(--effort high)会并行拉起多个general-purpose子 agent,每个 agent 都是一个全新会话,其请求体在 wire 顺序上由四部分组成:

组成部分来源同一轮评审中是否随 agent 变化
系统指令内置general-purpose模板(静态文本)+ 非交互后缀 + 用户记忆 + 自动记忆(buildChatSystemPrompt,见 packages/core/src/agents/runtime/agent-core.ts 中的buildChatSystemPrompt否——同一模板、同一父会话
工具声明父级注册表复制到每个 agent 的 override(rebuildToolRegistryOnOverride否——同一工具集、确定性注册表顺序
启动前奏getInitialChatHistory→ 日期 + 平台 + cwd + 目录结构否——所有 agent 都钉在同一个 worktree(working_dir)或同一主 checkout 上;目录结构上限 20 项、按字母排序、跳过node_modules/.git/dist
任务提示来自agent-prompt --roster的每 agent 独立块是——这是尾部,位于共享前缀之后

也就是说,前三大块(约 50K token)在所有 agent 之间是共享前缀,本应只计费一次。设计文档提出两条降低成本的路径,且都恪守"recall-first"契约——不改变评审覆盖的范围,也不改变判定结论所需的证据

  1. 共享前缀 / prompt caching:如果前缀字节级一致且服务端缓存生效,agent 2..N 只需要为前缀付一次钱。问题是它是否已经发生,如果没有,原因是什么。
  2. Glue sinking:流水线的一贯方向(DESIGN.md——"确定性半段做成子命令")是把确定性的编排判断从模型回合中移出,放进经过测试的子命令。对剩余判断点的盘点发现了一个值得下沉的候选。

排查发现一:缓存链路其实已经端到端建好

设计文档的排查结论是:客户端没有可修的漂移,问题本质上是经验性的。证据分三层:

Wire 层:DashScope provider 已打缓存标记

在 packages/core/src/core/openaiContentGenerator/provider/dashscope.ts 中,addDashScopeCacheControl会为请求打上cache_control: {type: 'ephemeral'}标记:

  • system 消息上打标记(index === systemIndex && systemIndex !== -1);
  • 流式请求的最后一条消息上打标记(conversation anchor);
  • 最后一个工具声明上打标记(addCacheControlToToolstools[tools.length - 1]追加cache_control: { type: 'ephemeral' })。

该能力默认开启(enableCacheControl !== false)。实现中还处理了一个微妙场景:当会话尾部存在易变的 reattach 区域(如重挂载的图片)时,断点会落在最后一个稳定块上而非最后一条消息上,否则缓存前缀每一轮都会漂移(源码注释提及 issue #11627)。

响应解析层:同时兼容两种cached_tokens形状

OpenAI 兼容转换器在 packages/core/src/core/openaiContentGenerator/converter.ts 中同时读取两种格式:

const cachedTokens = usage.prompt_tokens_details?.cached_tokens ?? // OpenAI 标准嵌套格式 extendedUsage.cached_tokens ?? // 某些模型返回的顶层格式 0;

并将其映射为cachedContentTokenCount,最终进入usageMetadata(源码注释明确说明"Some models return cached_tokens at the top level instead of in prompt_tokens_details")。cachedContentTokenCount随后被recordTokenUsage写入运行统计(packages/core/src/agents/runtime/agent-core.ts 的recordTokenUsage)。

测量层:qwen review cost-ledger已就绪

packages/cli/src/commands/review/cost-ledger.ts 从 harness 自身的 transcript 记录(与check-coverage信任的同一批记录)中聚合每个 agent 的input / cached / output / thinkingtoken 计数,输出每流总计、主会话、agent 列表,并计入续跑的前序会话成本与缺失流。回答"缓存是否命中"不需要任何新插桩——Step 8 会把账本归档在每份报告旁。实现还特意说明它是信息性的:账本算不出来时打印原因并退出 0,因为评审绝不能因为自己的记账而失败。

排查发现二:前缀在 fan-out 中本应字节级一致

代码库已经为前缀缓存优化了前奏顺序——packages/core/src/core/environmentContext.ts 的getInitialChatHistory中有明确注释:

"Stable parts first (MCP, skills, startup) so prefix-caching servers retain the KV-cache for the shared prefix. Deferred-tools is last because tool_search revelations change it — only the tail recomputes."

即 MCP 指令、skills 提示、启动提示等稳定部分排在最前,会随tool_search变化而变化的 deferred-tools 提示排最后。仓库里也已经存在专门为共享字节级一致缓存前缀而生的 fork 机制;评审 agent 不能是 fork(它们必须内联返回 findings),但它们也不需要是 fork——其前缀在结构上已经一致。

推论:客户端没有需要修复的漂移。开放问题变成了纯经验问题——DashScope 是否真的为 qwen3.8-max 上的这种形状提供缓存命中?这是测量而非代码改动,而测量工具(cost-ledger 的cached列)已经存在。如果基线显示未命中,需要排查的候选原因包括:服务端最小可缓存大小、缓存 TTL 与 agent 墙钟时间的比较、fan-out 上的并发首写竞争、以及该端点上的ephemeral标记语义——其中多项在服务端,可能需要模型服务团队介入。

排查发现三:Glue 盘点——只有一个候选存活

设计文档逐条走查了 SKILL.md,盘点剩余的模型回合判断点。绝大多数已经下沉为子命令(parse-args、capture-local、fetch-pr、plan-diff、load-rules、repo-context、agent-prompt、check-coverage、findings、resolve-anchors、compose-review、presubmit、submit、script-lint、test-plan、base-tree、test-delta、extract-step、save-artifact、cost-ledger、cleanup)。幸存下来的判断点及其处置如下:

剩余判断点处置
Step 1 远程匹配(pr-url:解析git remote -v,按精确段匹配 host + owner/repo;裸 pr-number:挑选 URL 为派生 owner/repo 的远程)下沉。确定性解析,且带着两个已上线 bug 类别:子串匹配曾把shao/qwen-code匹配到wenshao/qwen-code远程(评审一个仓库、发帖到另一个);一次猜测 owner/repo 曾在读任何代码之前就停掉了评审。带 bug 历史的散文规则正是 DESIGN.md 要移进代码的那一类
增量缓存检查(比较两个 JSON 字段、三个分支)保留为散文——约 3 行;下沉增加的 prompt 比省下的还多(DESIGN.md:每个子命令都是 prompt 成本的一部分)
Step 3C 角度、whiff 检查、Agent 8 选择、去重/模式聚合、open-Critical 复检、跳过 CI 检查的裁决保留——语义判断;流水线的纪律是启发式喂给 agent,绝不替 agent 做裁决
Step 8 报告渲染保留——#8642 已把尾部批量成四个响应;剩余散文是真正的散文(摘要、输出语言标题)
Step 5 累积 findings 合并保留——看起来机械,但其输入是编排器从自然语言中提取的 verifier 裁决;机械部分每轮只是一次文件编辑

Workstream A:缓存命中基线——先测量,代码改动被测量门控

Workstream A 的规则非常严格:在这个基线选出分支之前,不为此工作流写任何代码

用当前构建对同一个仓库的 PR 以 medium effort 跑一次评审,读 cost-ledger,记录每个 agent 的inputcached

  • 命中率高(agent 2..N 从缓存读取大部分共享前缀):客户端零改动。把结果记入本文档历史与 DESIGN.md 成本表,关闭工作流。
  • 命中率低:先诊断再改任何东西。检查顺序:
    1. 经验性确认前缀一致性(通过 mock provider / HTTP trace 抓取两个 agent 的首个请求体,diff 前缀);
    2. 若前缀不同,找出漂移并修复(上面的表格就是嫌疑清单);
    3. 若前缀一致但无命中,问题转向 DashScope 侧(标记语义、最小大小、TTL、并发)——在添加客户端变通方案之前上报模型服务团队。

基线结果(2026-08-07,PR #8651,medium effort,当前提交的冻结 bundle)

命中率高的分支决定性胜出。从 harness transcripts(cost-ledger读取的同一批记录)聚合:12 个 agent、184 次模型调用、1287 万输入 token、1200 万 cached——93.3% 的输入由缓存提供。两个结构性观察:

  • 前缀在 fan-out 中经验性字节级一致。每个 agent 的首个请求定价为 34,250 ± 7 输入 token;三个在更早 agent 写入之后才发出首请求的 agent,从缓存读取了其中 30,311 个 token(88~89%)。共享前缀约 34K token,且它确实是共享的。
  • 跨 agent 的未命中是并发首写竞争,而非漂移。11 个 agent 在一个响应中同时启动;其中 8 个的首请求与缓存写入竞争,各自为前缀付了一次全价。每个 agent 首次调用之后的一切都命中(多轮历史锚定)。这场竞争约消耗该轮 2% 的输入 token。fan-out 前的预热请求可以封堵它,但那是为约 2% 的 token 差额多付一次模型调用并引入新机制——在已达 93% 命中率的前提下,被"简单优先"原则否决。fan-out 的墙钟时间比前缀写竞争更值钱。

Workstream A 以客户端零改动关闭。产生这一结果的链路(wire 级缓存控制、前缀友好排序、字节级一致的子 agent 前缀)早已就位;DESIGN.md 中"~88 万~120 万输入 token"的数字是原始重投递 token,不是计费成本——缓存承载了其中绝大部分。

Workstream B:qwen review match-remote的设计与接口

Workstream B 新增一个只读子命令,独占 SKILL.md 目前以散文形式在两处承载的远程解析规则(Step 1 的 pr-url 匹配,以及裸 pr-number 的远程选择)。

接口契约

qwen review match-remote --owner <owner> --repo <repo> [--host <host>] # 匹配到唯一远程时在 stdout 打印其名字;无匹配时退出 6
  • 读取git remote -v,结构化解析每种 URL——两种形状:git@<host>:<owner>/<repo>.githttps://<host>/<owner>/<repo>(.git)ssh://拼写归入第一种)。
  • 一个远程只有在 host 等于--host(默认github.com其 owner/repo(去掉.git后缀)等于参数时才匹配,二者都按整段、大小写不敏感比较。子串包含正是上线错误仓库 bug 的成因,解析器从不做子串包含。
  • 恰好一个匹配→ 打印名字,退出 0。零匹配→ stdout 打印none,退出 6(轻量模式的信号)。多个匹配→ 全部打印,退出 7 并带warning:行;编排器停下而非挑选(与今天散文的规则相同)。
  • 不是 git 仓库 / git 不可用→ 退出 1(与其他闸门一样 fail-closed)。

完整参数定义见 packages/cli/src/commands/review/match-remote.ts:--owner--repodemandOption--host省略时继承操作者导出的GH_HOST,否则回退github.com;另有--group-path支持 Aone 嵌套组(group/subgroup/project)的全路径段比较。

退出码编号的设计考量

评审子命令已有结构化结果退出码:3(闸门拒绝/未覆盖)、4(预算)、5(收敛);本文档为"无匹配远程"认领 6、"多个匹配"认领 7,把 1/2 保留给错误/误用。7 而非 2 的理由在match-remote.ts的注释中写得很清楚:2 保留给 shell 层的误用,就像 run 选择 3 而非 2 一样。

与 SKILL.md 的联动

SKILL.md 的 Step 1 把原来两段散文(精确段解析、fork 布局、禁止猜测)坍缩为"运行match-remote;打印出名字即 worktree 流程,退出 6 即轻量模式"。净 prompt 大小大致中性(从该 PR 的 SKILL.md hunks 测得净增约 720 字符)——bash 调用与退出码散文抵消了被删的规则文本;赢的是确定性与测试,不是体积fetch-pr的接口不变(仍接受--remote),轻量模式分支保持原状,因此没有其他步骤移动。完整的 Step 1 命令调用方式见 packages/core/src/skills/bundled/review/SKILL.md(其中还强调所有qwen review命令都必须以"${QWEN_CODE_CLI:-qwen}"前缀书写,以规避 PATH 版本漂移)。

裸 PR 号码的 host 解析

裸号码没有 URL 可取其 host,因此 Step 1 同时调用gh repo view取仓库 URL,将其 authority 作为--host传入。gh通过自身的默认 host 解析(操作者导出的GH_HOST,或没有时的 auth 配置)解析该 URL——匹配器因此对比的正是流水线其余部分所路由的 host。首次实现省略了--host,让匹配器自己继承GH_HOST,假设gh的路由总是来自该环境变量;但gh也能仅凭 auth 配置解析 GHE host,只用了 auth 配置的操作者会被拿去与 github.com 比较并在退出 6 处硬停——被该 PR 自己的评审抓住。--host省略时的回退不变:显式旗标优先,其次操作者导出的GH_HOST,最后 github.com。

源码级实现:纯核心与命令壳的分离

纯解析/匹配核心remote-match.ts

packages/cli/src/commands/review/lib/remote-match.ts 承载无副作用的核心逻辑:

  • parseRemoteUrl(raw)只接受两种 GitHub 风格形状(含ssh://拼写),本地路径、无 scheme 的名字、bundle 文件一律返回null永不匹配;两个以上路径段(嵌套组,如 Aone 的group/subgroup/project)折叠为最后两段作为 owner/repo,完整路径保存在groupPath——折叠是非单射的,matchRemotes在双方都带路径时逐段比较;
  • normalizeSegment小写化并剥掉一个尾部.git
  • hostsEquivalent处理 Aone web/git 双主机别名等价类,并统一端口、FQDN 尾点、大小写等 DNS 拼写差异;
  • matchRemotes(remoteVOutput, {...})只统计(fetch)行(fetch-pr经由远程的 fetch URL 抓取pull/<n>/head,仅 push URL 指向该仓库的远程无法服务它;fetch/push 双行也因此天然去重),兼容部分克隆的[blob:none]尾部注解,仓库身份比较在目标带完整路径时逐段精确比较、否则按最后两段规则。

文件头注释直接点明了它存在的理由:从/reviewskill 的 Step 1 散文中提取并给测试,因为散文曾上线两个 bug——子串比较把shao/qwen-code匹配到wenshao/qwen-code远程(评审一个仓库、发帖到另一个),以及手工猜测远程名在读到任何代码前就停掉了评审。

命令壳match-remote.ts的 fail-closed 细节

  • 闸门是"git 在这里可用",而非"这是工作树":裸克隆(mirror/CI 式 checkout)也能服务整条流程(git remote -v、fetch-pr 的 fetch、git worktree add都成功),所以--is-inside-work-tree打印false不能阻止评审;失败意味着 git 本身拒绝了仓库,把它的 fatal 原样带到 stderr(固定两因猜测会误报容器 CI 的dubious ownership拒绝)并 fail-closed。
  • 匹配结果用响亮的writeStdoutLine而非*Safe变体:该行是命令承重的结果,若写入失败编排器必须看到非零退出(fail-closed),而非空输出下的退出 0。
  • 零匹配时 stdout 打印none(轻量模式信号),stderr 打印未匹配的 host/owner/repo 说明;多匹配时逐行打印全部名字并输出warning:行,拒绝挑选——评审停在这里

测试:真实 git 之上的表驱动验证

测试分两层,设计文档明确要求镜像 parse-args 套件的风格,且bug 历史提供首批测试行

  • 纯核心表驱动测试(packages/cli/src/commands/review/lib/remote-match.test.ts):两种 URL 形状、.git后缀、大小写不敏感、shao/qwen-codewenshao/qwen-code回归行、fork 布局(origin = fork、upstream = 目标)、GHE host 不匹配、多匹配、零匹配、畸形远程 URL。
  • 真实 git 集成测试(packages/cli/src/commands/review/match-remote.test.ts):用execFileSync('git', ...)跑真 git——mock 掉 child_process 会在真实调用下通过,正是 parse-args 套件存在的理由。覆盖:匹配打印并退出 0;fork 布局中挑出 upstream;子串诱饵 owner(shao)不得匹配wenshao并退出 6;多远程全部打印并退出 7 带warning:;跨 host 不匹配退出 6;git 仓库之外退出 1 且透传 git 的 fatal;裸仓库(mirror checkout)依然匹配成功GH_HOST为空/空白时回退 github.com;省略--host时继承导出GH_HOST;从子目录运行时 git 向上走到 checkout。

影响文件与范围边界

Files affected

  • packages/cli/src/commands/review/match-remote.ts(新)+ 同位测试;纯解析/匹配核心在packages/cli/src/commands/review/lib/remote-match.ts+ 其表驱动测试;
  • packages/cli/src/commands/review.ts——注册 + demand 消息(matchRemoteCommand已在子命令清单中,见 packages/cli/src/commands/review.ts);review.test.ts——注册清单;
  • packages/core/src/skills/bundled/review/SKILL.md——Step 1 收缩;
  • docs/design/review-cpu-for-tokens.md——追加基线结果。

Scope boundaries(明确不做)

  • 不改 agent fan-out、roster、brief、验证、反向审计、判定计算或发帖;
  • 不做增量缓存子命令,不做报告渲染子命令(见盘点);
  • 不做逐 agent effort 或逐 agent model override——那是另一个提案(token 经济讨论中的方向 A/B),不属于本文档;
  • 不做 AST 级 diff 预消化——需要先有它自己的 A/B 证据。

开放问题与经验沉淀

  1. DashScope 在 qwen3.8-max 上的缓存行为——已被基线回答:是,同一 API key 的并发请求内可提供跨请求前缀命中(该轮 93.3% 的输入被缓存);唯一缺口是 fan-out 首请求上的并发首写竞争,判定为不值得为此引入机制。
  2. 退出码编号——评审子命令已用 3(闸门拒绝/未覆盖)、4(预算)、5(收敛)表达结构化结果;本文档为"无匹配远程"认领 6、"多个匹配"认领 7,保留 1/2 给错误/误用。已对照当前测试套件检查冲突,实现将其钉死。

这条改造留给工程实践的三个可迁移结论:测量先于编码(Workstream A 在写任何代码前用已有账本工具关闭);带 bug 历史的散文规则是下沉的第一优先级shao/qwen-codewenshao/qwen-code的教训被直接固化为回归测试行);prompt 缓存的经济学是结构性的——wire 级标记、稳定部分前置的前奏排序、字节级一致的子 agent 前缀三者共同把"原始重投递 token"与"计费成本"彻底分离,93.3% 的缓存命中率并非靠客户端新增机制,而是既有架构自然兑现的结果。

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询