Agent 的文件变更追踪:修改了哪些文件、改了什么——Diff 生成与回滚机制

先破一个误区:模型根本不知道自己改了文件

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

Abstract blue waves of glowing particles

我最早以为会。毕竟 Agent 都已经能读写文件了,怎么会不知道自己改了什么?后来我也试着自己写了一个简单的 Agent 工具,才发现这个直觉错得离谱。

大模型本身是无状态的。你给它旧文件内容,它输出新文件内容,它并不知道这两者之间发生了什么变化。你让它在文件里加了一行 import,它不会自动意识到"这个文件被我改过了"。任何关于"改了哪些文件、改了什么"的信息,都必须由外部工程逻辑显式地记录,模型本身不提供这个能力。

所以当你问 Agent "你刚才改了哪些文件",它只能去查工具的日志。这个日志,就是文件变更追踪系统。

追踪文件变更的三条路线

具体怎么记?业界做法大致分三种。

  1. 直接抱上 git 大腿。Aider 这类工具,把每次 AI 修改包装成一次自动 commit,甚至在你未提交的脏工作区上也能工作——先把用户改动 stash 起来,AI 改完再 pop 回去。这样 Agent 天然有 diff、有回滚,你只需要一条 git 命令就能回到过去。代价是强迫你使用 git 工作流,没有 git 仓库的项目就用不了。
  2. 写前快照。工具在 Agent 写入文件之前,先把旧文件完整备份到内存或临时目录。Agent 写完后,工具手里同时握着旧版和新版。回滚就是把旧版复制回来。简单、粗暴、可靠,代价是空间:哪怕只改一个字节,也要存一份完整旧文件。
  3. 写后 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

(0)
上一篇 4小时前
下一篇 4小时前

相关推荐