先破一个误区:模型根本不知道自己改了文件
你让 Agent 改了 12 个文件。改完运行,挂了。你想回到改动之前,但你没开 git。唯一的指望是 Agent 自己留了后手。问题是:它到底会不会留?

我最早以为会。毕竟 Agent 都已经能读写文件了,怎么会不知道自己改了什么?后来我也试着自己写了一个简单的 Agent 工具,才发现这个直觉错得离谱。
大模型本身是无状态的。你给它旧文件内容,它输出新文件内容,它并不知道这两者之间发生了什么变化。你让它在文件里加了一行 import,它不会自动意识到"这个文件被我改过了"。任何关于"改了哪些文件、改了什么"的信息,都必须由外部工程逻辑显式地记录,模型本身不提供这个能力。
所以当你问 Agent "你刚才改了哪些文件",它只能去查工具的日志。这个日志,就是文件变更追踪系统。
追踪文件变更的三条路线
具体怎么记?业界做法大致分三种。
- 直接抱上 git 大腿。像 Aider 这类工具,把每次 AI 修改包装成一次自动 commit,甚至在你未提交的脏工作区上也能工作——先把用户改动 stash 起来,AI 改完再 pop 回去。这样 Agent 天然有 diff、有回滚,你只需要一条 git 命令就能回到过去。代价是强迫你使用 git 工作流,没有 git 仓库的项目就用不了。
- 写前快照。工具在 Agent 写入文件之前,先把旧文件完整备份到内存或临时目录。Agent 写完后,工具手里同时握着旧版和新版。回滚就是把旧版复制回来。简单、粗暴、可靠,代价是空间:哪怕只改一个字节,也要存一份完整旧文件。
- 写后 diff。Agent 写完后,工具用差分算法计算新旧版本的差异。它只存差异,不存完整旧文件,更轻量,适合展示和行级控制,但对二进制文件无能为力。
还有一个容易被忽略的设计:哈希。不少工具会在写入前后各算一次文件哈希并记入日志,用来快速判断"文件是否真的变了"。哈希不告诉你改了什么,但能以极低成本交叉验证 Agent 的报告,避免写入失败却误报成功。
diff 长什么样,又是怎么算出来的
要理解 diff 怎么生成,先得认识它的长相。这是 git diff 的典型输出:
--- a/function.py
+++ b/function.py
@@ -1,5 +1,5 @@
def count_items(items):
- total = len(items)
+ total = len(items) + 1
print(f"Total: {total}")
return total
这个格式叫 unified diff。前两行是旧文件名和新文件名,@@ -1,5 +1,5 @@ 表示旧文件从第 1 行开始取 5 行,新文件同理。行首空格是上下文,- 是删除,+ 是新增。中间每个被 @@ 隔开的块,叫做一个 hunk。
生成这种 diff 的算法叫差分算法,最著名的是 Eugene Myers 在 1986 年提出的 Myers 差分算法。它的核心思路是把两个文件放在一个二维网格上,寻找从左上角到右下角的最短路径:斜着走代表匹配,往右或往下代表增删。路径越短,diff 越干净。
Myers 的时间复杂度是 O((N+M)D)。N 和 M 是两边行数,D 是差异数量。注意这个 D——如果两文件几乎相同,D 很小,算法飞快;如果文件被完全重写,D 接近 N+M,复杂度退化成平方级。好在日常改动通常只涉及少量行,所以 Agent 实时生成 diff 几乎没有性能压力。
回滚的两种姿势:反向 patch 和快照恢复
有了 diff,回滚有两条路。
反向应用 patch。把 diff 里的增删对调,再用 patch -R 打回去。这条路的优点是只依赖 diff 文本,不依赖备份,而且可以精确到行。缺点是如果后来有其他修改改变了行号,patch 可能找不到位置,直接失败。
快照恢复。把写前备份的旧文件直接拷回来。必定成功,但只能文件级回滚——哪怕你只想撤销一行,也要还整个文件。
| 维度 | 快照恢复 | 反向 patch |
|---|---|---|
| 空间占用 | 存完整旧文件 | 只存差异,轻量 |
| 恢复速度 | 直接覆盖,快 | 需要定位,稍慢 |
| 可靠性 | 必定成功 | 上下文漂移可能失败 |
| 粒度 | 文件级 | 可精确到行 |
所以工程上成熟的 Agent 工具不是二选一,而是混合:写前快照兜底,写后 diff 展示。有些工具还能把 diff 切成多个 hunk,让你像 code review 一样只接受某些块的修改。这就是你看到"接受/拒绝"按钮的来源。
更进一步,一些工具把每个操作步骤都做成 checkpoint,你可以回到任意一步,而不只是最后一步。这本质上是把快照和 diff 串成一条可回溯的历史。
但要小心:行级回滚不意味着行级正确。我见过 Agent 先改了一个函数,又改了调用它的地方。你回滚了后面的 hunk,前面的还在——代码已经不能编译了。行级回滚只保证语句层面恢复,不保证语义一致。
你以为能恢复,实际上全是坑
CRLF 换行符。我踩过这个坑。Windows 的文件换行是 ,Linux 是 。有一次我在 Windows 下让 Agent 改配置文件,生成的 diff 显示 480 行全变了。我以为是模型发疯了,排了半天才发现是换行符被统一成了 CRLF,导致每一行都被当成不同。
二进制文件。diff 按文本行工作,遇到图片、数据库文件只能告诉你"不同",无法告诉你哪里不同。这时候只有快照能兜底,所以工具通常会让二进制文件走整体备份。
用户手动干预。快照在 Agent 写入前拍下。如果你在 Agent 运行期间手动改了同一个文件,回滚会把你的修改也抹掉。这个坑比前两个更隐蔽,因为它考验的不是技术,而是用户和 Agent 的并发意识。
FAQ:三个你可能想问的问题
Agent 为什么不直接自己用 git?
有些工具确实用,比如 Aider。但把 git 命令直接暴露给模型是糟糕的设计——一次误操作可能毁掉整个仓库的历史。多数工具宁可自己实现一套 diff 和快照,也不让模型碰 git。
为什么我用 IDE 时看不到这些 diff?
很多图形化插件把 diff 藏起来了,让你感觉 AI 只是在悄悄改代码。换个思路:用命令行类的 Agent 工具,或者主动打开 IDE 的 diff 面板,你能看到每一次修改的具体内容。看到变更永远比盲信重要。
生成 diff 会拖慢 Agent 速度吗?
小文件几乎瞬间完成。但一次改几百个小文件,快照和哈希计算也需要时间。所以一些工具会用文件哈希去重,只对确实变化过的文件生成 diff。
所以 Agent 能回滚吗?能,但不是因为聪明
模型负责想象新世界,工程层负责记住旧世界。Agent 的文件变更追踪,本质上是在给一个没有记忆的系统装外置记忆。
这套机制的能力边界也很清楚:单文件级回滚已经成熟,但跨文件的依赖冲突、语义一致性检测还差得远。真正可靠的防线,永远是你自己 review diff 的那双眼睛。
原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/764.html