跳到正文

工具输出设计

修剪会断章取义地使用旧结果。这是必要的。这还不够。

如果一根工具结果是5000token,修剪下一根并不能救你。伤害已经造成了。该模型已经输出了5000token的grep,现在还得维持至少三回合。

更好的解决办法是上游。工具应默认产生小而有结构且有界限的输出。修剪是清理团队。工具设计就是预防。

学习成果

你机束中的每个工具都有明确的输出上限(行数、匹配数、字符数),截断行为会反馈给模型,以便它需要分页时进行分页。

快速路径

  1. read最大500行,并带有偏移/限页码
  2. 上限grep50场比赛,返回总比赛数
  3. 输出上限bash5000字符,保留尾部
  4. 每个帽都会显示模型能看到并处理的截断消息

动手练习 5.3

对这三种工具应用有界输出契约。

要求:

  1. read保留了模块1的500 行上限,并设有offsetlimit参数用于分页
  2. grep保留了模块1的50场限制,截断时加上“(N total and show first 50)”后缀
  3. bash增加了stdout的5000字符上限。保留尾部(最后5000个角色),不要保留首部,因为错误通常留在结尾
  4. 每个截断都附加一个清晰的信息,比如"... (truncated, showing last 5000 chars)"

实现提示:

  • 截断信息是模型唯一表明数据更多的信息。它必须是可见的
  • 对于bash来说,切尾通常是正确的。构建输出、测试失败和栈跟踪通常放在最后。如果你的工具能执行对头很重要的命令,那就换个方式
  • “有界限”并不意味着“很小”。500条线,50根火柴,5000个角色。足够回答问题,又足够小以保持语境

帽表

工具为什么要这么做
read500行足够读懂大多数文件。要足够大以抓住结构,又要小到不会把模型埋没
grep50场比赛搜索结果返回了50条,回答了这个问题。五百个数据就是数据倾倒
bash5000条大多数指令输出都能合适。npm install和朋友们制造了模型不需要的噪音

这些数字并不神圣。他们通过执行真实任务并注意疼痛部位来调校。如果你的Harness持续执行输出较长的指令,那就提高上限。如果你主要做快速搜索,那就降低它。

带电容的 Bash 输出

bash工具之前没有输出上限。添加一个:

ts
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已经做到了,但值得重申这个模式:

ts
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可以决定缩小搜索范围或分页。

截断契约

所有能够产生无界输出的工具都应遵循相同的形状:

  1. 将输出限制在一个合理的上限
  2. 告诉模型,输出被截断了多少,删减了多少
  3. 在工具支持的分页参数上提供(read偏移/限位,grep``glob模式更窄)。

契约让Agent能够做出反应。一个无声截断的工具比完全没有截断更糟糕,因为模型认为自己掌握了全貌,却基于不完整的数据采取行动。

**注意:瓶盖是Agent在分页中缴纳的税**

有界输出会让某些任务稍微变慢一些。要读取一个2000行的文件,Agent现在需要四个read 调用而不是一个。这是正确的权衡。四次有界读取在成本和和 token上都比一次大量读取更便宜,后者会在整个会话中污染上下文。

动手试试

进行一个你知道会返回大量匹配的搜索:

bash
bun run index.ts . "Find all import statements in this project"

你应该会看到grep回了50个匹配,尾部会统计总匹配数。如果你让Agent继续,它应该缩小搜索范围或使用更具体的球状,而不是要求无界转储。

试试一个能输出大量输出的命令:

bash
bun run index.ts . "Run: ls -laR"

如果递归列表超过5000字符,你应该会看到截断信息。Agent应通过缩小列表范围或要求特定子目录来应对。

bash
npx tsc --noEmit

提交

bash
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调音。注意权衡:可配置电容意味着用户有更多旋钮可以设置错。正确的默认在哪里?

参考实现

ts
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;
    },
  });
}

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