Agent 洗冤集录:前缀缓存 —— MCP 排序问题,如何影响模型调用的延迟与成本

发布时间:2026/9/30 8:26:19
Agent 洗冤集录:前缀缓存 —— MCP 排序问题,如何影响模型调用的延迟与成本 Agent 洗冤集录前缀缓存 —— MCP 排序问题如何影响模型调用的延迟与成本系列引言《洗冤集录》是南宋宋慈汇集前人验尸经验并结合自身刑狱实践写成的法医学著作。“告状切不可信须是详细检验务要从实。”书中还反复强调亲自检验、多方求证“不可避臭恶……须是躬亲诣尸首地头……须是多方体访务令参会归一切不可凭一、二人口说便以为信。”「Agent 洗冤集录」取名于此借的是这份重检验、重实据的态度也提醒自己少些浮躁把事情查清楚再下结论。作为 Agent Harness 的开发维护者我们收到的用户反馈往往可以归结为两个字慢性能问题、笨能力问题。而在我们的排查经验中这些问题的原因多数落在 Harness 的设计或实现 bug 上。上下文遗漏、工具结果传递错误、状态流转异常都可能表现为 Agent 的“笨”重复执行、缓存失效、轮次设计不合理则可能表现为“慢”。很多时候改善 Agent 的能力与性能做的其实是修复这些具体的 bug。用户的感受是排查的起点原因则需要逐项检验模型实际收到了什么工具究竟返回了什么系统又如何处理这些结果。这个系列记录这些排查与修复从原始请求、日志和代码出发用实验验证推断。借用“洗冤”之名就是希望对 Agent 的每一次归因都能拿出可复查的证据也能说清背后的原理。从 MCP 排序开始Agent 一慢我们很容易先怀疑模型是不是推理太重、上下文太长再看不断累积的输入 tokens似乎“又慢又贵”就是复杂任务的必然代价。但有时让 Agent 多花时间、多算一遍旧内容的只是一段不起眼的拼接代码。我们就遇到了这样一个 bug接入 MCP 后运行框架从 Go map 中取出服务说明拼进 system prompt。说明一字没改排列顺序却可能每次都变。对人来说这几段话先读哪段都差不多对依赖相同前缀的提示缓存来说这次重排却会影响后面整段历史的复用。在一个真实续跑样本的六组配对测量中我们对照了两种请求保留原有重排以及固定服务说明顺序。与保留重排相比固定顺序时的缓存命中率从39.01% 升到 99.15%单次模型调用的平均耗时从7.31 秒降到 4.24 秒减少约 42%。总输入 tokens 保持不变更多旧内容得以直接复用计算在缓存读取更便宜的计费方式下这也意味着降低输入费用的机会。实际账单与完整任务耗时还需要另行验证。本篇从这个 MCP 排序问题出发讨论前缀缓存如何影响模型调用的延迟与成本。排查始于一个很容易漏掉的问题那些没有变化的内容每次发给模型时真的保持原样了吗为什么顺序会影响速度Agent 通常需要多次调用模型才能完成任务。每次工具执行结束运行框架会把结果追加到对话历史再发送给模型。随着任务推进请求中重复出现的内容也越来越多。提示缓存可以复用这些重复内容的计算。OpenAI 的文档说明这种复用要求提示具有完全一致的前缀。因此固定指令适合放在前面新消息追加在后面。先看请求是怎样增长的。一轮用户对话可能包含多次模型调用模型先要求读取文件工具返回结果后模型再继续回答用户追问才开启下一轮对话。请求里的内容第一轮首次调用第一轮工具返回后第二轮用户追问固定指令、工具定义、MCP 说明首次带入原样保留原样保留用户消息 U1首次带入原样保留原样保留助手工具调用 T1、工具结果 R1尚未产生追加到输入原样保留助手答复 A1、用户追问 U2尚未产生尚未产生追加到输入与上次请求的关系假设尚无可用缓存旧输入是新输入的前缀旧输入仍是新输入的前缀图中蓝色表示与上次输入一致的前缀黄色表示新追加到输入的内容。它说明了缓存可以复用的原因具体命中多少还取决于缓存有效期、服务端处理等条件需要读取实际 usage。图中是教学示例省略了推理项等细节并非下文实验的原始对话。我们的排查受到《深入解析 Codex 智能体循环》的启发。文章提到MCP 工具顺序不稳定曾导致缓存未命中。我们检查了自己的请求发现了类似问题MCP 服务说明的拼接顺序不稳定。有意思的是连 Codex 的开发者也曾踩过同样的坑。相关修复 PR #2611 于 2025 年 8 月 25 日合并到我们排查这个问题时已过去一年多。那篇博客我其实很早就读过却没能在自己的开发过程中认出这个问题——前人踩过的坑看过了也未必就能避开。把这次排查写下来也是希望给后来者多留一个提醒能少踩一个坑就少踩一个尽量不要让“后人复哀后人”。用三个匿名服务表示对比正常追加和顺序变化服务说明的内容没有变化但它位于对话历史之前。这里一旦发生重排后面的历史就不再拥有与上次相同的完整前缀。前面仍可能命中缓存后面的复用却受到影响。代码中的原因很简单我们从 Go map 读取服务说明然后直接拼接。map 的遍历顺序不固定拼接函数也没有排序。修复放在公共拼接入口先复制有效的服务说明再按服务名称排序。初始化和续跑都经过这个入口因此同一组说明会生成相同文本。说明内容和工具权限保持原样。这里也需要区分协议和实现。MCP 的tools/list返回的是数组原文提到的 Codex bug 发生在 Rust 实现中修复是将内部 HashMap 收集的工具转成列表再按完整工具名排序见PR #2611。TypeScript 的原生Map则按插入顺序遍历但不会自动按名称排序如果每次按不同的异步完成顺序插入最终顺序仍可能变化。需要保证的是相同内容形成相同序列。用真实请求验证在一份包含 99 次请求的会话日志中我们发现了 32 次这样的纯顺序变化。随后选择其中一次续跑做实验工具始终是 41 个历史从 76 条追加到 79 条变化的是一段 276 字符的 MCP 服务说明。实验使用qwen3.8-max和现有网关比较“保留重排”与“固定顺序”两组。除顺序和用于隔离两组缓存的标记外工具、历史和模型参数保持一致。我们交替运行了6 组配对实验共 24 次真实调用12 次预热、12 次测量。每组先发送前一轮输入建立缓存再测量续跑请求。输出上限统一为 64 tokens业务工具没有实际执行。下表只统计测量调用每种方案各 6 次数值为每次调用的平均值指标顺序变化固定顺序变化总输入 tokens62,99862,998不变缓存读取 tokens24,57662,464增加 37,888未缓存输入 tokens38,422534减少 98.61%缓存命中比例39.01%99.15%增加 60.14 个百分点平均首个推理片段时间5.896 秒2.769 秒减少 53.03%平均模型调用耗时7.311 秒4.241 秒减少 41.99%六组实验中固定顺序后的调用耗时都更短平均减少3.07 秒。这里需要区分两个数字总输入没有减少每次少了 37,888 个未缓存输入 tokens。请求仍然包含完整历史只是更多内容可以复用已有计算。表中的“首个推理片段”是模型返回的thinking_delta不等于用户看到第一段正文的时间。耗时结果也只覆盖这次长历史、短输出的模型调用不能直接推导为完整任务或所有用户都快 42%。缓存命中的输入也更便宜缓存还会影响输入费用。下面以2026-09-29核验的部分 GPT、Claude 型号为例并列出本次 Eval 使用的 Qwen 型号。单位为美元 / 百万输入 tokens模型普通输入缓存读取命中部分单价降低Qwen3.8-Max本次 Eval$2.00$0.2587.5%GPT-6 Astra$10.00$1.0090%GPT-6 Sol$2.00$0.2090%Claude Fable 5.1$10.00$0.2597.5%Claude Opus 5.5$4.00$0.2095%Claude Sonnet 5$2.00$0.2090%报价口径Qwen取新加坡国际部署的隐式缓存价GPT取 Standard 短上下文档Claude取默认全球路由标准价。不含 Batch、加速或地域附加费。读取便宜写入也要算GPT-6 的缓存写入按普通输入单价的1.25 倍计费Claude的 5 分钟缓存写入为1.25 倍1 小时为2 倍。这是写入 tokens 的适用单价不是在普通输入费用上再加同额费用。这也解释了为什么总输入 tokens 不变费用仍可能下降更多输入按缓存价格计费。但上表是命中部分的输入单价降幅整个请求还要算未缓存输入、缓存写入及输出费用。本次实验经过内部网关公开报价不代表该网关的实际账单GPT、Claude 也未参与上面的速度实验。对 Agent 开发的启示提示缓存是否有效部分取决于运行框架如何组织输入。一个对人来说无关紧要的顺序变化放在长历史之前就可能让大量内容失去复用机会。这次我们修复了 MCP 服务说明的排序并用回归测试检查不同排列生成相同文本真实内容变化仍能反映到提示中。接下来线上验证还需要覆盖工具执行、长输出和多轮任务观察用户实际等待时间。反求诸己。把原因归到模型上很容易让排查止于“这是模型的问题Agent 开发者控制不了”。但看似模型的性能或能力不足也可能是 Agent 自身的 bug上下文组织不当、工具结果传递错误、执行流程反复都在开发者能检查、能修复的范围内。这次的缓存失效就是一例。先核对实际请求、工具结果和执行链路再判断哪些是实现缺陷哪些才是模型的边界。我们增强了 Cache Token Trace 的实现也把这次的两点收获写进了仓库的AGENTS.md作为后续开发约定。修改 system prompt 时同时考虑缓存影响。除了判断内容是否必要还要检查它是否经常变化、是否会改变已有前缀。同一内容应保持稳定顺序动态信息在语义和权限允许时追加。需要更新的约束仍应正常更新缓存不能优先于正确性。把输入缓存作为 Agent 的重要性能指标。评测时同时记录总输入、cached input tokens、未缓存输入以及缓存命中率缓存读取 tokens / 总输入 tokens再结合响应耗时判断收益。这样才能区分“输入变少了”和“相同输入复用了更多计算”也能更早发现前缀变化造成的性能退化。实验说明2026-09-17单一真实续跑样本先做 2 组试验再扩至 6 组。平均调用耗时减少量的配对 bootstrap 95% 区间为 2.136–4.247 秒。tokens 取自网关原始 usage24 次调用的请求、输出与耗时均已留存并校验。前期 mock 只用于筛查问题未用于上表。代码修复与这组上线前实验分别记录线上效果尚待验证。参考OpenAI《深入解析 Codex 智能体循环》OpenAI Prompt caching。文中性能数据来自我们的 Qwen 链路实测。