5.3 Agent与工具调用:消息追加式命中优化 本节摘要:Agent 的多轮循环里,消息列表只增不减,这恰好是前缀缓存最舒服的形态。但一个常见的"每轮重新摘要"动作会把前缀全打碎。本节给出三条纪律、一个反例复盘,以及多 Agent 并行时的缓存共享与隔离原则。 5.2 的网关把流量收拢到一处,统一了计量。但计量出来的命中率,在 Agent 场景里往往惨不忍睹——不是网关不行,是消息被组织坏了。这一节专讲 Agent:它的循环结构本该是前缀缓存的天堂,可大多数实现却把它变成了地狱。讲清它,5.4 的命中率仪表盘才有好看的数据可画。
本节摘要:Agent 的多轮循环里,消息列表只增不减,这恰好是前缀缓存最舒服的形态。但一个常见的"每轮重新摘要"动作会把前缀全打碎。本节给出三条纪律、一个反例复盘,以及多 Agent 并行时的缓存共享与隔离原则。
5.2 的网关把流量收拢到一处,统一了计量。但计量出来的命中率,在 Agent 场景里往往惨不忍睹——不是网关不行,是消息被组织坏了。这一节专讲 Agent:它的循环结构本该是前缀缓存的天堂,可大多数实现却把它变成了地狱。讲清它,5.4 的命中率仪表盘才有好看的数据可画。
一个标准的 Agent 循环是这样跑的:系统提示和工具定义先就位,然后用户提问,模型回一句,调工具拿到结果,把结果塞回消息列表,再让模型看一眼……每一轮,消息列表都在变长,但前面的内容从不被改动。
这正是前缀缓存梦寐以求的形态。前缀是"已经算过且固定"的那一段,而 Agent 的消息列表前缀恰恰稳定地累积。第一轮算过的系统提示、工具定义、前几轮的工具结果,到了第十轮依然躺在列表最前面,理论上每一次新推理都能命中这一长串前缀。收益比 5.1 的"两条请求共享系统提示"大得多,因为这里共享的是越来越长的整个历史。
要让这份红利真正落袋,组织消息时守三条纪律。
第一条,系统提示与工具定义保持稳定且靠前。它们必须固定在消息列表最前面,且内容绝不随轮次变化。有人图省事,把"当前时间""会话编号"塞进系统提示,结果每轮系统提示都变,整个前缀从第一块就失配,后面全废。时间类动态信息请放用户消息,别污染固定前缀。
第二条,工具结果的序列化格式要稳定。工具返回的 JSON 里常含时间戳、随机 id、字段顺序抖动。如果每次工具结果序列化出来都不一样,那"工具结果"这一段前缀就失配了。对策是剥离时间戳、对字段做稳定排序、丢弃非必要随机字段,让相同事实总是序列化成相同字符串。
第三条,新增消息只追加在尾部。绝不在前半段插入、修改或重排已有消息。很多框架的"记忆压缩"会偷偷把旧消息改写或挪位,这等于主动打碎前缀。如果必须压缩,把压缩结果作为新的一条消息追加到尾部,而不是原地修改历史。
纪律是骨架,落地还有两个缝要补。
其一是推理轨迹的稳定性。有些模型会在消息里插入一段"思考过程"或内部轨迹,这段内容每次生成都不同,如果它被当作普通消息塞进列表前部,就会污染前缀。处理办法是把它放到尾部、或单独成一类不进前缀缓存的字段,别让它夹在稳定前缀和新问题之间。
其二是上下文预算的分配。追加式命中意味着历史越积越长,前缀命中率走高,但显存和上下文窗口是有限的。当历史长到逼近上限,你得决定截断哪一段:保留靠前的系统提示与工具定义(它们命中价值最高),优先截断中段的老旧工具结果。截断会牺牲一部分前缀命中,但换来整体不溢出,是更稳的取舍。

说一个我见过的真实翻车。某 Agent 为了控制上下文长度,每轮都把历史对话重新摘要成一段,再拼上最新一轮,作为"新的消息列表"发给模型。意图很好——省 token。但代价是:每一轮的消息列表前缀都和上一轮不同(因为摘要文本变了),前缀缓存命中率直接跌到个位数。他以为在省,实际把引擎层最肥的一块复用红利整个扔掉了。
更糟的是,这种重写还引入不确定性:摘要模型每次输出略有差异,导致同一次会话里连"看起来一样"的历史都无法对齐。正确做法是把摘要作为追加的一条消息,保留原始历史不动;或者对超长会话做"截断旧消息"而不是"重写旧消息"——截断只缩短前缀,不影响已缓存块的对齐。
对同一段十轮工具调用会话,按纪律组织消息前后,典型命中率(前缀命中 token 占输入 token 比例)对比:
| 轮次 | 重写摘要式(命中率) | 追加式(命中率) |
|---|---|---|
| 第 2 轮 | 8% | 71% |
| 第 4 轮 | 6% | 83% |
| 第 6 轮 | 5% | 88% |
| 第 10 轮 | 4% | 91% |
差距来自前缀长度:追加式每轮都能命中越来越长的历史,命中率随轮次走高;重写式每轮前缀都变,命中率始终贴地。十轮之后,追加式省下的 prefill 已经相当可观。
当多个 Agent 实例同时跑,缓存怎么摆?两条原则。共享的是稳定前缀:系统提示、工具定义这类所有 Agent 都相同的内容,理应命中同一批缓存块,所以不要给每个 Agent 实例塞一份略有不同的系统提示,那会制造无谓的副本。隔离的是会话私有历史:不同用户、不同任务的中间结果绝不能串,键里必须带会话或任务标识,否则会出现"A 的对话被 B 命中"的串台事故。
落到 5.2 的网关上,就是 make_key 里既要包含规范化消息,也要包含任务隔离标识;稳定前缀部分天然跨任务共享,私有部分各自隔离。这层隔离做对了,多 Agent 并行既能摊薄固定前缀的成本,又不会泄露上下文。
还有一层账要算:多 Agent 并行时,固定前缀被摊薄是好事,但每个 Agent 的私有历史命中率会偏低,因为彼此不共享。别因此误判"Agent 缓存没用"——要分两层看指标,固定前缀那层的命中才是衡量共享收益的正确口径,私有层本来就靠单次会话内的追加来命中。
落地时可以用一张三行清单自查:第一行看 system 与 tools 是否字面冻结(连注释里的日期都不能有);第二行看工具结果是否剥掉了时间戳并做了键排序;第三行看新增消息是否严格追加在消息数组尾部、没有中间插入。三行全绿,追加式命中的三块收益(省 prefill、省延迟、省写溢价)才会一起兑现。
回到本章的拐点:前缀缓存开不开,是引擎的事;命中率高低,是工程纪律的事。Agent 场景把这句话放大得最彻底——同样的开关、同样的模型,消息组织对了命中率九成,组织错了个位数。5.2 的网关给了你统一的拦截点,这一节的纪律给了你"在该拦截点做什么"的具体规矩。下一节我们把这些规矩跑出来的数据,变成看得见的仪表盘。