Grok 缓存前缀与 cache key

2026-11-04 16:55 更新

缓存这件事最容易被误判。团队看到官方有缓存价,就在代码里把 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。静态内容前置、应用不依赖缓存、缓存不影响输出质量等口径来自该页。
  • 缓存单价按模型而异,具体数值以官方定价页与控制台当前显示为准。本节不承诺任何具体命中率。

七、动手试

  1. 给 diagnose() 加一个「重排」用例:把 system 与 user 顺序对调,确认它能判出前缀在第 1 条断开,并解释为什么这类改动在多轮里最难发现。
  2. 把 prefix_len() 扩展到工具调用消息:当一条 assistant 消息里带 tool_calls、下一条是 tool 结果时,验证「只追加」是否仍然成立。
  3. 设计一个前缀结构:把 system、few-shot、检索文档、对话历史四段排好序,写出你会固定哪几段、哪一段每轮会变,并说明这样排的理由。
以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号