从零构建 Coding Agent
English

03. 工具设计基础

工具是 Agent 接触外部世界的唯一通道。工具设计不好,模型就会学到错误行为;工具输出不稳定,loop 就难以测试;工具权限过大,安全边界会变成装饰。一个可用的 Coding Agent,工具层要同时服务模型、用户界面、日志和安全策略。

工具不是函数包装器

把本地函数直接暴露给模型通常会失败。普通函数的参数是给程序员看的,错误信息也是给程序员看的;Agent 工具的参数和错误要给模型消费。模型不擅长从堆栈里推断“下一步该怎么改”,但很擅长根据结构化、明确、短小的反馈修正调用。

一个工具定义应该回答四个问题:

  • 什么时候使用这个工具。
  • 参数如何表达。
  • 输出会包含什么,可能被怎样截断。
  • 失败时模型可以怎么修正。

例如 grep 的描述不要只写“搜索文本”。更好的描述是:在不知道文件位置时搜索工作区;查询应尽量具体;结果最多返回前 50 条;如果结果太多,请缩小关键词或限定目录。

参数校验

模型输出是 unknown。即使 provider 宣称会按 schema 返回参数,运行时也必须重新校验。因为流式 tool args 可能被截断,模型可能多给字段,旧会话恢复时可能带着旧 schema 的参数。

教学项目可以先手写校验:

type ReadInput = {
  path: string;
};

function parseReadInput(value: unknown): { ok: true; input: ReadInput } | { ok: false; message: string } {
  if (typeof value !== "object" || value === null) {
    return { ok: false, message: "Expected an object with a path field." };
  }
  const record = value as Record<string, unknown>;
  if (typeof record.path !== "string" || record.path.length === 0) {
    return { ok: false, message: "Expected path to be a non-empty string." };
  }
  return { ok: true, input: { path: record.path } };
}

生产系统可以使用 JSON Schema、Zod 或 Valibot,但原则一样:校验失败要产生 tool result,而不是让运行时崩溃。错误消息要可行动,例如“path 必须是相对路径”,而不是“validation failed”。

输出为模型而写

工具输出有两类消费者:模型和人。模型需要简洁、稳定、可继续推理的文本。人可能需要完整 diff、命令退出码、执行耗时、截断策略和可展开详情。不要把人类 UI 需要的所有结构塞进 tool result 文本里。

建议每个工具返回两层结果:

type ToolResult<Details> = {
  message: ToolResultMessage;
  details: Details;
};

message 进入 LLM 上下文,details 进入事件流、日志或 UI。edit 工具可以给模型一句“替换成功,修改了 2 行”,同时给 UI 一个结构化 diff。这样既省 token,又不会牺牲可观察性。

只读工具箱

在给 Agent 写权限之前,先实现只读工具箱:

  • read: 读取一个文本文件,支持行号范围和输出截断。
  • ls: 列出目录,区分文件、目录、符号链接和隐藏项。
  • grep: 搜索文本,限制结果数量,返回匹配行和路径。
  • find: 按名称查找文件,限制遍历范围和返回数量。

只读工具的目标不是功能全面,而是让模型建立“先观察再行动”的习惯。系统提示词也应该明确要求:编辑前必须读取目标文件;不知道路径时先用搜索;不要猜测文件内容。

运行观察

一个好的 read 结果应该像这样:

Read src/config.ts lines 1-42.
Output was truncated after 200 lines. Request a narrower range if needed.

1 export type Config = {
2   model: string;
3   maxTurns: number;
...

这里同时给了模型事实、边界和下一步建议。坏输出则是“文件太长”或直接贴满几万行。前者无法继续,后者浪费上下文。

生产化取舍

工具层至少要考虑这些策略:

  • 路径必须先解析到工作区边界内,不能让 .. 逃逸。
  • 文本读取要处理编码和二进制文件。
  • 长输出要截断,并明确告诉模型截断发生了。
  • 命令类工具要有超时和进程树清理。
  • 写文件工具要参与同一文件的写队列,避免并行覆盖。
  • 工具结果要带稳定 id,方便 UI 和日志关联。

这些策略听起来像细节,但它们决定 Agent 是“偶尔能演示”还是“能在真实仓库里使用”。

练习

完成 readlsgrep 三个只读工具。

验收标准:

  • 对不存在路径返回 isError: true,错误里包含可修正建议。
  • 对超长文件截断,并说明截断策略。
  • 对二进制文件拒绝读取,而不是把乱码塞进上下文。
  • 所有路径都必须解析在工作区内。
  • 用 faux provider 让模型先 grepread,验证两次工具调用能串起来。