修剪旧结果
解决办法是四行。
这部分会让人觉得有些反高潮。你在上一节课中测量了题目,观察输入token攀升,在第三十步勾勒出灾难场景。现在我们加上四条线,曲线变平。
台词本身很简单。他们去哪里,为什么去,这就是教训。
学习成果
prepareCall在每个模型调用前运行pruneMessages,移除比前三条消息更早的工具调用/结果对。上一节课的token增长曲线趋于平稳,而不是永远攀升。
快速路径
- 从
ai进口pruneMessages - 给你的
ToolLoopAgent配置添加一个prepareCall - 里面,调用
pruneMessages({ messages, toolCalls: "before-last-3-messages" }) - 先
...options,并防止messages在第一调用
动手练习 5.2
将线材修剪到Agent中,重复第5.1课的多步骤任务。
要求:
- 把
prepareCall: async (options) => ({...})添加到Agent配置中 - 展开
...options,确保必填字段如model和tools得以延续 - 定义明确后有条件修剪
options.messages - 目前先用
toolCalls: "before-last-3-messages"(最简单且合理的策略) - 确认输入token台阶上停滞,而不是攀爬
实现提示:
prepareCall在每个模型调用前运行,完整请求options。你进来时还在修改消息- 先分散
...options,否则会失去model、tools和system。修剪过的消息覆盖了传播 - 在第一个调用还没有消息(
prompt已设置,messages``undefined)。那就省略修剪了
修复方案
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 收到完整的请求选项,包括model、tools和system。忘记了牌数会悄然掉落,Agent以令人困惑的方式断裂。
Guard messages. 在第一个调用,SDK给你一个prompt场,但没有messages阵列。调用 pruneMessages({ messages: undefined })投掷。三元系统句柄。
为什么要三条消息
toolCalls: "before-last-3-messages"设置保留了对话的最后三条消息,而不仅仅是最近三对工具。这对模型来说,足够让模型知道自己在多步骤任务中的位置,而无需保留全部历史。
你可以调音这个。before-last-1更激进,节省更多token。before-last-5更温和,也保留了更多上下文。三是个合理的默认配置,适用于各种任务形状。从这里开始。如果你有具体任务需要,可以稍后调整。
动手试试
运行第5.1课中的同样多步骤任务,并比较token曲线:
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第二或第三步时会停滞,而不是一直爬。
npx tsc --noEmit**注意:证明在于形状,而不是数字**
不要指望能得到和示例相同的数字。token数量取决于文件大小、模型选择以及提示词的具体措辞。需要验证的是曲线形状:之前是线性,之后是平台。
提交
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-1、before-last-3和before-last-5运行,看看什么时候线会断线Agent。你Harness的正确数量取决于它的工作类型。
参考实现
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,
}),
});