M-perf.2 · 延迟减法四刀:planner 快档、终结直出、一步到手价、阶段预告(第三轮延迟)#
面试向开发文档:讲「为什么、取舍在哪、量到了什么」,不写函数签名与代码细节。 承接
Mperf(结论:~96% 耗时是模型解码)与Mperf.1(候选 id 化 + over-loop 哨兵,砍掉了 「重吐候选」这个最大的解码浪费)。前两轮之后,链路已经没有明显的「冤枉 token」——这一轮的题目变成: 在每个 token 都看似有用的链路上,还能减掉什么?
一句话#
同一条验收 query(「便宜抗造旅行三件套,预算 300,不要塑料,喜欢小众」)从 83.2s 砍到 39.7s (−52%),主 loop 模型调用 9 轮 → 5 轮,没换模型、没降候选质量。方法论只有一句:延迟 ≈ 解码速率 ×(主 loop 每轮 token × 轮数 + 工具内 LLM token),前两轮把「每轮 token」压干净了, 这一轮压剩下两个乘数——思维链(占总输出 45%,其中 planner 一个调用占 70%)和轮数(9 轮里 有 4 轮不产生任何决策价值)。四刀全是减法:不加缓存、不加并行框架、不换更快的供应商。
归因:先把 83.2s 拆到「轮」这一级#
前两轮的教训(没有基线就改性能 = 拍脑袋)这轮直接复用,但归因粒度从「段」细到「轮」:
- 全量事件时间线:包一层 monitor 的事件出口,把主 loop + 子 fork 的每个事件都带上单调时间戳—— 比只挂根 thread 的假 WebSocket 多看到一层(子 Agent 事件按子 thread 路由,根连接收不到)。
- 逐轮 token 复盘:跑完从落盘的消息历史里取每轮的
token_usage——每轮的 in/out/reasoning/ cached 四个数,比时间戳更能定位「这轮为什么慢」。
两份数据对上后,83.2s 的构成一目了然:
| 耗时块 | 时间 | 定位 |
|---|---|---|
| planner 工具内 LLM | 27.5s | 输出 790 tok,其中 reasoning 557(70%) |
| 主 loop 9 轮解码 | ~42s | 输出 3409 tok,其中 reasoning 1536(45%) |
| shopping_summary 内部 LLM | 8.3s | 给 10 件产 reason(上一轮已治理过,合理) |
| 全部真实工具(检索/比价/精挑) | <2.5s | 不是瓶颈,与前两轮结论一致 |
最大单点不在主 loop,在工具内部。 主 loop 的解码大家都会去看(它有 assistant_call 事件), 而 planner 的 27.5s 藏在一个 tool span 里——AGUI 时间线上它只是「一个比较慢的工具」。这是第一轮 「category_insight 被误读成 160s」的镜像教训:工具的耗时必须拆到「工具逻辑 vs 工具内 LLM」, 否则 LLM 驱动工具会把最贵的解码藏起来。
另外两处「轮级」浪费也是这次粒度细化才看见的:
- 撞闸门轮:模型在候选入池后又发了两个 item_search,被阶段机(phase_check)拒绝——机制是对的, 但模型花了一整轮解码(3.1s)去「撞墙学规则」,且下一轮的 reasoning 飙到 498 tok(它在消化被拒)。
- 收尾复述轮:shopping_summary 的返回值本来就是面向用户的完整清单文案,主模型却在终结后又花 729 tok / 7.5s 把同一份清单重新排版一遍——双重吐字,还引入转录数字出错的风险。
刀1 · planner 换快档:思维链对结构化抽取没有增益#
planner 是「意图 → 结构化字段」的抽取任务(预算/品类/硬排除/软偏好/检索词),全仓其余内部 LLM 调用(summary 文案、记忆 curator、偏好 parser、子 Agent)早在路由降级改造时就换成了「同模型关 reasoning」的快档——planner 是唯一漏网的,而它恰好是链路上第一个、也是最贵的一个。
改之前先做对照实验(同一条 intent 各跑两次):
| 档位 | 耗时 | 输出 tok | 其中 reasoning | 解析产出 |
|---|---|---|---|---|
| 主档(带思维链) | 12.4~16.7s | 567~878 | 306~667 | tasks/排除词/预算 ✓ |
| 快档(关思维链) | 2.8~3.1s | 213~216 | 0 | 完全一致 ✓ |
这印证了路由降级那轮的论点:lite 档不该换弱模型,该同模型关 reasoning——换弱模型会多绕几轮把 省的钱吃回去,关思维链则是「能力不降、只省最贵的那段解码」。结构化抽取尤其如此:schema 本身就是 约束,思维链在这里产出的是「我要先理解用户意图……」这类过程独白,不改变 JSON 的内容。
风险与对冲:对照实验只有一条 query,不能证明无系统性退化。对冲是现成的——M11 的 rubric 种子集 就是为这种「改了链路某环、要确认整体不退化」准备的回归基线;planner 判错的形态(漏排除词、错档位) 也早有机制兜底(档位由规则判、币种/收货国确定性回填),模型换档动摇不了这些确定性层。
刀2 · 终结直出:终结工具的产出就是答案,别让模型再念一遍#
发现很朴素:shopping_summary 走 content_and_artifact,喂给模型的 ToolMessage 内容就是 面向用户的完整清单文案(同一份文本也直接落盘成可下载的 md)。终结之后再唤起模型,它能做的只有 把这份文案换个口吻复述——729 tok 的解码买来一句「好的,清单来了!」的开场白,还冒着把到手价 抄错一位的风险。
治法与预算耗尽的 fallback 档同构:执行层看到「本轮已终结 + 消息里有终结产物」,就直接合成收尾 消息,不再调模型——无工具调用,loop 自然终止。产物从 artifact 取而非工具结果字符串:字符串会过 截断 Hook,artifact 是结构化原文,长清单不会被截半。
两个边界想清楚才动手:
- chat_fallback 不走此路。它的返回是结构化对象不是成品文案,且闲聊收尾本就该由模型口吻说; 这轮量到的浪费在购物链路,不顺手扩大打击面。
- 口吻会变:用户看到的回复从「主模型的转述」变成「summary 工具的原文」。副作用是三处文本 (聊天回复 / 落盘 md / 商品卡文案)从此必然一致;代价是语气少了一层人味。这轮实测里 summary 原文如实写了「材质大多未标注、无法确认非塑料」——比主模型转述版更诚实,这个 trade 我认为是赚的。
刀3 · 一步到手价:两个 0 秒的工具,各收一轮解码的过路费#
price_compare(汇率归一)和 shipping_calc(运费+关税)都是 ~0s 的纯计算,但拆成两个工具意味着 主模型要在中间多解码一整轮(实测 6.5s + 4.0s),而这一轮的全部工作只是把上一个输出里的 item_id 搬进下一个输入——两步之间没有任何需要模型决策的东西。id 化那轮已经把「搬运的内容」压到最小, 这轮把「搬运本身」砍掉:price_compare 归一完直接内联算到手价,按到手价排序返回,结果里明说 「无需再调 shipping_calc」。
真正的取舍在「合并的方式」。第一直觉是新建 landed_cost 工具、删掉两个旧工具——排查后放弃: 两个工具名散布在 35+ 个文件里(阶段机白名单、顺序断言、rubric 评测、前端事件渲染、few-shot 示例、 planner 任务枚举),删除式合并的爆炸半径远超 4~6s 的收益。做减法的原则也适用于减法本身: 最终 shipping_calc 保留(补算兜底、docstring 收窄成「通常不需要」),一个名字都不改,阶段机/前端/ 评测零改动。收货国照旧走机制兜底(planner 确定性判定、写进会话上下文),不依赖模型传参——这条 在 shipping_calc 里验证过的纪律原样搬过来。
刀4 · 阶段预告:撞闸门学规则 → 看提示走对路#
阶段机(M11)在执行层拒绝越权工具是刻意设计——不摘工具、保 prompt cache 前缀,模型收到拒绝理由 后下一轮自然转向。但这次逐轮归因量出了它的学费:拒绝本身要花模型一整轮解码去「撞」,撞完 下一轮还要多花几百 reasoning tok 消化。机制没错,信息时机错了:SEARCHING→COMPARING 的转移发生在 post_reflect,静默完成,模型根本不知道检索已收线。
治法是在转移发生的当下注入一条阶段提示(新阶段 + 白名单,白名单从阶段机现取不硬编码),复用 Hook 体系现成的消息注入通道。约 60 tok 的提示换掉一整轮 3~5s 的试错——闸门继续兜底(提示是给 模型看的,不是契约),只是模型从此不需要用一轮解码来发现规则。
量化与副产物#
四刀落地后同一条 query 复测:
| 指标 | 改前 | 改后 |
|---|---|---|
| 端到端 | 83.2s | 39.7s(−52%) |
| 主 loop 模型调用 | 9 轮 | 5 轮 |
| 主 loop 输出 tok | 3409 | 2311 |
| planner 内部 | 27.5s | 2.8s |
| 终结后收尾轮 | 7.5s / 729 tok | 0(合成,不调模型) |
时间线上四刀逐一可见:planner 秒回、候选入池后模型直奔 price_compare(没有撞闸门轮)、比价一步带出 到手价(没有 shipping_calc 轮)、summary 结束 40ms 后 task_result。全量测试 690 条不动一条业务断言 全过(新增 4 条覆盖四刀行为)。
意外的副产物:轮数压缩后,模型自发把 category_insight 和 item_search 并进了同一轮(并行 tool call)——链路变短之后,模型对「剩余步数」的规划也变了。这不是我设计的,但顺手又省了一轮。
留下的观察项(这轮明确不动):item_picker 在无约束 query 上空转一轮(picked 10/10),但它对 有硬排除的 query 是机制执行点,砍它伤正确性;DeepSeek 侧 prompt cache 两次实测各出现一次 cached=0 (best-effort 缓存的供应商侧 miss,非前缀破坏),只影响 prefill,继续观察不修。
方法论小结#
- 归因粒度决定发现的量级。 「段级」归因(第一轮)找到的是 over-search;「轮级」归因(这轮) 才看得见「一轮解码没有产生任何决策」这类浪费。瓶颈治到后面,剩下的都藏在更细的粒度里。
- LLM 驱动工具会把最贵的解码藏进 tool span。 最大单点(planner 27.5s)不在人人盯着的主 loop, 在一个「看起来只是有点慢」的工具里。
- 机制正确 ≠ 机制免费。 阶段闸门拒绝得对,但模型撞闸门的那轮解码是实打实的延迟——护栏的 成本要和护栏的收益一起算,信息给早一点,护栏就从「拦截器」退成「保险丝」。
- 减法优先于加法,且对减法本身也要做减法。 这轮零新增基础设施:没加缓存层、没加并行框架、 没换供应商;连「合并工具」都从删除式收敛成注释式。每一刀都是「把不产生价值的 token/轮次拿掉」, 所以没有新的东西需要维护、也没有新的故障面。