Chapter 03 · 常用工具集

演进 · loop + 真实工具12 个测试动手 chapters/03/tools.ts

目标:实现一套真实、可复用的工具(read/write/edit/glob/grep/bash),掌握三条决定"能用 vs 玩具"的工程不变量;把它们插进 Day 1 的 loop,得到一个能真正读写代码、跑命令的 coding agent 雏形。

Day 1 的工具是"返回假字符串的函数"。今天让它们真的动文件、跑命令——但真实副作用不可逆,安全和可靠比"能读能写"重要得多


一、工具就是函数,但要设计

一个工具 = 一个"名字 → 函数"。难的不是写函数,是设计:哪些动作该做成专用工具,哪些交给通用 bash?

主流经验(Anthropic《Building effective agents》):

  • bash 给模型最大杠杆(几乎能干任何事),但你的 harness 只拿到一个不透明的命令字符串——难以拦截、审批、并行判断
  • 把动作升级成专用工具(read/write/edit…),harness 就拿到带类型的参数,能:①按安全边界拦截/审批(删文件、发请求要不要确认)②做过期检查(edit 前文件变了没)③并行调度(只读的 grep 可并行,git push 不行)④渲染 UI
  • 经验法则:先用 bash 求广度,需要"拦截/审批/并行/渲染"时再升级成专用工具。

二、三条工程不变量(本章的真干货)

① workspace 边界(guardrail,不是 OS sandbox) 所有路径必须落在 workspace root 内,挡住 ../ 逃逸。这是教学护栏,不是操作系统级隔离——它防手滑/防模型改到不该改的地方,但不能防真正的恶意代码(那需要 OS sandbox)。诚实地知道边界在哪,是工程能力的一部分。

② 原子写(temp + rename) 写文件要么全成功、要么磁盘不变,绝不留半个文件。做法:写到临时文件,再 rename 覆盖目标。为什么?进程可能在写一半时崩;rename 在同一文件系统上是原子的,读者永远看到"旧的完整"或"新的完整",绝不会看到"写了一半"。

③ 精确 edit(恰好命中一次) 按精确字符串替换,且必须恰好命中一次:0 次(改错文件/内容过期)和多次(有歧义、会改错地方)都要报错。这是"模型看着旧内容提议改动"时,防止改错、防止基于过期内容改动的关键防线。

三、bash 的缰绳

bash 最有用也最危险,至少要三根缰绳:超时(到点杀进程)、输出截断(别让海量输出撑爆上下文)、受控 cwd(在 workspace 里跑)。工具"跑飞"和模型"跑飞"一样要防。


四、工具还要对模型"好用"(2026:工具设计即 prompt engineering)

§二/§三 让工具对你的系统安全可靠。但工具还有另一个用户:模型——它只透过工具的 名字 + 描述 + 参数 schema 认识这个工具(函数体它看不见)。所以工具的描述和签名本身就是 prompt engineering——Anthropic《Writing effective tools for agents》把它列为提升工具最有效的手段之一。四条落地:

  • 描述即 prompt:name / description / 参数名要说清"干嘛、什么时候用、参数什么意思"。描述被加载进模型上下文,直接左右它调得对不对——含糊的描述 = 模型乱调
  • 返回"给人看的信息",不是原始 ID / 技术字段:模型读 owner: "张三" 比读 owner_id: "u_8f3a" 推理得好。返回值也是上下文,要为模型的推理服务。
  • token 效率是硬约束:工具结果吃的是模型的上下文预算。会吐大量内容的工具(grep / read / glob)要内建分页 / 范围 / 过滤 / 截断 + 合理默认值,否则一次调用就把上下文塞爆、把注意力冲淡(呼应 §三 bash 的输出截断,以及 Day 5 的上下文预算)。参照:Claude Code 默认把单次工具结果截到 25,000 token
  • 要高杠杆,别做薄包装:一个工具应有意义地扩展能力,而不是把某个 API 原样裹一层。粒度对了(§一),模型才不会在一堆近义工具里犯选择困难;必要时给工具命名空间划清边界。

放到行业里:MCP(Model Context Protocol)。 你这章把工具收进本地 ToolRegistry——这是单个 agent 内的工具形态。跨 agent / 跨宿主怎么共享同一批工具?2024 年 11 月 Anthropic 开放了 MCP:一个厂商中立的开放协议,把"模型 ↔ 工具 / 资源"标准化成 client-server 接口(常被称作"AI 的 USB-C")。到 2026 它已是事实标准——OpenAI、Google、微软都接了,2025 年 12 月捐给 Linux 基金会下的 Agentic AI Foundation 做中立治理,Cursor / Claude Code 都能直接挂 MCP server。本质没变:MCP 传的还是"名字 → 带类型参数 → 结果"这套你已经手写过的东西,只是把它标准化、可跨进程共享。你先手写过 ToolRegistry,才看得懂 MCP 到底在标准化什么。

五、你要建的(练习)

打开 tools.ts,实现 7 个工具:

Lab 工具 关键不变量
3.1 resolveInWorkspace 边界:挡 ../ 逃逸(其他工具都先过它)
3.2 read 过边界读文件
3.3 write 原子写(temp+rename)
3.4 edit 精确替换、恰好一次
3.5 glob 递归 + 后缀过滤 + 返回相对路径
3.6 grep 递归逐行正则,返回 路径+行号+行
3.7 bash 超时杀进程 + 输出截断
npm run test:03   # 12 个测试,跑在真实临时目录上(测完自动清理)

resolveInWorkspace 起(其他工具依赖它),让红色牵着走。卡住按 定位→签名→伪代码→局部 找 tutor。

六、插进 loop:coding agent 雏形

把每个工具包成 Day 1 的 Tool(args → 字符串结果)放进 ToolRegistry,再喂给 Day 1 的 runAgent——你就有了一个能真正读代码、改代码、跑测试的 coding agent 雏形。工具失败(文件不存在、命令非零退出)按 Day 1 的约定归一成 error 结果喂回模型,而不是炸掉 loop。

这一步把前三天串起来了:Day 1 的 loop + Day 2 的真实模型 + Day 3 的真实工具 = 一个真能干活的 agent。


七、收尾

  • 讲回来(JOURNAL.md):为什么写文件要 temp+rename?为什么 edit 命中多次要报错而不是替换第一个?"workspace 边界是 guardrail 不是 sandbox"——这句话的边界在哪?工具设计的两个轴(对系统安全可靠 / 对模型好用)各包含哪些做法?你手写的 ToolRegistry 和 MCP 是什么关系?
  • 迁移题:见 TRANSFER.md——完整 glob 语法、软链接逃逸(realpath 检查)、bash 取消(AbortSignal)、把工具包成 ToolRegistry 接进 loop。
  • 真检验:用你的工具集 + Day 1 loop + 一个(假或真)模型,让 agent 真的读一个文件并改一行,测试通过。