第 06 章 · 精确补丁与 Diff
本章目标:理解“模型会写代码”和“Agent 能安全修改现有代码”之间,隔着定位、冲突检查、差异审查和回滚四层工程。
1. 为什么整文件重写很危险
write_file 很适合创建新文件,却不适合频繁修改已有文件。模型可能只关注目标函数,却在重写时改变换行、格式、注释和用户未提交的修改。文件越大,重复传输越浪费上下文,也越容易出现截断。
精确补丁表达的是“在已知旧内容的前提下,把这一小块替换成新内容”。它天然带有乐观并发检查:如果旧内容已经不匹配,补丁应失败并要求重新读取,而不是强行覆盖。
2. Patch 的运行时契约
一个补丁工具通常需要目标文件、上下文行和新增删除块。Executor 执行前检查路径和当前内容,执行后返回哪些 hunk 成功、文件是否变化以及简短 diff。模型下一轮看到的是结构化结果,不必猜测写入是否生效。
def total(items):
- return sum(item.price for item in items)
+ return sum(item.price * item.quantity for item in items)
这段差异比重发整个文件更容易让人类审查,也容易让 Reviewer 聚焦行为变化。
3. Diff 是 Agent 与人类的共同语言
模型内部可能有很长推理,但最终最可靠的审查对象是 diff:哪些文件变了、每一行怎样变、是否出现意外文件。Runtime 可以在每次写入后更新 working diff,在结束前强制模型重新阅读最终 diff。
审查 diff 时至少检查:
- 修改是否落在任务范围;
- 是否覆盖用户原有变化;
- 是否夹带格式化或生成文件噪声;
- 实现和测试是否同步;
- 是否出现秘密、调试输出或临时代码;
- 删除量是否异常。
Git diff 只展示文本变化,不证明行为正确,所以它属于验证闭环的一环,而不是最终证据。
4. 冲突为什么是有价值的失败
补丁上下文不匹配时,最安全的结果是失败。它说明模型依据的文件版本已经过期,可能是用户或另一个 Agent 修改了同一位置。Runtime 应返回当前位置附近内容,让模型重新规划。
不要在补丁失败后自动退化为整文件覆盖。这会把并发冲突隐藏成“成功”。生产系统可以用文件版本、哈希或 mtime 辅助判断,从而明确告诉模型:“读取后的文件已经变化。”
5. 回滚与分组
窄任务可以依靠 Git 恢复,但 Agent 不应随意回滚整个工作树。更好的方式是记录本轮触及的文件和 hunk,仅撤销自己产生的变化。对于大任务,可以按语义分组:实现、测试、文档分别形成清晰提交或变更集。
回滚不是失败的羞耻按钮,而是探索式修改的正常工具。模型可以尝试一个方案,验证失败后撤销自己的补丁,再尝试另一条路线。前提是系统能区分“自己的变化”和“用户已有变化”。
6. 代码落点
在最小 Agent 中新增 apply_patch 后,TOOLS 定义描述参数;Executor 实现匹配和写入;history 记录补丁摘要;验证器读取最终 diff。四处都要改,说明工具不是一个孤立函数,而是一条端到端协议。
失败案例
Agent 修改成功后运行了格式化器,格式化器改动了整个仓库。虽然测试通过,最终 diff 却远超任务范围。没有结束前 diff 审查,系统会把噪声当成成果交付。