多轮对话里怎么让 Grok 的缓存一直保持命中

2026-08-25

数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。

多轮对话里 Grok 的缓存能不能一直命中,取决于一件很朴素的事:你有没有做到「只往末尾追加,绝不回头动前面」。 xAI 官方文档写得非常直接——任何对早前消息的改动都会打断缓存,只能在末尾追加新消息。配合固定的会话 ID 把请求路由到同一台服务器,再用响应里的 cached_tokens 确认命中,这三件事做齐了,多轮对话的缓存就是可以持续吃到的。反过来,最常见的三个翻车动作是:为了省 token 把历史里的助手回复裁短、把某条消息删掉、把 system 和 user 的顺序换了——官方文档把这三种情况分别列了示例,结果都是缓存未命中。推理模型还多一条:官方文档明确指出,不带回上一轮的 reasoning_content 是缓存未命中的首要原因。

先把「前缀」这个词理解对

官方的 How It Works 页面对机制的描述只有一句话,但这句话决定了后面所有的操作纪律:缓存从 messages 数组的开头 开始工作,请求到达时系统检查开头有多少条消息与之前的请求完全一致,这一段匹配上的部分就是「前缀」,由缓存提供服务。同一页把流程拆成三步——首次请求会把完整提示处理并缓存下来,后续请求只要前缀对得上就复用缓存部分,而复用的那部分按较低的费率计费。

这句话的分量在于它决定了失效的传播方向:前面一动,后面全废。不是「改了哪条哪条失效」,而是从被改动的那一条开始,往后所有内容都得重新计算。所以在一段长会话里,越靠前的内容越金贵——它被复用的次数最多,也最经不起折腾。

官方给的最小例子就是两次请求:第一次是 system + user + assistant 三条,第二次在末尾追加一条新的 user 消息。前三条与上一次逐字一致,于是走缓存,只有新追加的那条需要计算。这也是多轮对话的理想形态。

第一步:给这段会话钉死一个 ID

自动缓存是默认开着的,官方文档说 xAI API 会自动执行提示缓存。但缓存条目是按服务器存储的,请求落到哪台机器上决定了那台机器上有没有你的前缀。所以官方推荐设置会话标识,把同一段对话的请求路由到同一台服务器上去。

三套接口对应三种写法,官方文档分别给了示例:

  • Chat Completions API:加 HTTP 头 x-grok-conv-id
  • Responses API:在请求体里直接放 prompt_cache_key 字段,官方说明它的作用与设置 x-grok-conv-id 完全一致
  • gRPC API(xAI SDK):把 x-grok-conv-id 作为 gRPC metadata 传入,实现粘性路由

Chat Completions 这一路最常见,用 OpenAI SDK 的话就是走 extra_headers

response = client.chat.completions.create(
    model="grok-4.6",
    messages=messages,
    extra_headers={"x-grok-conv-id": conversation_id},
)

ID 取什么值?官方最佳实践里写的是「使用稳定的会话 ID,一个 UUID 或者你应用自己的 session ID 都可以」。重点在稳定——同一段对话从第一轮到最后一轮用同一个值。有个反向用法也值得知道:官方 FAQ 里提到,如果你想强制一次未命中,换一个不同的 x-grok-conv-id 或者干脆不带这个头就行,请求会被路由到可能不同的服务器上,那里没有你的前缀缓存。调试对照的时候用得上。

第二步:每轮只追加,三类动作一律禁止

官方那页标题就叫 What Breaks Caching,正文第一句是「对早前消息的任何改动都会打断缓存,只在末尾追加新消息」。文档里的告警框把禁令说得更硬:为了让多轮对话命中缓存,永远不要编辑、删除或重排早前的消息

它给了三个反例,都是真实项目里会不小心做出来的:

改:把历史里的助手回复裁短

最典型的动机是省钱——上下文越滚越长,有人会把之前那条冗长的助手回复精简成一句话再传回去。官方示例里正是把一条助手消息的内容缩写了,结果标注为缓存未命中。这个操作很讽刺:为了省输入 token 而重写历史,代价是整段前缀重算,按完整价而不是缓存价计费。

