提示词缓存
本指南通过 Grok API 演示提示词缓存如何降低长对话的费用,以及哪些细节会让缓存失效。在我们的测试中,仅仅把当前时间放到系统提示词开头,同一段对话的费用就变成原来的 2.2 至 2.9 倍。
每轮对话都会重新发送全部历史,因此每轮费用都会比上一轮高。如果提示词的开头与上一次相同,Grok 就能从提示词缓存读取相同部分,价格为原来的四分之一。此示例在三个聊天中同时重放相同的 30 条消息,并逐轮绘制费用图。它们都会告诉 Grok 当前时间。第一种将时间放在最新消息开头;第二种和许多应用一样,将时间放在系统提示词开头,仅这一行的变化就使后续整个提示词无法命中缓存;第三种做法和第一种相同,但会随对话增长而压缩历史。
你将学到什么
满足提示词缓存的条件:当前提示词的开头与上一次相同
哪些做法会破坏缓存(如将当前时间放在系统提示词开头),以及应将时间放在哪里
如何找到缓存 token 数量,查看
usage,并根据 token 数量计算每轮费用将每轮聊天发送至已缓存该聊天的服务器,使用
prompt_cache_key在轮次之间传递 Grok 的加密推理内容,使用
toInput(),让缓存也覆盖 Grok 先前的回答压缩聊天所用的方法
responses.compact(),以及压缩的费用和压缩后下一轮未命中缓存的原因从请求成功通过的那次尝试开始计时,使用 SDK 的
onRequest钩子
运行示例
需要 Node.js 22.13 或更高版本。将 API 密钥放入 .env(位于仓库根目录),或导出环境变量 XAI_API_KEY。
cd examples/prompt-caching/typescript
npm install
npm run web打开 http://localhost:3000,点击 Play the chat three ways。三段对话都以虚构的火星研究站 Ares Station 的乘员助手为对象。助手根据研究站手册作答,手册文件是 handbook.md。手册约有 2,100 个 token,随每次提示词一同发送,就像客服 Bot 随请求发送帮助文档一样。页面将 script.txt 中的 30 条消息同时在三个聊天中重放。顶部的曲线会对比前两个聊天均已完成的轮次。下方每个聊天会显示总费用、相对第一个聊天的费用、每轮的分段柱状图(缓存输入、新输入、输出和压缩),以及截至目前的对话与每轮提示词中缓存读取的比例。将鼠标悬停在柱子上,可查看该轮各项的 token 数和费用,以及命中或未命中缓存的原因。点击柱子,三个聊天都会滚动到该轮。Stop 可取消运行,完成后可通过 Results 下载包含各轮数据的 JSON 文件。
也可以从终端运行:
npm start每完成一轮对话或一次压缩就会打印结果,最后输出汇总,并将所有轮次保存至 output/comparison-<time>.json。传入数字可限制播放轮数,例如 npm start -- 10。编辑 handbook.md 和 script.txt 可换成你自己的手册和消息。
下表所示的运行共用时 3 分 13 秒,按标价计算花费 56 美分,其中将时间置于系统提示词开头的聊天花费 27 美分。
工作原理
发送每轮请求的代码位于 src/chat.ts:
sendMessage()流式获取一轮回答,使用模型grok-4.7。输入依次为最近的压缩项(如果有)、含手册的系统提示词、此后的各轮对话和新消息。第一和第三个聊天在最新消息开头放入Current time:和当前时间;第二个则放在系统提示词开头。时间每轮都会变化,放在系统提示词开头意味着整个提示词都是新的。放在最新消息开头时,它属于原本就是新增的内容。它将
prompt_cache_key设为chat:与聊天 ID 的拼接值,将每轮请求发送至已缓存该聊天的服务器。它采用较低的推理强度,几秒内就能回答手册相关问题。它将消息以及
response.toInput()加入聊天。toInput()包含 Grok 的推理项及其加密内容。缺少它们时,下一轮只能命中到 Grok 上一次回答开始前的位置,Grok 也会丢失先前回答的推理依据。SDK 的
Usage类型提供各项计数:usage.input_tokens、usage.input_tokens_details.cached_tokens(其中从缓存读取的部分),以及usage.output_tokens,后者包含usage.output_tokens_details.reasoning_tokens。API 也会在每个响应的usage.cost_usd中报告实际计费,但只提供总额;压缩甚至不会报告费用。因此,price()根据 token 数量逐项计算,并使用以下方法取得费率:client.models.language.get()。每百万输入 token 为 $2.00,每百万缓存 token 为 $0.50,每百万输出 token 为 $6.00。compactChat()将各轮聊天连同上次的压缩项发送给client.responses.compact(),再以它返回的单个加密项替代这些历史。compacted.usage会给出该次压缩的 token 用量和dropped_message_count。与普通响应不同,压缩的output_tokens不含推理 token。第三个聊天在距上次压缩的各轮 token 数超过COMPACT_AFTER(1,500 个 token)时再次压缩。只有这些轮次可以被进一步压缩。如果对整个提示词设限(如 4,500 个 token),当摘要大小接近剩余空间时,我们的聊天就会在每轮结束后都触发压缩。压缩项必须放在最前面,因为 API 会丢弃它之前的一切内容,包括
instructions。因此,系统提示词不参与压缩,紧跟在压缩项后面,无论对话压缩几次都能逐字保留手册。若与各轮消息一同压缩,系统提示词在首次压缩时仍会原样保留,但下次压缩就会和其他内容一起被概括。每个请求都会经过客户端的
onRequest钩子,记录每次尝试的开始时间,避免将因限流而重试的等待计入首个 token 延迟。由于三个聊天同时运行,客户端最多重试七次(默认两次)。SDK 每次重试前的等待时间逐次延长,最长达 30 秒,七次重试足以等到每分钟 token 限额恢复。客户端还设置了retryBeforeOutput,防止单个请求失败导致聊天中途停止。
src/compare.ts 在 CHATS 中定义三种聊天,通过 compare() 同时在三者中重放脚本并保存结果。src/server.ts 是一个小型 node:http 服务器。/api/compare 重放聊天,通过 SSE 将每一轮对话和每次压缩发给网页,页面关闭时会停止运行。服务器只提供其运行时写入的结果文件。public/index.html 用原生 HTML、CSS 和 JavaScript 显示这些事件。src/index.ts 则在终端中运行对比。
对比结果
三个聊天均发送相同的 30 条消息。以下是一次运行的结果:
| 聊天方式 | 费用 | 从缓存读取的输入比例 | 首个 token 延迟中位数 | 压缩次数 |
|---|---|---|---|---|
| 时间放在最新消息中 | $0.117 | 87% | 0.6 秒 | 0 |
| 时间放在系统提示词开头 | $0.270 | 3% | 0.7 秒 | 0 |
| 启用压缩 | $0.167 | 86% | 0.7 秒 | 3 |
时间放在开头的成本。这个聊天每次的开头都不同,每轮只能从缓存读取所有请求共有的 128 个前导 token,其余均按全价计费。随着聊天从 2,400 个 token 增至 5,900 个,每轮费用从 0.6 美分升至 1.2 美分,而第一种聊天大部分轮次为 0.2 至 0.5 美分。本次运行费用为第一种的 2.3 倍,多次运行的范围为 2.2 至 2.9 倍。每一轮都要重发所有历史,差距会随对话增长而扩大。
压缩的花费超过了省下的费用。三次压缩分别花费 1.4、1.7 和 3.3 美分,耗时 19、29 和 53 秒。压缩生成了 600 至 1,800 个摘要 token,外加最多 2,600 个推理 token,均按输出价计费,却仅从提示词中减少了 600 至 900 个 token。每次压缩后的下一轮,由于新摘要排在最前面,只能从缓存读取 128 个 token,费用约为普通轮次的三倍。本次运行的聊天费用为第一种的 1.4 倍。启用缓存时,每压缩掉 10,000 个 token,每轮可省半美分。因此,压缩适合每轮长度达到摘要多倍的长对话,也可用于避免超出上下文窗口。本示例将
COMPACT_AFTER设得较低,是为了让聊天能触发几次压缩。压缩可能丢失的信息。本次运行中,三个聊天都正确回答了最后的问题:9 号铺位、6 号航天服和三份含花生的晚餐。但在多次测试中,摘要长度从 15 到 2,350 个 token 不等,只有 15 个 token 的摘要就漏掉了用户的航天服编号。如果将系统提示词与各轮消息一起压缩,第二次压缩后的提示词比单独的系统提示词还短,Grok 只能根据手册摘要作答。若将系统提示词放在压缩项之前或放入
instructions,API 会把手册全部丢弃,Grok 就会编造菜单细节,例如实际不存在的晚餐。缓存以 128 个 token 为一块。所有缓存计数都是 128 的倍数,所以提示词末尾不足一块的 token 总是按新输入处理。
缓存键能提高命中率。API 繁忙时,第一种聊天除首轮外还有三轮未命中。我们在不设置
prompt_cache_key的情况下也进行了测试:无论有没有缓存键,API 都能缓存大部分轮次,缓存输入占比在 64% 到 95% 之间。不设置键时,请求可能到达一台没有缓存这段聊天的服务器;设置键可降低这种可能性。要自行尝试,可移除prompt_cache_key,所在文件是sendMessage()。首个 token 延迟变化很小。在 2,400 到 6,900 个 token 的范围内,读取提示词只占等待时间的一小部分。API 繁忙时,无论是否命中缓存,部分轮次最长都要等 80 秒才开始输出。