缓存控制
修剪解决了问题的一半。另一半原因是每个调用仍然会传递那些未变的上下文部分。
你在十调用系统提示词和一时调用是一样的。早期用户提示词也是如此。工具定义是一样的。供应商以前见过这些token,你得付钱再寄一次。
缓存控制是医生表达“我记得这件事,不要再处理”的方式。在支持它的地方,成本下降是真实的。即使没有,这种模式仍然教会了一个有用的习惯:将稳定的情境与新的情境分开。
学习成果
一个prepareCall流水线修剪旧工具结果,然后将剩余上下文中稳定部分标记为可缓存。支持缓存控制的供应商,重复投入成本大幅下降。
快速路径
- 添加一个
addCacheControl(messages)助手,标记稳定消息可缓存 - 在
prepareCall后再写pruneMessages - 将系统消息和早期对话标记为可缓存;保持最新消息为新鲜
动手练习 5.4
在修剪台阶后面缓存控制铁丝网。
要求:
- 写入
addCacheControl(messages)返回一个新的消息数组,并在适当情况下返回providerOptions.cacheControl设置 - 第一个消息(系统或初始用户提示词)应始终可缓存
- 比前两条更早的消息应可缓存
- 最近的一两条消息应该保持不缓存
- 更新
prepareCall给调用addCacheControl(pruneMessages(...))
实现提示:
cacheControl: { type: "ephemeral" }是带有拟人味的形状。其他供应商使用不同的密钥。如果你遇到不支持缓存控制的,调用依然能用,只是首部被忽略了- 缓存断点按前缀作用。将消息5标记为可缓存,缓存了包括消息5在内的所有内容。提供者在下一调用检查前缀
- 不要把最近消息标记为可缓存。它们快要被替换了
辅助函数
import type { ModelMessage } from "ai";
export function addCacheControl(messages: ModelMessage[]): ModelMessage[] {
return messages.map((msg, i) => {
if (i === 0) {
return {
...msg,
providerOptions: { cacheControl: { type: "ephemeral" } },
};
}
if (i < messages.length - 2) {
return {
...msg,
providerOptions: { cacheControl: { type: "ephemeral" } },
};
}
return msg;
});
}第一条消息和除了最后两条外的其他都显示缓存标记。最近的更新会保持不在,这样就不会在下一个调用替换它们之前被缓存。
用修剪来作曲
在index.ts中,prepareCall变成了一条管道:
import { addCacheControl } from "./src/cache";
prepareCall: async (options) => {
const pruned = options.messages
? pruneMessages({
messages: options.messages,
toolCalls: "before-last-3-messages",
})
: undefined;
return {
...options,
messages: pruned ? addCacheControl(pruned) : undefined,
};
},修剪是因为它改变了消息数量。缓存会在存活的消息上进行。
节省的具体情况
这些数字取决于提供者、缓存命中率以及上下文的稳定性程度。对于典型的人类支持Agent进行长时间会话:
| 组成部分 | 无缓存的成本 | 含缓存的成本 |
|---|---|---|
| 50调用,每人20万输入 | 每一个调用全额输入 | 缓存的稳定前缀 |
| 1000万token全价 | ~每次30美元 | ~每次6美元 |
这对长时间训练来说是一个数量级的波动。短会话时,差额较小,因为缓存没有时间摊销。自律依然适用。
**注意:缓存控制是针对医疗服务提供者的**
Anthropic 在消息部分使用cacheControl。OpenAI使用不同的头部和不同的模型。有些服务商根本不在提示词层面暴露缓存。这种模式(稳定型和新鲜型分开)能在这些差异下存活下来。但providerOptions形状的确切形状则不然。
为什么即使在不支持缓存的情况下,模式也很重要
你强迫自己思考提示词哪些部分是稳定的。这是个有用的习惯。它告诉你你的系统提示词是每调用做的工作太多(每次重建)还是刚好(只做一次,重复使用)。
即使缓存明天消失,识别稳定上下文的纪律依然值得保留。稳定上下文也更容易测试、更容易版本化,也比每调用变化的上下文更容易推理。
动手试试
运行一个多步骤任务,观察token计数:
bun run index.ts . "Read package.json, tsconfig, index.ts, then summarize"如果你的服务提供者在使用对象中返回缓存命中信息,请记录:
onStepFinish: ({ usage, stepNumber }) => {
console.error(
`Step ${stepNumber}: ${usage.inputTokens} input, ${usage.outputTokens} output, ${usage.cachedInputTokens ?? 0} cached`,
);
},你应该能看到cachedInputTokens从第一步开始增长,inputTokens(你付全价买的部分)保持小规模。
npx tsc --noEmit提交
git add src/cache.ts index.ts
git commit -m "feat(context): add cache control to stable messages"完成标准
- [ ]
addCacheControl(messages)表示稳定消息可缓存 - [ ]
prepareCall先修剪再缓存,顺序就是这样 - [ ] 最新的消息未缓存
- [ ] 在支持缓存的提供商中,使用
cachedInputTokens显示 - [ ]
npx tsc --noEmit
**注意:构建一个token预算的仪表盘**
你有5.1课的遥测,5.2的剪枝,5.3的caps,还有本课的缓存。把它们接在一起。每一步后,记录输入token(未缓存)、缓存token、输出token和运行成本。会话结束时,打印总成本以及如果不剪枝和缓存会话的成本。数字决定了这门学科的价值。
参考实现
import type { ModelMessage } from "ai";
export function addCacheControl(messages: ModelMessage[]): ModelMessage[] {
return messages.map((msg, i) => {
if (i === 0) {
return {
...msg,
providerOptions: { cacheControl: { type: "ephemeral" } },
};
}
if (i < messages.length - 2) {
return {
...msg,
providerOptions: { cacheControl: { type: "ephemeral" } },
};
}
return msg;
});
}prepareCall: async (options) => {
const pruned = options.messages
? pruneMessages({
messages: options.messages,
toolCalls: "before-last-3-messages",
})
: undefined;
return {
...options,
messages: pruned ? addCacheControl(pruned) : undefined,
};
},