简介:大模型推理同样有缓存,但命中逻辑与传统业务缓存截然不同:前者按Token前缀隐式匹配,后者靠显式key读写。本文系统对比两者的缓存对象、失效粒度、控制权与成本结构,拆解Agent多轮请求中前缀被破坏的典型原因,并给出请求组装、上下文分层与命中率监控的工程实践建议,帮助读者判断如何守住命中率、压低推理成本。
有公开测试显示,在请求组装足够稳定的Agent链路中,大模型服务的缓存命中率可以达到99%量级;而部分服务商的公开定价里,缓存命中与未命中的输入价格差距可达数十倍。两个数字放在一起,多数人的第一反应是“能省多少钱”。但对工程师来说,更关键的问题是:同样的模型、同样的业务逻辑,为什么有的团队命中率常年保持在高位,有的团队账单里却有大量Token按未命中计费?
答案往往不在模型侧,而在应用侧——具体说,在每一轮请求是怎么被组装出来的。要把这件事讲透,最好的方式是把两类开发者都会接触到的缓存放在一起对比:
两者的目标高度相似,但命中逻辑、失效模型、控制权归属和工程保障方式几乎完全不同。混淆这两套逻辑,正是很多团队“明明做了分层、缓存却总不命中”的根源。
传统业务缓存是应用架构里的经典组件:把热点数据、查询结果或计算中间态以键值对形式存放在独立的缓存层,应用通过显式设计的key进行读写,命中则跳过数据库查询或重复计算。它的核心特征是控制权完全在应用手里——key怎么起、什么时候写入、什么时候失效、失效策略是TTL还是主动淘汰,都由代码明确决定。它解决的问题是“别让同一个请求反复打到下游”。
到了大模型这里,缓存没有消失,只是对象变了。自回归模型在推理时,每个Token都会产生对应的KV Cache(注意力计算的中间结果)。如果两次请求从开头算起的一段Token完全一致,服务方就有机会直接复用上一次已经算好的KV Cache,只对新增的尾部做增量计算。system prompt、工具schema、项目规则、会话历史,只要保持相同的Token顺序,下一轮就有机会从旧前缀“接着算”。
它和业务缓存最本质的不同在于:业务缓存会显式设计key,而Prompt Cache是否命中,取决于从请求开头算起的一段Token前缀是否逐字一致。一个工具换了位置、一段当前时间插在前面,或者旧消息被重新序列化,后面看着没变的内容,也可能整体失去复用机会。
在展开差异之前,先明确两者的共同点,避免把对比做成“非此即彼”:
业务缓存的读写路径是明确的:应用构造key、调用get/set,命中与否一目了然。前缀缓存则没有应用可以指定的key,命中条件是“从第一个Token开始的一段前缀完全一致”。这意味着人类视角的“内容差不多”和缓存视角的“同一段前缀”之间可能存在巨大落差——两个字段的顺序对调、一个多余的空格、一次重新序列化,都会让缓存判定为不同前缀。
业务缓存中,key A失效不影响key B,失效是相互隔离的“点失效”。前缀缓存则是“截断式失效”:如果前缀在第N个Token处出现差异,第N个Token之后的所有内容都无法复用,只能整段重算。差异出现的位置越靠前,损失越大——系统提示词开头插入一个当前时间,可能让整个会话历史全部重算。
业务缓存中,读写都由应用代码显式控制,出了问题去查调用方即可。前缀缓存中,应用无法直接“写入”缓存,能否命中完全取决于每次请求实际发出去的字节序列。而在Agent运行时里,请求是由框架的组装链生成的——工具列表怎么排序、系统提示词里有没有注入当前时间、历史消息有没有被重写,都由运行时决定。这就把缓存命中从“应用问题”变成了“运行时问题”,也是本文最重要的一个差异点。
业务缓存命中的收益是间接的:减少数据库压力、降低下游调用费用。前缀缓存命中则直接改变输入Token的计费口径——公开定价中命中与未命中的输入价差可达数十倍,同时长前缀重算还会显著推高首Token延迟。对长上下文Agent来说,命中率几乎直接决定推理账单的量级。
业务缓存的命中率是成熟指标,中间件普遍自带统计。前缀缓存的可观测性需要自建:通常要从服务方返回的用量字段中读取“命中缓存的Token数”与“未命中Token数”,在网关或调用层聚合为命中率指标;而定位未命中原因,往往需要回到请求组装链路逐段比对两轮请求的差异。
| 维度 | 传统业务缓存 | 大模型前缀缓存 |
|---|---|---|
| 缓存对象 | 查询结果、对象、计算中间态 | 请求前缀对应的KV Cache |
| 命中方式 | 显式key,应用层读写 | 隐式前缀匹配,从请求开头逐Token对齐 |
| 失效粒度 | 单个key失效,互不影响 | 前缀任一位置变化,其后内容全部失效 |
| 控制权 | 应用完全可控 | 取决于运行时请求组装链 |
| 典型失效原因 | 数据更新、TTL过期 | 工具重排、动态信息前置、旧消息重写、重序列化 |
| 成本影响 | 间接:减轻下游压力 | 直接:输入计费差异可达数十倍,影响首Token延迟 |
| 可观测性 | 命中率指标成熟 | 需从用量字段自建聚合与告警 |
| 工程保障 | key规范、失效策略 | 上下文分层、请求组装稳定性契约 |
考虑一个Agent里很常见的场景:它刚读完项目规则和工具说明,调用了一次搜索,又把结果交回模型。下一步请求里,前面那一大段内容其实没变——模型、系统提示词、工具定义和之前的对话都还在,只在末尾多了一次工具调用和结果。只要这段前缀原样保留,服务方就有机会直接复用已经算过的KV Cache。
但运行时有三种高频破坏行为,会让“人眼看着差不多的两次请求”在缓存看来变成完全不同的前缀:
用伪代码对比两种组装方式:
# 反例:每轮请求都在破坏前缀messages = [{"role": "system","content": system_prompt + f"\n当前时间:{now()}"} # 动态信息插在最前]tools = shuffle(tools) # 每轮重新排序工具messages += rewrite(history) # 回头改写旧消息# 正例:稳定前缀 + 尾部追加messages = [{"role": "system", "content": fixed_system_prompt}, # 稳定不动*history, # 只追加,不重写{"role": "user", "content": new_input}, # 新内容放最后]tools = sorted(tools, key=lambda t: t["name"]) # 排序规则固定
围绕前缀缓存的工程实践,实际上可以分成三层来理解:
在这一层,缓存命中率反过来成了运行时稳定性的“体检指标”:命中率突然下降,往往说明组装链路里有部件在不该变化的位置发生了变化。
传统业务缓存与大模型前缀缓存,解决的都是“别重复算”的问题,但路径完全不同:前者靠显式key把控制权交给应用,失效是隔离的点;后者靠前缀一致把控制权交给了运行时,失效是截断式的。真正决定命中率的,不是设计文档里“稳定前缀”四个字,而是每轮请求实际发出去的字节序列。把系统提示词、工具排序、会话历史、上下文压缩和服务方边界收进同一条请求组装链,命中率就不再只是账单上的一个数字,而是整个运行时是否稳定的度量——这也是评估任何Agent框架时,比功能列表更值得追问的一件事。