跳到正文

缓存控制

修剪解决了问题的一半。另一半原因是每个调用仍然会传递那些未变的上下文部分。

你在十调用系统提示词和一时调用是一样的。早期用户提示词也是如此。工具定义是一样的。供应商以前见过这些token,你得付钱再寄一次。

缓存控制是医生表达“我记得这件事,不要再处理”的方式。在支持它的地方,成本下降是真实的。即使没有,这种模式仍然教会了一个有用的习惯:将稳定的情境与新的情境分开。

学习成果

一个prepareCall流水线修剪旧工具结果,然后将剩余上下文中稳定部分标记为可缓存。支持缓存控制的供应商,重复投入成本大幅下降。

快速路径

  1. 添加一个addCacheControl(messages)助手,标记稳定消息可缓存
  2. prepareCall后再写pruneMessages
  3. 将系统消息和早期对话标记为可缓存;保持最新消息为新鲜

动手练习 5.4

在修剪台阶后面缓存控制铁丝网。

要求:

  1. 写入 addCacheControl(messages)返回一个新的消息数组,并在适当情况下返回 providerOptions.cacheControl 设置
  2. 第一个消息(系统或初始用户提示词)应始终可缓存
  3. 比前两条更早的消息应可缓存
  4. 最近的一两条消息应该保持不缓存
  5. 更新prepareCall给调用 addCacheControl(pruneMessages(...))

实现提示:

  • cacheControl: { type: "ephemeral" }是带有拟人味的形状。其他供应商使用不同的密钥。如果你遇到不支持缓存控制的,调用依然能用,只是首部被忽略了
  • 缓存断点按前缀作用。将消息5标记为可缓存,缓存了包括消息5在内的所有内容。提供者在下一调用检查前缀
  • 不要把最近消息标记为可缓存。它们快要被替换了

辅助函数

ts
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变成了一条管道:

ts
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计数:

bash
bun run index.ts . "Read package.json, tsconfig, index.ts, then summarize"

如果你的服务提供者在使用对象中返回缓存命中信息,请记录:

ts
onStepFinish: ({ usage, stepNumber }) => {
  console.error(
    `Step ${stepNumber}: ${usage.inputTokens} input, ${usage.outputTokens} output, ${usage.cachedInputTokens ?? 0} cached`,
  );
},

你应该能看到cachedInputTokens从第一步开始增长,inputTokens(你付全价买的部分)保持小规模。

bash
npx tsc --noEmit

提交

bash
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和运行成本。会话结束时,打印总成本以及如果不剪枝和缓存会话的成本。数字决定了这门学科的价值。

参考实现

ts
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;
  });
}
ts
prepareCall: async (options) => {
  const pruned = options.messages
    ? pruneMessages({
        messages: options.messages,
        toolCalls: "before-last-3-messages",
      })
    : undefined;

  return {
    ...options,
    messages: pruned ? addCacheControl(pruned) : undefined,
  };
},

非官方简体中文翻译 · 原课程来自 Vercel Academy