简介:大模型Prompt Cache按Token前缀匹配,命中与未命中的输入成本差距可达一个数量级。本文拆解Agent运行时在请求组装链上的关键环节——系统提示词、工具排序、会话历史与上下文压缩,给出稳定前缀、追加式写入、确定性序列化等工程实践,并把命中率用作检验运行时稳定性的标尺,帮助团队同时守住成本、延迟与稳定性。
主流大模型服务的输入计费,普遍区分“缓存命中”与“未命中”两种口径:命中部分的Token按大幅折扣的单价结算,未命中部分按全价结算,两者差距可达一个数量级。与此同时,在一组面向Agent运行时的公开测试中,Prompt Cache命中率被推到了99%。
两个数字放在一起,第一反应是算账:一次Agent会话动辄几十轮请求,每轮都携带大体量的系统提示词、工具定义与会话历史。假设稳定前缀有2万Token、会话跑50轮,若每轮都未命中,累计按全价计费的输入至少是百万Token量级;若命中率达99%,这部分绝大多数转为折扣价结算,整体成本可以压缩到原来的几分之一。命中率每提高一个百分点,成本曲线就整体下移一段。
但账单只是表象。工程师对缓存并不陌生:进程内有本地缓存,服务之间有分布式键值缓存,静态内容前面还有CDN。它们处理的是同一类问题——已经算过、取过、传过的东西,能不能少做一遍。到了大模型这里,缓存没有消失,只是对象变了。
模型处理一段输入时,会为每个Token生成中间计算状态(KV状态)。如果下一次请求从第一个Token起,有一段与历史请求完全一致的前缀,服务端就可以直接复用这段前缀对应的KV状态,只对新增Token做增量计算。这就是Prompt Cache的基本原理。
它和业务缓存有本质区别:
为什么必须是前缀?因为KV状态是按位置累积的:位置i的状态依赖位置0到i-1的全部Token。这意味着两段“内容相似但顺序或措辞不同”的文本,无法共享任何中间状态。一个工具换了位置、一段当前时间插在系统提示词前面、一条旧消息被重新序列化——哪怕语义完全一样——都会让断点之后的全部Token失去复用资格。人眼看着差不多的两次请求,在缓存看来可能已经不是同一段前缀。
还要注意,部分平台以固定长度的块为最小缓存单位,并对可缓存的前缀长度设有门槛。也就是说,前缀不仅要稳定,还要“足够长且按块对齐”才能吃满折扣。
考虑Agent里最常见的循环:读取项目规则与工具说明 → 调用一次搜索 → 把结果交回模型 → 模型继续推理。
理想情况下,下一轮请求的消息序列是:
[system] 项目规则 + 角色设定 ← 不变[tools] 工具schema列表 ← 不变[user] 任务描述 ← 不变[assistant] 思考与工具调用 ← 不变[tool] 搜索结果 ← 新增
前面四段原样保留,只在末尾追加一次工具调用与结果。从第一个Token到倒数第二段,整条前缀与上一轮完全一致,服务端直接复用已算好的KV状态,只对新增的少量Token做计算。99%的命中率,正是把这种“追加式增长”贯穿整场会话的结果。
现实中,运行时稍不留神就会把前缀切断:
[system] 项目规则 + 角色设定 + 当前时间戳 ← 每轮都变,第一个Token就断了[tools] 工具schema(按注册顺序,可能重排) ← 顺序抖动[history] 旧消息被模板升级后重新渲染 ← 序列化结果变化[new] 工具调用与结果
三种典型破坏者:
把视野从单次请求拉高到Agent运行时层,会发现命中率不是由某一行代码决定的,而是由整条请求组装链的确定性决定的。这条链上至少有六个环节:
1. 系统提示词:会话期内冻结。 系统提示词位于前缀最前端,一字之差即整条前缀失效。它应当像版本化的静态资源一样对待,动态信息一律不许混入。
2. 工具定义:序列化确定、排序稳定。
# 反例:依赖注册顺序,重启后可能变化tools = list(tool_registry.values())# 正例:按稳定键排序后再序列化tools = sorted(tool_registry.values(), key=lambda t: t.name)
3. 会话历史:append-only。 已确认的消息不再修改、不再重排、不再重新格式化。需要修正历史时,宁可开辟新的会话分支,也不要原地改写。
4. 上下文压缩:把断点变成计划内事件。 压缩本质上是一次前缀重置——所有KV状态作废,压缩后的第一轮请求几乎必然未命中。因此压缩应当低频、可预期:按Token阈值触发、在任务边界触发,而不是每轮都做增量截断。
5. 运行时状态:统一后置。 当前时间、任务进度、重试计数这类每轮变化的信息,应集中放在消息序列末尾:
messages = [{"role": "system", "content": SYSTEM_PROMPT}, # 会话期冻结*history, # append-only{"role": "user","content": f"[环境] 当前时间 {now()},请继续。"} # 动态信息放最后]
6. Provider边界:收敛组装逻辑。 不同服务对缓存粒度与计费口径的细节可能不同,“构造稳定前缀”的逻辑应收敛在统一的组装层,而不是散落在各业务调用点。这样适配多模型或切换服务时,前缀策略才不会失控。
事实上,包括百度智能云在内的主流大模型服务平台,普遍提供上下文缓存能力,并在计费上区分命中与未命中。接口是现成的,真正拉开差距的,是调用方能否持续送出稳定的前缀。
组装链上任何一环引入不确定性,都会立刻反映在命中率上。这正是命中率最有价值的地方:它不只是账单上的一项数字,还能反向检验运行时是否稳定。
实践中可以建立三个习惯:
此外,命中还有一份隐性收益:复用KV状态能明显降低首Token延迟(TTFT),对交互式Agent的体感提升非常直接。成本与延迟在同一条前缀上汇合,守住了前缀,就同时守住了两者。
回头看,守住缓存命中率是三层工程的叠加:
前缀设计得再漂亮,如果运行时每轮都重排工具、改写旧消息,缓存一样守不住;反过来,运行时再确定,若前缀里塞满每轮变化的时间戳,也无济于事。99%的命中率,归根结底不是运气,而是一条确定性组装链的自然结果。对认真做Agent工程的团队来说,守住请求前缀,就是守住成本、延迟与稳定性三者的交汇点。