跳到正文

修剪旧结果

解决办法是四行。

这部分会让人觉得有些反高潮。你在上一节课中测量了题目,观察输入token攀升,在第三十步勾勒出灾难场景。现在我们加上四条线,曲线变平。

台词本身很简单。他们去哪里,为什么去,这就是教训。

学习成果

prepareCall在每个模型调用前运行pruneMessages,移除比前三条消息更早的工具调用/结果对。上一节课的token增长曲线趋于平稳,而不是永远攀升。

快速路径

  1. ai进口pruneMessages
  2. 给你的ToolLoopAgent配置添加一个prepareCall
  3. 里面,调用 pruneMessages({ messages, toolCalls: "before-last-3-messages" })
  4. ...options,并防止messages在第一调用

动手练习 5.2

将线材修剪到Agent中,重复第5.1课的多步骤任务。

要求:

  1. prepareCall: async (options) => ({...})添加到Agent配置中
  2. 展开...options,确保必填字段如modeltools得以延续
  3. 定义明确后有条件修剪options.messages
  4. 目前先用toolCalls: "before-last-3-messages"(最简单且合理的策略)
  5. 确认输入token台阶上停滞,而不是攀爬

实现提示:

  • prepareCall在每个模型调用前运行,完整请求options。你进来时还在修改消息
  • 先分散...options,否则会失去modeltoolssystem。修剪过的消息覆盖了传播
  • 在第一个调用还没有消息(prompt已设置,messages``undefined)。那就省略修剪了

修复方案

ts
import { ToolLoopAgent, stepCountIs, tool, pruneMessages } from "ai";

const agent = new ToolLoopAgent({
  // ... existing config
  prepareCall: async (options) => ({
    ...options,
    messages: options.messages
      ? pruneMessages({
          messages: options.messages,
          toolCalls: "before-last-3-messages",
        })
      : undefined,
  }),
});

四条线,一条进口。大部分代码是第一代调用的守卫。

到底发生了什么

每模型一调用前,prepareCall就运行。它接收了SDK即将发送的完整请求。我们用修剪过的版本替换messages,删除所有比最近三条消息更旧的工具调用和结果。

Before pruning at step 15:
  [user prompt]
  [assistant + tool_call] -> [tool_result]    (old, will be pruned)
  [assistant + tool_call] -> [tool_result]    (old, will be pruned)
  ... 12 more pairs ...
  [assistant + tool_call] -> [tool_result]    (recent, kept)
  [assistant + tool_call] -> [tool_result]    (recent, kept)
  [assistant] -> [user]                       (recent, kept)

After pruning:
  [user prompt]                               (kept, original prompt)
  [assistant + tool_call] -> [tool_result]    (recent)
  [assistant + tool_call] -> [tool_result]    (recent)
  [assistant] -> [user]                       (recent)

原始用户提示词总是存活。最近的工具互动得以保存。对话中段,工具结果堆积起来,每个调用都被抛到脑后。

**Warning:有两个值得说的陷阱loud**

Spread ...options first. prepareCall 收到完整的请求选项,包括modeltoolssystem。忘记了牌数会悄然掉落,Agent以令人困惑的方式断裂。

Guard messages. 在第一个调用,SDK给你一个prompt场,但没有messages阵列。调用 pruneMessages({ messages: undefined })投掷。三元系统句柄。

为什么要三条消息

toolCalls: "before-last-3-messages"设置保留了对话的最后三条消息,而不仅仅是最近三对工具。这对模型来说,足够让模型知道自己在多步骤任务中的位置,而无需保留全部历史。

你可以调音这个。before-last-1更激进,节省更多token。before-last-5更温和,也保留了更多上下文。三是个合理的默认配置,适用于各种任务形状。从这里开始。如果你有具体任务需要,可以稍后调整。

动手试试

运行第5.1课中的同样多步骤任务,并比较token曲线:

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

你应该看到类似这样的内容:

Step 0: 1,200 input, 450 output
Step 1: 2,800 input, 200 output
Step 2: 3,100 input, 180 output    (old results pruned)
Step 3: 3,400 input, 350 output    (growth plateaus)
Step 4: 3,200 input, 600 output    (stays flat)

具体数字取决于你的项目。形状才是关键。输入token第二或第三步时会停滞,而不是一直爬。

bash
npx tsc --noEmit

**注意:证明在于形状,而不是数字**

不要指望能得到和示例相同的数字。token数量取决于文件大小、模型选择以及提示词的具体措辞。需要验证的是曲线形状:之前是线性,之后是平台。

提交

bash
git add index.ts
git commit -m "feat(context): prune old tool results in prepareCall"

完成标准

  • [ ] pruneMessages是从ai进口的
  • [ ] prepareCall是有线连接到Agent配置里的
  • [ ] ...options先被传播,然后消息被覆盖
  • [ ] 处理未定义消息的情况
  • [ ] 在4+步任务中,输入token停滞而非线性增长
  • [ ] npx tsc --noEmit

**注意:找出你任务的修剪阈值**

默认before-last-3-messages是猜测。选择一个需要Agent记住几步前读过的内容(配置值、函数名、找到的TODO)的任务。用before-last-1before-last-3before-last-5运行,看看什么时候线会断线Agent。你Harness的正确数量取决于它的工作类型。

参考实现

ts
import { ToolLoopAgent, stepCountIs, tool, pruneMessages } from "ai";

const agent = new ToolLoopAgent({
  model: "anthropic/claude-haiku-4-5",
  instructions: buildSystemPrompt({ /* ... */ }),
  tools,
  stopWhen: stepCountIs(15),
  onStepFinish: ({ usage, stepNumber }) => {
    console.error(
      `Step ${stepNumber}: ${usage.inputTokens} input, ${usage.outputTokens} output`,
    );
  },
  prepareCall: async (options) => ({
    ...options,
    messages: options.messages
      ? pruneMessages({
          messages: options.messages,
          toolCalls: "before-last-3-messages",
        })
      : undefined,
  }),
});

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