删:把某条消息移除

滑动窗口式的上下文裁剪属于这一类。官方示例是把中间那条助手消息整条拿掉,同样是未命中。如果你确实需要控制上下文长度,得接受「裁剪的那一刻缓存重置」这个事实,而不是指望裁完还能接着命中——所以裁剪频率越低越好,别每轮都裁一点。

换:调整消息顺序

官方示例把 user 消息挪到了 system 消息前面,仅仅是顺序变了,前缀匹配就断了。官方最佳实践第 3 条也是同一句话的另一种说法:永远不要修改早前的消息,只追加新的,任何编辑、删除或重排都会打断缓存。

这一类最难查,因为你自己维护的那个消息列表可能确实没动过,但真正发出去的请求体是经过 SDK、中间层、序列化一路加工之后的产物。所以核对对象必须是最终发出去的那份 messages,不是你手里的那个变量。

顺带一条正向建议,官方最佳实践里叫 front-load static content:把系统提示词、few-shot 示例、参考文档统统放在数组最前面,让它们构成一段稳定的前缀。这些内容本来就不该随轮次变化,放在前面等于把复用价值最大化。这一条和怎么把 Grok 的缓存命中率做上去里讲的思路是同一件事的两面。

推理模型:reasoning_content 必须原样带回去

这是多轮场景下最容易漏的一条,官方文档直接把它点名为「缓存未命中的首要原因」(the top cause of cache misses):对于推理模型,你必须把上一轮响应里的 reasoning_content 包含进去。

官方给了两条可选路径来保持命中:

  1. 把加密的推理内容回传。文档说明推理内容由 xAI 加密,在 Responses API 里传 include: ["reasoning.encrypted_content"] 可以拿到,之后可以把这段加密内容随新请求送回去,为之前的对话提供上下文。
  2. 改用有状态的续接。用 previous_response_id 自动接续对话——Responses API 允许你只发上一次响应的 id 加上要追加的新消息,不必把完整历史重发一遍。

第二条背后是服务端存储:官方文档说明之前的输入提示、推理内容和模型响应会保存在 xAI 服务器上,这个行为默认开启,也可以用 store: false 关掉。服务端留存是有期限的,超期之后就得靠你自己在本地保存历史与加密思考内容再传回,具体期限以官方文档当前版本为准。这里有个取舍:走 previous_response_id 省心,但把状态托管给了服务端;自己维护完整 messages 数组更可控,代价是必须严格遵守「只追加」纪律。

第三步:用 cached_tokens 确认,而不是靠感觉

官方文档给出了明确的读数位置,两套接口字段路径不同,这一点很容易搞混:

  • Chat Completions API:usage.prompt_tokens_details.cached_tokens
  • Responses API:usage.input_tokens_details.cached_tokens
  • gRPC(xAI SDK):文档示例里读的是 response.usage.cached_prompt_text_tokens

取值怎么解读,官方也列了表:为 0 是未命中,整个提示从头计算,首次请求或缓存被驱逐之后出现属正常;大于 0 是命中,数值表示被复用了多少 token;等于 prompt_tokens 是完全命中,官方注明这种情况少见,通常发生在重发完全相同的请求时。

官方文档还描述了一段健康的多轮对话应有的形态:cached_tokens 随轮次递增——第一轮建立缓存,之后每一轮命中的量大致等于上一轮的提示长度。所以线上监控别只看单点,把它按轮次画成曲线,不涨或者掉回零就说明前缀在某个环节被打断了。

官方给的排查提示也很直白:如果同一段会话里多次请求的 cached_tokens 持续为 0,先确认你有没有设 x-grok-conv-id(或 prompt_cache_key),再确认两次请求之间有没有改动过早前的消息。官方给出的排查顺序就是这两条,先查会话标识有没有带上,再查历史消息有没有被动过;最佳实践第 5 条说的也是同一件事——监控 cached_tokens,持续为 0 就去核对会话 ID 与消息顺序。至于命中之后账单上体现成什么样,可以对照API 提示缓存的计费机制那篇的通用口径来看。

