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 是“偶尔能演示”还是“能在真实仓库里使用”。
练习
完成 read、ls、grep 三个只读工具。
验收标准:
- 对不存在路径返回
isError: true,错误里包含可修正建议。 - 对超长文件截断,并说明截断策略。
- 对二进制文件拒绝读取,而不是把乱码塞进上下文。
- 所有路径都必须解析在工作区内。
- 用 faux provider 让模型先
grep后read,验证两次工具调用能串起来。