工具输出设计
修剪会断章取义地使用旧结果。这是必要的。这还不够。
如果一根工具结果是5000token,修剪下一根并不能救你。伤害已经造成了。该模型已经输出了5000token的grep,现在还得维持至少三回合。
更好的解决办法是上游。工具应默认产生小而有结构且有界限的输出。修剪是清理团队。工具设计就是预防。
学习成果
你机束中的每个工具都有明确的输出上限(行数、匹配数、字符数),截断行为会反馈给模型,以便它需要分页时进行分页。
快速路径
read最大500行,并带有偏移/限页码- 上限
grep50场比赛,返回总比赛数 - 输出上限
bash5000字符,保留尾部 - 每个帽都会显示模型能看到并处理的截断消息
动手练习 5.3
对这三种工具应用有界输出契约。
要求:
read保留了模块1的500 行上限,并设有offset和limit参数用于分页grep保留了模块1的50场限制,截断时加上“(N total and show first 50)”后缀bash增加了stdout的5000字符上限。保留尾部(最后5000个角色),不要保留首部,因为错误通常留在结尾- 每个截断都附加一个清晰的信息,比如
"... (truncated, showing last 5000 chars)"
实现提示:
- 截断信息是模型唯一表明数据更多的信息。它必须是可见的
- 对于
bash来说,切尾通常是正确的。构建输出、测试失败和栈跟踪通常放在最后。如果你的工具能执行对头很重要的命令,那就换个方式 - “有界限”并不意味着“很小”。500条线,50根火柴,5000个角色。足够回答问题,又足够小以保持语境
帽表
| 工具 | 帽 | 为什么要这么做 |
|---|---|---|
read | 500行 | 足够读懂大多数文件。要足够大以抓住结构,又要小到不会把模型埋没 |
grep | 50场比赛 | 搜索结果返回了50条,回答了这个问题。五百个数据就是数据倾倒 |
bash | 5000条 | 大多数指令输出都能合适。npm install和朋友们制造了模型不需要的噪音 |
这些数字并不神圣。他们通过执行真实任务并注意疼痛部位来调校。如果你的Harness持续执行输出较长的指令,那就提高上限。如果你主要做快速搜索,那就降低它。
带电容的 Bash 输出
bash工具之前没有输出上限。添加一个:
const MAX_BASH_CHARS = 5000;
const stdout = result.stdout || "(no output)";
const cappedStdout =
stdout.length > MAX_BASH_CHARS
? stdout.slice(-MAX_BASH_CHARS) +
`\n... (truncated, showing last ${MAX_BASH_CHARS} chars)`
: stdout;
return cappedStdout;从末端切片是有意为之。Agent执行的大多数命令最后都会大声失败。失败的测试会最后打印失败的部分。失败的构建会最后打印错误。保留尾巴可以保留Agent需要行动的部分。
结构化回报,而非原始数据倾销
grep工具从模块1已经做到了,但值得重申这个模式:
const lines = stdout.trim().split("\n").filter(Boolean);
const MAX_MATCHES = 50;
const truncated = lines.length > MAX_MATCHES;
const result = truncated ? lines.slice(0, MAX_MATCHES) : lines;
return truncated
? result.join("\n") + `\n... (${lines.length} total, showing first ${MAX_MATCHES})`
: result.join("\n") || "No matches found.";截断消息给模型提供了两个可作用的信息:结果比显示的多,以及具体数量。这样,Agent可以决定缩小搜索范围或分页。
截断契约
所有能够产生无界输出的工具都应遵循相同的形状:
- 将输出限制在一个合理的上限
- 告诉模型,输出被截断了多少,删减了多少
- 在工具支持的分页参数上提供(
read偏移/限位,grep``glob模式更窄)。
契约让Agent能够做出反应。一个无声截断的工具比完全没有截断更糟糕,因为模型认为自己掌握了全貌,却基于不完整的数据采取行动。
**注意:瓶盖是Agent在分页中缴纳的税**
有界输出会让某些任务稍微变慢一些。要读取一个2000行的文件,Agent现在需要四个read 调用而不是一个。这是正确的权衡。四次有界读取在成本和和 token上都比一次大量读取更便宜,后者会在整个会话中污染上下文。
动手试试
进行一个你知道会返回大量匹配的搜索:
bun run index.ts . "Find all import statements in this project"你应该会看到grep回了50个匹配,尾部会统计总匹配数。如果你让Agent继续,它应该缩小搜索范围或使用更具体的球状,而不是要求无界转储。
试试一个能输出大量输出的命令:
bun run index.ts . "Run: ls -laR"如果递归列表超过5000字符,你应该会看到截断信息。Agent应通过缩小列表范围或要求特定子目录来应对。
npx tsc --noEmit提交
git add src/tools.ts
git commit -m "feat(tools): cap bash output at 5000 chars with tail-keep"完成标准
- [ ]
read最多500行,带有偏移/限位分码 - [ ]
grep最多匹配50个,截断时加上(N total)后缀 - [ ]
bashstdout最多5000字符,保留尾部 - [ ] 每个帽都会显示模型能识别的清晰截断信息
- [ ] 没有工具能将无界数据直接导入上下文
- [ ]
npx tsc --noEmit
**注意:让电容可配置**
硬编码的上限是一个起点。一个快速检查的子 Agent可能需要100行,而不是500行。深入分析可能需要2000美元。重构你的工具工厂,让它接受caps配置对象。现在来电者可以按每个Agent调音。注意权衡:可配置电容意味着用户有更多旋钮可以设置错。正确的默认在哪里?
参考实现
export function createBashTool(
sandbox: Sandbox,
needsApproval: (input: { command: string }) => boolean,
) {
const MAX_BASH_CHARS = 5000;
return tool({
description: `Execute a shell command in the working directory.
WHEN TO USE: build commands, package install, tests, git, directory listings.
WHEN NOT TO USE: reading file contents (use read).
DO NOT USE FOR: reading files (use read), searching code (use grep).`,
inputSchema: z.object({
command: z.string().describe("Shell command to execute"),
}),
execute: async ({ command }) => {
if (needsApproval({ command })) {
return `Blocked: "${command}" requires approval.`;
}
const result = await sandbox.exec(command);
const stdout = result.stdout || "(no output)";
return stdout.length > MAX_BASH_CHARS
? stdout.slice(-MAX_BASH_CHARS) +
`\n... (truncated, showing last ${MAX_BASH_CHARS} chars)`
: stdout;
},
});
}