我们有个智能体业务,接入线上之后输入 Token 的账单一直不正常。查了一圈才发现:缓存这个开关我们是开了的,但基本没命中,等于白开。这篇把整个排查过程、接入代码和踩过的坑记下来。
大模型不记事,每次请求都要把整段上下文从头算一遍(预填充),这部分算在输入上。我们的智能体办一件事大概要走二十多步、调几十次工具,每一步都带着同一份系统提示词——两万多 Token 的那种。也就是说同一段话,被重复计费了二十多次。
所以第一反应是用前缀缓存:把前面那段稳定的内容算完后存下来,后面请求直接复用。但真正接上以后,账单几乎没动。
我们用的是 Anthropic 的接口,缓存的开关不是"全局打开",而是要显式打缓存断点(cache_control):
import anthropic
client = anthropic.Anthropic()
resp = client.messages.create(
model="claude-...", # 换成你实际使用的模型
max_tokens=1024,
system=[
{
"type": "text",
"text": STATIC_SYSTEM_PROMPT, # 稳定段:角色、规则、工具定义
"cache_control": {"type": "ephemeral"}, # 断点打在稳定段的末尾
}
],
messages=build_messages(),
)
u = resp.usage
# 关键:这三个字段要一起落日志,否则根本查不出问题
print(u.input_tokens, # 未命中、按基础价计的输入
u.cache_creation_input_tokens, # 写入缓存的输入
u.cache_read_input_tokens) # 命中读取的输入这段代码本身没问题,问题出在 STATIC_SYSTEM_PROMPT 和 messages 的边界上。
我们用 print(u.cache_read_input_tokens) 一看,几乎一直是 0,说明压根没读命中。
排查下来,原因是系统提示词里塞了一块"工作记忆"——智能体当前的状态、上一步查到的结果,都拼在里面。而这块东西几乎每走一步就会被改写一次,偏偏它夹在缓存断点之前。缓存存的是算完的前缀状态,要求逐字一致;前面隔几步就变一个字,后面的缓存全部作废。
改成只有一个动作:把那块会变的内容从 system 里挪出来,放到 messages 末尾的用户消息里。断点仍然只打在前面的稳定段。
messages=[
{"role": "user", "content": STATIC_EXAMPLES}, # 稳定示例,靠前
{"role": "assistant", "content": STATIC_REPLY},
# ……历史轮次保持追加、不重写……
{"role": "user", "content": WORKING_MEMORY}, # 会变的工作记忆,挪到最后
]一次上线,命中率从 7% 升到 74%,几周后稳定到 84%,整体调用成本降了 59%。
缓存不是免费的,第一次写入比不缓存贵一点,之后命中读取才便宜。以 Anthropic 公开口径为例(各模型有差异):
写入多付 0.25,一次命中省 0.9,所以第一次取出来就把多付的赚回来了。假设八成命中、两成没命中,输入这一栏的总开销能落到原来的三成三左右。
两个别算错的地方:
缓存失手不报错、不打警告,返回内容一模一样,只有账单悄悄变贵。这四种要记牢:
如果模型是自己部署的,省的是显卡时间,验收方式也不一样。
自建的验收不看开关看计数:拿同一段前缀冷热各压一轮,对比命中计数。参数写上去了,不等于真命中。
静态的放前面,会变的那几行挪到末尾;开着缓存不等于命中,先去看那两个计数。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。