第 6 篇把缓存与压缩的取舍算成了一笔战术账:命中读取只要 0.1 倍输入价,而压缩改写历史会把这笔折扣亲手作废。那篇的视角是单次决策——这一轮压还是不压。这一篇把镜头拉远:当一个系统每天跑几万轮 Agent 调用,命中率就不再是一次代码优化的副产品,而是一个值得上监控大盘、定告警线、进周报的业务指标。
这是《上下文工程》系列第 14 篇。本文先算清命中率的回报杠杆,再给出决定它的第一性原理——前缀稳定性与稳定金字塔,接着过一遍四大命中杀手的破坏路径、Anthropic 四断点的布局策略、多用户场景的前缀共享设计,最后附上监控代码与一次从零命中到九成的重构实录。
1、命中率的经济账
先把口径钉死:命中率 = cache_read_input_tokens ÷(cache_read_input_tokens + cache_creation_input_tokens + input_tokens)。分母是本轮全部输入——注意 input_tokens 只统计未缓存的全价部分,三个字段相加才是完整输入,单看任何一个都会误判。
算一笔典型账。一轮 Agent 调用输入 50K token,其中 45K 命中读取、2.5K 是本轮新增写入、2.5K 是断点后的全价尾巴,按 Sonnet 5.5 口径(输入 $2/MTok,写 1.25 倍、读 0.1 倍,计价细节见官方缓存文档):
- 命中读取:45K × $0.2/MTok ≈ $0.009
- 新增写入:2.5K × $2.5/MTok ≈ $0.006
- 全价尾巴:2.5K × $2/MTok ≈ $0.005
- 单轮输入合计约 $0.020,全价对照是 $0.100
输入侧成本砍掉八成。输出 token 不参与缓存,摊到整张账单上依然省一半以上——"账单近乎减半"是保守说法。再对比其它优化杠杆:换小模型动质量、砍历史动能力、改提示词动行为,唯独命中率只动计价系数、不动任何模型行为,是上下文工程里回报率最高、风险最低的指标。
杠杆的另一头是写入溢价。1.25 倍的写入费意味着断点打错位置是纯亏:打在每请求独有内容的末尾,等于花 1.25 倍买一个永远不会被读回的条目。断点必须落在"之后还会被整段读回"的边界上——这正是第 4 节的主题。
2、前缀稳定性金字塔
第 6 篇讲过命中的唯一条件:前缀逐字节一致。由此推出命中率的第一性原理:命中率由前缀稳定性决定,前缀稳定性由内容排列顺序决定。渲染字节序固定为 tools → system → messages(第 2 篇埋的伏笔),你能控制的只有每层内容的排布与 messages 内部的顺序。
设计纪律一句话:按变化频率分层,把变的东西赶到上下文尾部。五层从稳定到易变依次排列如下:
每一层的稳定契约不同。系统提示与工具定义做到版本级冻结:内容只在发版时变,工具清单固定排序。长期记忆会话级稳定:加载一次贯穿全程。对话历史只增不改:追加新轮次,绝不回头编辑。当前轮天然易变:检索结果、时间戳、用户输入都在这里——它们是塔尖,本来就不该被缓存,也无需被缓存。
排列错了,断点标记贴再多也没用:一个时间戳插在 system 第二行,其后 40K token 的缓存全部作废。这套纪律对三家通用——OpenAI 全自动缓存要求前缀 ≥1024 token,Gemini 隐式缓存自动省约 90%(OpenAI 缓存指南、Gemini 缓存文档),原理同为前缀匹配,破坏前缀在哪一家都是纯损失。最常见的破坏姿势,下一节清点。
3、四大命中杀手
四个反模式的共同点是不报错:请求照常成功、回答照常生成,只是
cache_read_input_tokens静默归零,账单默默回到全价。
反面教材如下:
# 三个命中杀手挤在同一段拼装代码里
system = f"当前时间:{datetime.now()}\n" + BASE_PROMPT # 杀手一:时间戳在前缀头部
tools = list(TOOL_SET) # 杀手二:set 遍历序逐次漂移
system += "\n相似案例:\n" + retrieve_cases(query, 3) # 杀手三:动态内容塞进 system
四宗罪的完整破坏路径:
| 反模式 | 常见动机 | 命中破坏分析 |
|---|---|---|
| 每轮重排工具顺序 | 把最可能用的排前面 | 工具渲染在最前端,顺序一变三层缓存全灭,波及所有会话 |
| 时间戳注入 system | 让模型知道当前时间 | 前缀第二个字节就漂移,其后全部作废,命中率归零 |
| 动态 few-shot 塞头部 | 检索相似案例提质 | system 每轮不同,写入费照付、条目永不复用,纯亏溢价 |
| 过早压缩 | 省 token | 每轮改写历史,消息区永不命中,第 6 篇模拟贵 69% |
修复手法一律是"挪"而不是"删"。时间戳与 few-shot 检索结果挪到最后一个断点之后,作为消息内容注入;工具按名排序后冻结成模块级常量;压缩改为按第 4 篇的预算线触发,平时保持 append-only。中途要下达指令,用 {"role": "system"} 消息追加在历史尾部——前缀不动,只影响当前轮。
4、断点布局策略
Anthropic 的缓存要显式打 cache_control 断点,每个请求上限 4 个——刚好覆盖金字塔的四层台阶。推荐的断点布局如下(字节从前往后):
四个断点各就各位的请求骨架如下:
{
"model": "claude-sonnet-5-5",
"tools": [
{"name": "search_logs", "description": "检索日志",
"input_schema": SCHEMA,
"cache_control": {"type": "ephemeral"}}
],
"system": [
{"type": "text", "text": "……冻结的系统提示……",
"cache_control": {"type": "ephemeral"}}
],
"messages": [
{"role": "user", "content": [
{"type": "text", "text": "……长期记忆……",
"cache_control": {"type": "ephemeral"}}]},
{"role": "assistant", "content": "……已定稿历史……"},
{"role": "user", "content": [
{"type": "text", "text": "……本轮输入……",
"cache_control": {"type": "ephemeral"}}]}
]
}
四个断点各保护一段资产:断点 1 打在最后一个工具上,让工具层单独上保险——哪怕 system 按用户定制,工具前缀仍可跨用户复用;断点 2 冻结 tools + system 组成的最大稳定块;断点 3 封存"已定稿"的历史,此后近段哪怕被清理改写,旧段照常命中;断点 4 是移动断点,钉在最新一条用户消息上逐轮前移,形成"读全量旧前缀、写本轮增量"的循环。
三条使用纪律。其一,断点之前的内容必须超过模型的最小可缓存长度(512~4096 token 不等),否则静默不缓存。其二,长 TTL 条目要排在短 TTL 之前:共享前缀用 {"type": "ephemeral", "ttl": "1h"}、会话尾部用默认 5 分钟是合法组合,反过来会被拒绝。另外新 SDK 支持请求级 cache_control 自动落断点,多轮会话可以当默认,但"共享前缀 + 尾部独有内容"的场景仍要显式断点——自动断点会落在独有内容之后,白付写入费。
5、多用户前缀共享
命中率经营到多用户规模,缓存对象分两类。会话内缓存是天然的:历史只增不改,命中率随轮次自然爬升,做好第 4 节的断点就白拿。真正的设计题是跨用户共享前缀——把所有用户共享的资产做成字节级相同的模板,让用户 A 写入的缓存条目被用户 B 命中。
服务端模板的要点:模板字符串版本化冻结,动态数据一律后置到断点之后。示例如下:
from anthropic import Anthropic
client = Anthropic()
COMMON_TOOLS = sorted(ALL_TOOLS, key=lambda t: t["name"]) # 冻结排序
COMMON_SYSTEM = [{"type": "text", "text": SYSTEM_TEXT,
"cache_control": {"type": "ephemeral", "ttl": "1h"}}]
def ask(user: dict, history: list, query: str):
tail = history + [{"role": "user", "content": [
{"type": "text", "text": user_profile(user)}, # 用户数据在断点后
{"type": "text", "text": query,
"cache_control": {"type": "ephemeral"}}]}]
return client.messages.create(
model="claude-sonnet-5-5", max_tokens=4096,
system=COMMON_SYSTEM, tools=COMMON_TOOLS, messages=tail)
共享层用 1 小时 TTL 是划算的:2 倍写入价被全天所有用户分摊,任何用户隔二十分钟回来都还能命中公共前缀;会话尾部保持默认 5 分钟,因为同一会话的轮间隔远短于 5 分钟——长 TTL 在前、短 TTL 在后,恰好满足第 4 节的排序纪律。
两个工程含义。用户身份、偏好、权限这类 per-user 数据一旦插进 system,共享前缀就被打碎成每用户一份,跨用户命中率直接归零——它们只能活在断点之后。并发扇出还有时序坑:N 个 worker 拿同一份上下文并行起跑,在第一个响应开始流式输出之前,谁也读不到谁写的缓存;先放一个探路请求、等到首 token 再扇出其余,就能写入一次、命中 N-1 次。
6、命中率监控报表
指标要经营,先得可观测。每次响应的 usage 三字段就是全部原始数据,采集与会话级聚合如下:
from collections import defaultdict
turns: dict[str, list] = defaultdict(list)
def record(session_id: str, usage) -> None:
turns[session_id].append((usage.cache_read_input_tokens,
usage.cache_creation_input_tokens,
usage.input_tokens))
def hit_rate(session_id: str) -> float:
rows = turns[session_id]
total = sum(r + w + f for r, w, f in rows)
return sum(r for r, _, _ in rows) / total if total else 0.0
def report() -> None:
order = sorted(turns, key=hit_rate) # 升序:末尾就是待排查名单
for sid in order:
print(f"{sid[:8]} 命中率 {hit_rate(sid):6.0%} 轮数 {len(turns[sid])}")
健康会话的形态有固定签名:读取逐轮增长、写入稳定在小增量、全价 token 只剩断点后的尾巴。低于 80% 的会话回第 3 节的反模式清单逐项对照。
两个补充。总输入等于三字段之和,别拿 input_tokens 单独当输入量——Agent 跑了几小时它只剩几 K 是正常的,那正是缓存生效的证据。另外要把验证做成回归防护:集成测试里连发两个同前缀请求,断言第二个的 cache_read_input_tokens > 0。缓存失效是最典型的静默回归——上线时好好的,某次"顺手优化"动了拼装顺序,之后几个月没人发现。
7、零命中率改造实录
一个真实形态的重构。某运维排查 Agent:system 3K、工具 12 个共 5K、每轮检索 3 条相似案例约 2K、历史逐轮增长,日均 400 轮。接手时报表全红——命中率 0%。排查发现四宗罪与第 3 节一一对应:datetime.now() 写在 system 首行;工具列表由 set 遍历拼装;相似案例拼进 system 尾部;每轮触发一次压缩。
改造分四步:时间戳与案例检索挪到最后一个断点之后以消息注入;工具按名排序冻结为模块级常量;压缩改为按预算线触发,历史保持 append-only;按第 4 节布局打满四个断点。改造前后各跑一天,单轮平均 usage 对比:
| 指标(单轮均值) | 改造前 | 改造后 |
|---|---|---|
| 命中读取 token | 0 | 47,300 |
| 缓存写入 token | 0 | 3,100 |
| 全价 token | 52,000 | 1,600 |
| 命中率 | 0% | 91% |
| 输入成本($2/MTok 口径) | $0.104 | $0.020 |
输入成本降八成,叠加不可缓存的输出后整单仍省约六成;按日均 400 轮折算,每月省下约一千美元,而全部改动不到百行代码。比省钱更有价值的是确定性:断点布局与拼装纪律落成模板代码后,新会话上线即命中,不再依赖某个工程师的手气。
8、总结
把这一篇收拢成四句话。命中率是上下文工程里回报率最高的指标:九成命中即输入打二折,且不碰任何模型行为。命中率由前缀稳定性决定,设计纪律是稳定金字塔——system 与工具版本级冻结,变的东西赶到尾部。四大命中杀手都不报错,唯一防线是 usage 三字段的持续监控加回归测试。断点是稀缺资源,4 个名额打在稳定台阶的边界上,多用户场景把公共前缀做成字节级模板。
留一个练习:给手头的 Agent 加上第 6 节的 record,跑一个真实工作日;找出命中率最低的会话,用第 3 节的清单定位杀手;修完再跑一天,把两天的报表贴在一起——这张对比表会让你从此把命中率当成一等公民。
对于在维护生产 Agent 的团队,建议本周就把 usage 三字段打进日志:这是零成本改造,却往往能立刻暴露一个正在静默烧钱的前缀缺陷。

