简介:本文系统对比大模型推理中“缓存命中”与“缓存未命中”两条路径的差异:从KV Cache复用原理、计费结构、首token延迟到生效条件与失效风险,并结合Agent长上下文场景,给出提升命中率的上下文组织方法、用量监控指标与平台选型建议,帮助开发者有效降低长对话场景的token成本。
一个典型的 Agent 任务跑完,模型可能被调用上百次。按直觉推算,每次请求都携带几十万 token 的完整对话历史,账单应当随调用次数成比例膨胀,但实际账单往往远低于这个估算。原因并不神秘:它记录在用量接口的一个字段里——不同服务商对它的命名不同,常见表述是“缓存命中 token 数”或“缓存读取输入 token 数”。
对某类 Coding Agent 的实测数据显示,Agent 单步的典型形态是:读取约 119K token 的历史前缀,只对新增的约 875 token 做完整计算,而前者绝大部分按普通输入十分之一的价格计费。换言之,同样的对话历史,“是否命中缓存”直接决定了它按什么价格、走什么计算路径被处理。
这背后是推理系统的一条底层规则。本文对比的对象,就是大模型推理中的两条处理路径——缓存命中与缓存未命中,并延伸对比自动前缀缓存与显式缓存管理两种实现方式。理解这对差异,不只是看懂账单的问题:API 定价、显存管理、集群调度、Agent 框架设计,全部落在这条规则的约束之下。
当请求的输入前缀在推理服务的缓存中不存在(或已过期、或被调度到未持有该缓存的实例)时,推理系统需要对全部输入 token 重新执行 prefill 计算:把所有输入 token 过一遍模型,逐层计算出 Key/Value 张量(即 KV Cache),然后才开始逐 token 生成输出。这条路径下,全部输入 token 按标准输入单价计费,输入越长,prefill 计算量与首 token 延迟越高。
当请求的输入前缀与缓存中已有的某段前缀逐 token 完全一致,且缓存仍然有效时,推理系统可以直接复用已计算好的 KV Cache,跳过对这部分前缀的重复 prefill,只对新增的尾部 token 做完整计算。这条路径下,命中部分按折扣价计费(行业常见折扣约为普通输入价的十分之一,具体以服务商定价为准),只有未命中的新增部分按常规输入计费。
需要强调的是,Transformer 推理分为 prefill(并行处理输入、生成 KV Cache)与 decode(逐 token 生成)两个阶段。前缀缓存的本质,是把 prefill 阶段最昂贵的计算结果跨请求保存下来复用。这也解释了为什么命中部分“不是免费、而是打折”:平台省下了大量算力,但仍要承担缓存数据在显存/内存中的占用与管理成本。
未命中路径对全部输入执行 prefill,计算量与输入长度成正比;命中路径复用前缀 KV,只对新增 token 计算。以 119K 前缀 + 875 新增的请求为例,两条路径的 prefill 计算量相差约两个数量级,这正是折扣定价的物理基础。
未命中路径下,全部输入按标准输入单价计费,账单结构简单。命中路径下,输入被拆成两部分:命中部分按折扣价(部分采用显式缓存管理的服务还可能对缓存写入收取一次性费用),新增部分按常规价。这意味着成本估算公式发生变化,必须引入“命中率”这个变量。
prefill 是首 token 延迟(TTFT)的主要来源。命中缓存后大部分 prefill 被跳过,TTFT 明显下降,且输入越长、改善越明显。对交互式 Agent、代码补全等响应速度敏感的场景,这条差异的价值不亚于成本节省。
未命中路径行为确定:只要请求合法,处理路径可预期。命中路径则依赖三个条件同时成立:前缀逐 token 一致、缓存未过期(存在 TTL)、请求被调度到持有对应缓存的实例。任何一条不满足即退化为未命中,成本与延迟回到常规水平。
支持前缀缓存的服务通常在用量接口中把输入 token 拆分为“命中”与“未命中”两部分分别上报,便于核算命中率与成本归因;未命中路径下则只有统一的输入 token 计数。可观测性差异直接决定了团队能否做精细化的成本优化。
命中路径引入了新的失效点:前缀漂移(在历史中间插入或修改内容会使其后所有缓存失效)、TTL 过期(低频请求难以在有效期内复用)、路由打散(多实例部署下请求被负载均衡分配到不同节点)、容量淘汰(显存紧张时缓存条目被驱逐)。这些风险在未命中路径下不存在。
| 对比维度 | 缓存命中(读取缓存前缀) | 缓存未命中(常规输入) |
|---|---|---|
| 计算路径 | 复用前缀 KV Cache,仅对新增 token 做 prefill | 全部输入 token 重新执行 prefill |
| 计费价格 | 命中部分按折扣价(常见约 1/10,以服务商定价为准) | 全部输入按标准输入单价 |
| 首 token 延迟 | 显著降低,输入越长改善越明显 | 随输入长度线性增长 |
| 生效条件 | 前缀逐 token 一致 + 缓存有效 + 路由匹配 | 无前置条件 |
| 输出质量 | 与未命中一致 | 基准 |
| 可观测性 | 用量字段区分命中/未命中 token | 统一输入 token 计数 |
| 失效风险 | 前缀漂移、TTL 过期、路由打散、容量淘汰 | 无缓存相关风险 |
| 适用形态 | 前缀稳定、历史只追加的请求 | 前缀多变、一次性请求 |
行业内实现缓存命中通常有两种方式,选型时同样需要区分:
| 维度 | 自动前缀缓存(隐式) | 显式缓存管理 |
|---|---|---|
| 接入成本 | 低,通常无需改动请求结构 | 需调用缓存创建/管理接口或指定参数 |
| 命中控制 | 平台自动匹配,开发者不可直接干预 | 开发者显式指定缓存内容与生命周期 |
| 灵活性 | 依赖请求前缀天然一致 | 可将超长文档、大规模 few-shot 主动缓存 |
| 适用团队 | 请求形态规整、追求快速接入 | 有精细成本控制与长内容复用需求 |
# 反例:动态内容混入前缀,导致缓存反复失效system_prompt = f"你是代码助手。当前时间:{now()}。请遵循规范:{spec}"# 正例:稳定前缀 + 追加式历史 + 动态内容后置messages = [{"role": "system", "content": "你是代码助手。请遵循规范:……"}, # 稳定前缀,可被缓存{"role": "user", "content": "实现函数A"}, # 历史只追加、不修改{"role": "assistant", "content": "……"},{"role": "user", "content": f"当前时间:{now()}\n{question}"} # 动态内容放尾部]
# 伪代码:从用量字段拆解单次请求成本usage = response["usage"]input_all = usage["输入token总数"]hit = usage["缓存命中token数"]fresh = input_all - hitoutput = usage["输出token数"]cost = hit * 单价(缓存命中) + fresh * 单价(常规输入) + output * 单价(输出)hit_rate = hit / input_all # 持续监控命中率,跌破阈值即告警
回到开头的问题:Agent 跑了上百次调用、每次携带几十万 token 历史,账单却没有成比例膨胀,答案就是“缓存命中”。缓存命中与缓存未命中这对路径,共享同一个模型与接口,却在计算路径、计费结构、延迟、生效条件与失效风险上存在系统性差异。对开发者而言,真正的杠杆不在“选不选缓存”,而在三件事:把上下文组织成“稳定前缀 + 动态尾部”的形态、在用量字段中持续监控命中率、在选型时把缓存定价与生效条件纳入成本模型。在 Agent 与长上下文应用逐步成为主流的今天,这条推理系统的底层规则,值得每一位技术负责人纳入架构与预算决策。