Grok 缓存前缀与 cache key
缓存这件事最容易被误判。团队看到官方有缓存价,就在代码里把 system prompt 抽出来复用,上线一个月,账单没动。翻 cached_tokens,一直是 0。原因通常不是「缓存没开」,而是前缀被改了:有人在 system 里拼了时间戳,有人把检索到的文档插在了对话中间,有人每次重建数组时顺手调整了顺序。
这一节把缓存的匹配规则讲成可检查的函数,并给出两个 key 的正确位置——x-grok-conv-id 与 prompt_cache_key。
一、问题不是「缓存没开」,而是「前缀被改了」
按 2026-11-04 官方 Prompt Caching 文档,缓存从 messages 数组的开头开始匹配,只有与上一次请求逐条完全相同的前缀才会被复用。命中后有两个收益:首 token 更快,缓存 token 按更低单价计费。
三条约束决定了它能不能生效:
- 只能追加。编辑、删除、重排任何一条较早的消息,都会让前缀从改动处断开。
- 按服务器存储。缓存条目存在单台服务器上,请求路由到别的服务器就查不到。
- 不保证命中。内存压力会驱逐条目,重启也会。官方明确写了它不是 100% 保证。
所以工程上要做的不是「打开缓存」,而是把前缀做成稳定结构,再用 key 把同一会话的请求尽量钉到同一台服务器。
二、最小可运行示例:前缀命中检查器
下面的脚本不联网、不需要真实密钥。它比较两轮 messages,算出「从头开始完全相同」的条数,并据此判断这一轮还能复用多少缓存。
"""缓存前缀检查:判断两轮 messages 是否还能命中缓存。"""
def prefix_len(prev: list[dict], curr: list[dict]) -> int:
"""返回从头开始逐条完全相同的消息条数。"""
n = 0
for a, b in zip(prev, curr):
if a != b:
break
n += 1
return n
def diagnose(prev: list[dict], curr: list[dict]) -> str:
hit = prefix_len(prev, curr)
if hit == len(prev):
return f"前缀完整:{hit} 条全部命中,只计算新增 {len(curr) - hit} 条"
if hit == len(curr):
return f"当前请求全部命中:{hit} 条仍在上一轮前缀内(本轮更短)"
return f"前缀在第 {hit + 1} 条断开,缓存只覆盖前 {hit} 条"
def offline_check() -> None:
base = [
{"role": "system", "content": "你是内部文档问答助手。"},
{"role": "user", "content": "什么是上下文窗口?"},
{"role": "assistant", "content": "上下文窗口是一次请求能带上的 token 上限。"},
]
# 1) 只在末尾追加新问题:前缀不变
append_ok = base + [{"role": "user", "content": "那 20 万阈值呢?"}]
# 2) 改写了 system 的第一个字:从第 1 条就断开
system_edited = [
{"role": "system", "content": "你是问答助手。"},
*base[1:],
{"role": "user", "content": "那 20 万阈值呢?"},
]
# 3) 删掉中间的 assistant 回复:前 2 条仍在
removed = [
base[0],
base[1],
{"role": "user", "content": "那 20 万阈值呢?"},
]
print("追加新消息:" + diagnose(base, append_ok))
print("改写 system:" + diagnose(base, system_edited))
print("删除中间回复:" + diagnose(base, removed))
# 4) 本轮比上一轮短:删掉尾部消息
print("删除尾部消息:" + diagnose(base, base[:2]))
if __name__ == "__main__":
offline_check()
真正上线时,把 diagnose() 放进会话层:每一轮发出前先用它比对上一轮,命中率异常下降时能立刻指出是哪一类改动造成的,而不是等到月底看账单。
三、cache key 与 cached_tokens 的读取路径
两个 key 不是同一个东西,位置也不同:
| 接口 | 放置位置 | 作用 |
|---|---|---|
| Chat Completions | HTTP 头 x-grok-conv-id |
把同会话请求路由到同一服务器 |
| Responses | 请求体字段 prompt_cache_key |
同上,官方说明功能一致 |
| gRPC(xAI SDK) | metadata x-grok-conv-id |
同上,用于粘性路由 |
读取命中量同样分接口:
| 接口 | 字段路径 |
|---|---|
| Chat Completions | usage.prompt_tokens_details.cached_tokens |
| Responses | usage.input_tokens_details.cached_tokens |
| gRPC(xAI SDK) | usage.cached_prompt_text_tokens |
官方给的判读规则很干净:0 是未命中;大于 0 是命中,数值即复用的 token 数;等于 prompt_tokens 是全命中,通常只在重发完全相同的请求时出现。
三件要强调的事:
- 静态内容要前置。system prompt、few-shot 示例、固定的参考文档放在数组开头,它们才构成稳定前缀。把检索片段插在对话中间,等于每轮都在改写历史。
- 推理模型要带回推理内容。官方把「漏掉上一轮的
reasoning_content」列为缓存未命中的首要原因;另一条路是用previous_response_id走有状态续接。两种做法都指向同一件事:让前缀可复现。 - 长上下文阈值按总 prompt 判定。官方写明阈值计算包含缓存 token,跨档后缓存与非缓存各自按对应高档价计费。所以命中缓存不等于逃过跳档,这两件事要分开算(见 4.3)。
四、预期输出与异常分支
运行前缀检查器,预期输出:
追加新消息:前缀完整:3 条全部命中,只计算新增 1 条
改写 system:前缀在第 1 条断开,缓存只覆盖前 0 条
删除中间回复:前缀在第 3 条断开,缓存只覆盖前 2 条
删除尾部消息:当前请求全部命中:2 条仍在上一轮前缀内(本轮更短)
换成真实调用后,若出现异常,按下面顺序定位:
| 现象 | 优先怀疑 | 第一步动作 |
|---|---|---|
cached_tokens 恒为 0 |
前缀每轮都在变 | 用 prefix_len 比对相邻两轮 |
| 昨天命中今天不命中 | 请求被路由到别的服务器 | 核对 conv-id / prompt_cache_key 是否稳定 |
| 命中数忽高忽低 | 条目被驱逐 | 接受波动,先确认业务不依赖缓存 |
| 流式下命中变少 | 首个空 token 对应缓存查找 | 按官方说明理解,不据此判断失败 |
最后一行尤其重要:缓存是加速与降本手段,不是正确性依赖。官方最佳实践里专门写了「应用应当在没有缓存时也能正常工作」——把这句话写进设计约束,比追求某个命中率数字更实际。
五、判据卡
| 判断项 | 该这么做 | 不该这么做 |
|---|---|---|
| 前缀组织 | 静态内容前置,只追加不改写 | 在 system 里拼时间戳或随机 ID |
| 会话路由 | 稳定 conv-id / prompt_cache_key |
每轮生成新随机 key |
| 命中监控 | 读 cached_tokens 并告警 |
假定「开了就一定命中」 |
| 推理模型 | 带回 reasoning_content 或走有状态续接 |
丢掉上一轮推理内容 |
| 成本判断 | 命中与 200k 跳档分开算 | 以为命中缓存就能免跳档 |
这张卡的重点是先稳住前缀,再谈命中率。四类现象里「恒为 0」和「忽高忽低」常常同源:前缀不稳,表现一个是完全失效一个是随机波动,但修法都在数组组织上。
六、来源与核验
- 官方 Prompt Caching 总览:
https://docs.x.ai/developers/advanced-api-usage/prompt-caching(核验日期 2026-11-04)。自动缓存、x-grok-conv-id建议、子页导航以该页为准。 - 官方 How It Works:
https://docs.x.ai/developers/advanced-api-usage/prompt-caching/how-it-works。开头匹配、前缀定义与「不保证 100%」的说明来自该页。 - 官方 Usage & Pricing:
https://docs.x.ai/developers/advanced-api-usage/prompt-caching/usage-and-pricing。cached_tokens字段路径、判读规则、长上下文阈值含缓存 token 的说明来自该页。 - 官方 Best Practices & FAQ:
https://docs.x.ai/developers/advanced-api-usage/prompt-caching/best-practices。静态内容前置、应用不依赖缓存、缓存不影响输出质量等口径来自该页。 - 缓存单价按模型而异,具体数值以官方定价页与控制台当前显示为准。本节不承诺任何具体命中率。
七、动手试
- 给
diagnose()加一个「重排」用例:把 system 与 user 顺序对调,确认它能判出前缀在第 1 条断开,并解释为什么这类改动在多轮里最难发现。 - 把
prefix_len()扩展到工具调用消息:当一条 assistant 消息里带tool_calls、下一条是tool结果时,验证「只追加」是否仍然成立。 - 设计一个前缀结构:把 system、few-shot、检索文档、对话历史四段排好序,写出你会固定哪几段、哪一段每轮会变,并说明这样排的理由。