10. Coding 工具:read、edit、write、bash
Coding Agent 和普通工具 Agent 的差异,集中体现在写操作。读文件和搜索让模型观察世界;edit、write、bash 让模型改变世界并验证结果。这里的风险也最高:误改文件、覆盖用户改动、执行危险命令、输出过长、测试进程挂住,都会在真实仓库里发生。
先读再改
系统提示词和工具运行时都应该强化一个规则:编辑前必须读取目标文件。模型如果直接调用 edit 修改未读文件,运行时可以拒绝并返回错误 tool result:
Cannot edit src/config.ts because it has not been read in this session. Read the file first, then retry with exact oldText.
这个错误不是保守主义。edit 的可靠性依赖精确上下文。模型没读文件时,通常会猜测代码形状,导致替换失败或误改。
Edit 不是“让模型给 patch”
教学项目推荐先实现精确替换型 edit:
type EditInput = {
path: string;
oldText: string;
newText: string;
};
运行时读取当前文件,检查 oldText 是否出现且只出现一次,然后替换为 newText。如果没有出现,返回可修正错误;如果出现多次,也返回错误并要求模型提供更多上下文。这样做比让模型直接输出 patch 更容易校验,也更容易让模型自我修正。
失败反馈要具体:
oldText was not found in src/parser.ts.
The file currently contains a similar line:
return parse(input, options);
Read the latest file contents and retry with exact oldText.
模型通常能根据这类反馈重新 read,然后发出正确 edit。
Write 的边界
write 用于创建新文件或整体替换文件。它比 edit 危险,因为它不要求 oldText 匹配。建议策略:
- 创建新文件可以直接允许,但路径仍要在工作区内。
- 覆盖已有文件前要求文件已读,或要求用户确认。
- 对大文件写入给 UI 显示 diff。
- 写入后把修改摘要放入 tool result。
不要用 write 修小改动。小改动用 edit,降低覆盖风险。
Bash 的边界
bash 是最强也最危险的工具。最小实现也要有:
- cwd 限定。
- 超时。
- stdout/stderr 截断。
- exit code。
- AbortSignal 取消。
- 危险命令确认或拒绝。
命令输出给模型时要保留命令、退出码、截断说明和关键输出:
Command: npm run check
Exit code: 1
Output was truncated to the last 200 lines.
src/index.ts:42:10 - error TS2322 ...
如果输出很长,保留尾部通常比保留中间有用,因为测试和编译错误多在尾部总结。
文件写队列
模型可能在一个 turn 中并行调用两个写工具。即使你的 loop 串行执行,也要为未来并行留下边界。对同一个真实路径,写操作必须进入同一队列:
edit src/a.ts -> waits for previous write to src/a.ts
write src/a.ts -> same queue
edit src/b.ts -> can run independently
队列 key 应该是解析后的真实路径。符号链接、相对路径和大小写差异都可能指向同一个文件。没有写队列时,两个工具会基于旧内容计算修改,最后一个落盘的覆盖前一个。
Diff 给人,摘要给模型
模型不需要完整 unified diff 才能继续,用户界面需要。edit 结果可以分两层:
- 给模型:修改成功,文件、行数、下一步建议。
- 给 UI:结构化 diff、旧内容、新内容、是否创建文件。
如果把完整 diff 都塞进上下文,Agent 很快会耗尽 token。模型真正需要的是“修改已完成”和“测试下一步应该跑什么”。
练习
实现 edit、write、bash。
验收标准:
edit要求oldText精确出现一次。edit失败返回可修正 tool result,不抛出未捕获异常。write覆盖已有文件时有明确策略。bash有超时、截断、exit code 和取消。- 同一文件的写操作不会并行覆盖。
- UI 事件能拿到 diff details,模型上下文只拿到短摘要。