怎么把 Grok 的缓存命中率做上去
数据截至 2026-08,价格与限额以各官网为准。本文只讲计费与接入机制,不列具体价格数字。
xAI 的提示缓存是自动生效的,你不需要申请、不需要开开关,但”自动”不等于”高命中”。官方文档把提升命中率这件事归结成两个动作:一是给同一段会话设一个稳定的路由标识(Chat Completions 用 x-grok-conv-id 请求头,Responses API 用请求体里的 prompt_cache_key 字段,gRPC 用同名的 metadata),二是保证消息数组的前缀一个字都不改、只往后追加。命中与否不靠感觉,响应里的 cached_tokens 就是唯一凭据。如果你用的是推理模型,还有第三件事必须做——把上一轮的 reasoning_content 回传,官方文档明说这是缓存未命中的头号原因。
先学会读 cached_tokens,再谈优化
优化之前得先有度量。xAI 文档在「Usage & Pricing」这一节给出了缓存命中的读数位置,三套接口字段路径各不相同,这是最容易踩空的地方:
- Chat Completions API:
usage.prompt_tokens_details.cached_tokens - Responses API:
usage.input_tokens_details.cached_tokens - gRPC(xAI 官方 Python SDK):
response.usage.cached_prompt_text_tokens
拿 Chat Completions 举例,官方给的返回体里 prompt_tokens_details 除了 cached_tokens,还并列着 text_tokens、audio_tokens、image_tokens;completion_tokens_details 那一侧则有 reasoning_tokens、audio_tokens、accepted_prediction_tokens、rejected_prediction_tokens。这几个字段名建议照抄进你的日志埋点,别自己另起名字,后面对账的时候要跟控制台的口径对得上。
官方还给了一张判读表,把 cached_tokens 的取值分成三种情况:等于 0 是完全未命中,整个提示从头算了一遍,首次请求或者缓存被清掉之后出现这个值是正常的;大于 0 是命中,数值本身表示有多少 token 是复用的;等于 prompt_tokens 是全量命中,文档特意标注这种情况少见,通常只在你把完全一样的请求再发一次时出现。
文档里还画了一条多轮对话的典型曲线:第一轮命中为零,第二轮的命中数等于第一轮的提示 token 数,第三轮的命中数等于第二轮的提示 token 数——也就是说健康的多轮会话,命中数应该是台阶式往上走的。你把这条规律做成监控就有了报警线:只要某一轮命中数掉回零,说明前缀在那一轮被破坏了,直接去 diff 那一轮和上一轮的 messages。
第一个动作:给会话一个稳定的路由标识
理解为什么要设这个标识,得先知道缓存存在哪。官方文档写得很直白:缓存条目是按服务器存的(per-server),所以同一段会话的请求如果落到不同机器上,前缀再一致也命中不了。x-grok-conv-id 这个 HTTP 请求头的作用就是把带相同会话 ID 的请求路由到同一台服务器。
在 Chat Completions 这一侧,它是个纯粹的请求头,用 OpenAI 兼容 SDK 时通过 extra_headers 传:
from openai import OpenAI
client = OpenAI(
api_key="YOUR_XAI_API_KEY",
base_url="https://api.x.ai/v1",
)
response = client.chat.completions.create(
model="grok-4.6",
messages=[
{"role": "system", "content": "You are Grok, a helpful and truthful AI assistant built by xAI."},
{"role": "user", "content": "What is prompt caching?"},
],
extra_headers={"x-grok-conv-id": "conv_abc123"},
)
print(response.usage.prompt_tokens_details.cached_tokens)
Responses API 走的是另一条路:不用请求头,改用请求体里的 prompt_cache_key 字段。官方文档明确说它的作用与 x-grok-conv-id 相同,都是把请求路由到同一台服务器以复用缓存。用 OpenAI SDK 时它是 xAI 的扩展字段,得塞进 extra_body;如果你用的是 Vercel AI SDK,对应的写法在 providerOptions.xai.promptCacheKey。
gRPC 这一侧是第三种形态:官方 xai_sdk 的 Client 构造函数接受 metadata 参数,把 ("x-grok-conv-id", "conv_abc123") 作为 gRPC metadata 传进去,同样是为了粘性路由。
至于这个 ID 取什么值,官方建议用 UUID 或者你应用自己的会话 ID。关键词是稳定——同一段对话从头到尾用同一个,不要每次请求现生成一个。反过来,官方也给了主动制造未命中的办法:换一个不同的会话 ID,或者干脆不带这个头,请求就可能被路由到别的服务器,那边没有你的缓存。调试时想强制走一次冷路径,用这个办法比清缓存靠谱。
第二个动作:静态内容前置,前缀只许追加
xAI 的缓存是从消息数组的开头开始匹配的。请求到达时,系统检查开头有多少条消息与之前的请求完全一致,这段一致的部分就是”前缀”,从缓存里取。文档给的例子很清楚:第二轮请求里前三条消息与第一轮完全相同,这三条就命中缓存,只有新追加的那条要重新计算。
这个机制直接决定了提示词的排版顺序。官方最佳实践里写的是”front-load static content”——把系统提示、few-shot 示例、参考文档这类固定不变的内容放在最前面,让它们构成一段稳定的前缀。反过来说,把随请求变化的东西(时间戳、随机 ID、用户当前状态摘要)混在系统提示里,等于每一轮都把前缀第一条就改掉,后面全部作废。
哪些操作会破坏前缀,文档单开了一页列举,一共三类,都配了对照的请求示例:
- 改动早先的消息内容:文档的例子是把一条 assistant 回复缩写成更短的一句,结果就是未命中。工程上最常见的对应场景是”回写前把模型输出做了个清洗”,你以为只是去掉了几个空格,前缀已经断了。
- 删除消息:从对话里移掉任意一条消息都会破坏前缀。做上下文裁剪、丢弃早期轮次以省 token 的逻辑,正好撞在这条上。
- 调换消息顺序:文档的例子是把 system 和 user 两条对调,同样未命中。有些框架在序列化时不保证顺序稳定,这种问题很隐蔽。
所以官方最佳实践第三条写得斩钉截铁:永远不要修改早先的消息,只追加新的。任何编辑、删除、重排都会破坏缓存。
如果你的产品确实需要”截断长对话”,那就得接受截断那一轮必然未命中,并把截断做成低频动作——而不是每轮都滑动窗口。多轮场景下的具体取舍,可以对照跨厂商的缓存计费通用机制一起看,思路是通的。
推理模型的专属坑:reasoning_content 必须回传
这是整份缓存文档里语气最重的一条。官方原文的措辞是:对于推理模型,你必须把上一轮响应里的 reasoning_content 一起带回去,遗漏它是缓存未命中的首要原因(top cause of cache misses)。
道理不难理解:推理模型上一轮的思考内容也是前缀的一部分,你只回传了最终答案而丢掉思考内容,前缀自然对不上。但这个坑之所以难发现,是因为绝大多数会话管理代码是照着非推理模型的范式写的——把 choices[0].message.content 追加进 messages 就完事了,压根没意识到还有别的东西要回传。
官方给了两条路:
- 回传加密的推理内容。在 Responses API 里传
include: ["reasoning.encrypted_content"],就能拿到由 xAI 加密的推理内容,再把它送回去为后续对话提供上下文。文档另有提示:用 Vercel AI SDK 时,只要没有指定store: false,加密推理内容会自动带上,不需要额外配置。 - 改用有状态的响应。用
previous_response_id让服务端自动接续对话,不用自己拼历史。
第二条对新项目更省事:既然状态在服务端,你也就没机会把前缀改坏。已有项目要改造的话,先确认自己用的是不是推理模型——grok-4.6 和 grok-4.5 支持 reasoning_effort 参数,文档还写明推理不能关闭。也就是说这类模型上你没有”绕开推理内容”这个选项。
命中率还是上不去,按这个顺序查
官方最佳实践第五条给了排查的起点:盯 cached_tokens,如果它一直是 0,先核对会话 ID 和消息顺序。展开成可执行的顺序,大致是这样:
第一步,确认标识真的发出去了。 抓一次实际出网的请求,看 x-grok-conv-id 是不是在头里、prompt_cache_key 是不是在体里。SDK 包了一层之后,参数写错位置不会报错,只会静默地什么都没传。这也是我建议把命中数打进日志的原因——没有埋点,这类问题可以潜伏很久。
第二步,diff 相邻两轮的 messages。 不是肉眼看,是把两轮的 messages 序列化后逐条比对哈希。经验上问题多半出在这几处:给消息补了 name 字段、内容里带了本轮才生成的时间戳、markdown 做了二次转义、历史被中间件重新排过序。
第三步,如果是推理模型,检查 reasoning_content 有没有回传。 见上一节。
第四步,接受一部分未命中是正常的。 文档在多处标注:提示缓存不保证 100% 命中,缓存条目可能因为内存压力被清退,请求也可能被路由到别的服务器;条目随时可能因为服务器负载或重启而失效。官方最佳实践第六条因此写着”优雅处理未命中”——你的应用必须在没有缓存的前提下也能正常工作。把命中率当成成本优化项,别把它当成功能依赖。
三个常被误解的点,文档里其实有答案
缓存会不会影响输出质量? 不会。文档的回答是缓存只加速提示处理阶段,无论提示是从缓存取还是从头算,模型输出是一样的。这一点值得写进团队约定,省得每次线上表现有波动就有人怀疑到缓存头上。
流式请求能不能用缓存? 可以,流式与非流式都支持。文档还补了一个细节:流里的第一个空 token 对应的就是缓存查找与预填充阶段。
带工具调用的会话呢? 也可以。文档说可缓存的前缀包含直到工具调用结果为止的所有消息,只要这段前缀不变,后续请求照样受益。这对 agent 类应用是好消息——工具返回值只要不做二次改写,就能一直待在前缀里。
哪些模型支持? 文档写的是提示缓存在所有 grok 语言模型上可用,具体哪些模型支持以及各自的缓存 token 定价,指向官方定价页。
命中之后,账单那一侧会怎么变
计费口径文档也说清楚了,但请注意这里不涉及任何具体价格:命中的部分按”缓存提示 token 价”计费,低于常规提示 token 价;未命中的提示 token、补全 token、推理 token 都按各自的常规价走——推理 token 是按补全 token 价计的,这一条容易被忽略。
还有一个容易漏的规则:文档说长上下文定价的触发条件是提示 token 总量(包含缓存部分)超过该模型的长上下文阈值。也就是说缓存命中并不会把你”拉回”短上下文档位,命中的 token 照样计入总量,只是它们各自适用对应的长上下文费率。指望靠高命中率跨回便宜档位的想法,在这条规则下不成立。
具体单价随时可能调整,一律以 xAI 官方定价页为准。想把每天的花销拆到具体功能上,做法可以参考通用的 API 成本监控思路,以及Grok API 的计费口径。
最后:最容易栽的坑
按踩中概率排序,第一名是推理模型没回传 reasoning_content——官方自己都点名了,但它不报错、不告警,只是账单悄悄变高。第二名是设了会话 ID 却每次请求都重新生成,看起来该做的都做了,实际上等于没设。第三名是把动态内容混进了系统提示。
有个反直觉的地方值得留意:缓存这件事上,“整理一下历史消息让它更干净”几乎总是错的。你以为在优化,实际上每次改写都在把前缀砍断。正确的姿势是把消息数组当成一份只能追加的日志——写进去就不许再动。
下一步建议做两件事:一是把三套 API 各自的 cached_tokens 字段接进你的可观测面板,按会话 ID 聚合命中率;二是写一个断言,在追加新消息前校验历史部分的哈希与上一轮一致,一旦不一致就打日志。这两件事加起来不到一天,但能把上面这几个坑全部变成”当场可见”。