命中是优化,不是承诺

这一点必须写进代码假设里。官方在 How It Works 页面挂了告警:提示缓存不是 100% 保证的,缓存条目可能因内存压力被驱逐,请求也可能被路由到不同的服务器。最佳实践里对应的一条是「优雅处理未命中」——驱逐和路由意味着命中无法保证,你的应用必须在没有缓存的情况下也能正常工作。

翻译成工程约束:不要把缓存命中当成延迟预算的前提,不要把未命中当成错误来处理,也不要围绕命中率设计强依赖的业务逻辑。官方那张 cached_tokens 对照表已经把话说明白了——取值为 0 在首次请求或者缓存被驱逐之后本来就是预期之内的结果,把一个预期之内的状态当成故障去报错、去触发重试链路,等于给自己凭空加了一条失败路径。缓存是省下来的,不是借来的。

顺带纠正一个容易反过来理解的地方:官方对「完全命中」的注解写的是这种情况少见,通常发生在重发完全相同的请求时。也就是说重发相同请求并不必然再次未命中。但这不构成「靠重试来救命中率」的理由——x-grok-conv-id 只能最大化留存、不能保证留存,重试落到哪台服务器仍然是不确定的,正确的做法还是让应用在没有缓存的情况下也能照常工作。

关于持久性,官方 FAQ 的原话是缓存条目可能因服务器负载或重启在任何时候被驱逐,用 x-grok-conv-id 路由到同一台服务器可以最大化留存。注意这句的措辞是「最大化留存」,不是「保证留存」。

几个边界问题,官方都有明确答复

多轮场景里常见的三个疑问,Best Practices & FAQ 页面正面回答了:

  • 会不会影响输出质量:不会。官方说明缓存只加速提示处理阶段,无论提示是走缓存还是从头计算,模型输出是一致的。
  • 流式能不能用:可以,缓存对流式和非流式请求都生效。官方还补了一句细节——流里第一个空 token 对应的就是缓存查找与 prefill 阶段。
  • 工具调用能不能用:可以。官方说明可缓存的前缀包含直到工具调用结果为止的所有消息,只要前缀不变,后续请求就能吃到缓存。

第三条对 Agent 类应用意义很大:工具返回的结果本身也会进入可缓存前缀,所以一轮工具调用的产出在下一轮是能被复用的——前提还是那句话,别回头去美化或者压缩工具返回的内容。

至于哪些模型支持,官方 Best Practices 页写的是所有 grok 语言模型都提供提示缓存,具体哪些模型支持以及各自的缓存 token 价格,让你去定价页查。这个说法本身会随模型迭代变化,以官方文档为准。

最容易栽的坑

对照官方点名的三类破坏动作,落到实际项目里最常见的三种形态是:上下文压缩、消息顺序被动过、推理内容漏传

上下文压缩最隐蔽,因为它看起来是在省钱,实际上每压缩一次就把整段前缀作废一次,省下的输入 token 未必抵得上重算的开销。真要压,就压得少一点、间隔长一点,别做成每轮都执行的常规动作。

顺序被动过最难查,因为它往往不体现在你写的那几行代码里。定位方法是把最终发出去的 messages 数组原样打印出来逐轮 diff,一条一条比对到字符级,而不是看你自己维护的那个列表。

推理内容漏传最冤,因为它不会报错,只是 cached_tokens 一直是零。官方已经明说这是首要原因,用推理模型时先查这一条。

下一步建议做两件事:一是把 cached_tokens 接进你的成本看板,按会话 ID 聚合观察递增曲线,做法可以参考怎么优化 API 缓存命中率;二是在发请求前加一道断言,校验本轮 messages 的前 N 条与上一轮逐字相同,不同就打日志、并把差异那条一起记下来。这道断言的价值在于把问题拦在发出之前:等到 cached_tokens 掉回零再回头查,你已经分不清是自己动了历史,还是缓存被驱逐、请求被路由到了别的服务器——而这两类原因的处置方式完全不同,前者要改代码,后者只能接受并优雅降级。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。