AI 改动把已有代码吞了:差异错位与部分应用,落盘前怎么拦
数据截至 2026-07,各产品的额度与报错口径以官方最新说明为准。
多数人把这类事故归错因:以为是模型”乱改”,实际上绝大部分损失发生在”应用改动”这一步——模型给出的片段本身没问题,但把它写进文件的定位过程失败了,再叠加保存时机和格式化器的抢占,磁盘上就出现了你没批准过的结果。 归因错了,后面所有动作都会跑偏:你会去调整措辞、换模型、加约束,而真正该改的是落盘前的那道闸。
先说清本篇和站内相近内容的分工。补全弹不出来、光标处没反应、生成卡住不出字,那是交互层失灵,看 [Cursor 补全不工作、AI 没反应怎么办](/learn/cursor-buquan-bugongzuo/);模型把能跑的代码改成不能跑的、逻辑越改越偏,那是内容质量问题,看 [AI 把能跑的代码改坏了](/learn/ai-gaihuai-daima-huigun/)。本篇只管中间那一段:从”模型给出改动”到”字节写进文件”,怎么让错误在落盘前被拦住,以及已经落盘之后怎么捞。
一、先搞懂它为什么会吞代码
工具把改动写进文件,通常不是整文件替换,而是”片段 + 锚点”:模型输出若干代码块,工具在当前文件里找到相似的上下文行,然后把该区域替换掉。这个匹配是模糊的,允许一定的行内差异,否则任何空格变动都会让应用失败。
模糊匹配就是风险来源。三件事会让锚点漂掉:
一是文件在生成期间变了。你在等回复的十几秒里顺手敲了两行,或者保存触发了自动导入整理,模型看到的版本和落盘时的版本已经不是同一个。工具拿旧锚点去新文件里找,找不到精确位置就退化成”最相似的位置”,于是替换范围外扩,把邻近的完好代码一起吃掉。
二是同一文件有多个写入方。 另一个会话、后台任务、格式化器、语言服务的自动修复,都在写同一个文件。谁最后写谁赢,前面的改动静默消失。并发引起的连锁问题另有专文,见 [多个会话同时改一个仓库,代码互相覆盖、文件写](/learn/duo-huihua-bingfa-chongtu/)。
三是改动被截断。 输出中途断掉、多个片段只有前几个应用成功,剩下的报了个失败提示但你没注意。结果是半套改动落盘:新函数进来了,调用它的地方还没改,或者反过来。
理解这三条以后,排查就有了顺序:先看落盘结果的规模,再看范围,最后才看内容对不对。
二、按现象分因:一张判别表
出事后先别改代码,先做判别。下面这张表按”你实际看到的现象”入口:
| 现象 | 大概率成因 | 怎么验证 | 处置动作 |
|---|---|---|---|
| 目标函数改对了,但相邻几十行不见了 | 锚点漂移导致替换范围外扩 | git diff --stat 看删除行数远大于你预期;git diff -- <路径> 看删除块是否连续且与新内容不相关 | 整文件回退到基线,重新只针对单个函数应用一次 |
| 改动只落了一部分,代码引用不到新符号 | 多片段应用部分失败或输出被截断 | 全项目搜新增符号名,看定义和调用是否成对出现;对照这次改动本应涉及的文件清单,用 git status 看有没有文件一行没动 | 不要让模型”继续”,回退后重发一个更小的改动请求 |
| 内容看着对,但整文件都标成改动 | 行尾符翻转或格式化器全量重排 | git diff --stat 显示改动行数接近文件总行数;git diff --ignore-all-space 后差异骤减(只怀疑行尾则用 git diff --ignore-cr-at-eol 单独验证) | 单独提交一次格式化基线,再重做功能改动 |
| 缩进层级错乱、括号数量不对 | 片段边界切在语法块中间 | 让编译器或解析器跑一遍;缩进敏感语言用 python -m py_compile <文件> | 回退,改为让模型输出完整文件再人工落盘 |
| 你没提到的文件也被改了 | 改动范围失控,工具自行连带修改 | git status 列出全部改动文件,逐个确认是否在你的意图内 | 保留目标文件、丢弃其余文件的改动,见下节的分段落盘 |
文件里出现冲突标记 <<<<<<< | 补丁按合并方式写入,或与未解决的合并状态叠加 | 全项目搜索冲突标记;git status 看是否处于合并中 | 先把仓库状态收拾干净、解掉冲突再动 AI,不要在合并中途叠改动 |
| 反复应用同一改动,每次错位位置不同 | 上下文里的文件版本已经和磁盘脱节 | 让工具原样复述目标函数的前后各三行,逐字和文件里的实际内容比对,不一致就是脱节 | 换路径:关会话重开,手工贴片段,不再依赖自动应用 |
表里的第一列是你能直接观察到的,不需要任何工具专有信息就能对上号。这也是为什么验证列里反复出现 git diff --stat 和 git status——版本控制是这些工具之外的独立观测面,它看到的是磁盘上真实发生了什么,而工具界面里显示的”已应用”只是它自己的说法。两者不一致时,永远信前者。
三、落盘前的四道闸
事故的成本不对称:拦住一次的代价是几秒钟,丢一次的代价是半天。所以闸门要设在写入之前。
第一道:每次应用前,工作区必须干净。 意思是 git status 没有未提交的散乱改动。理由很直接:一旦 AI 改动和你自己的手改混在同一份未提交状态里,出事以后没有任何工具能把两者分开,只能整体丢弃。做法是给每次 AI 改动一个明确起点:
git switch -c ai/refactor-parser # 单独分支,出事直接扔
git status # 必须是干净的
# 如果手上有活干了一半,先存起来
git stash push -m "before-apply"
第二道:一次只改一个目标。 一个文件、一个函数、一件事。请求越大,模型输出的片段越多,锚点越多,失败面越宽。而且一次改动越小,git diff 越好看清。范围怎么框、怎么在需求侧就写清边界,见 [一句需求换来 30 个文件的改动,AI 的手](/learn/ai-gaidong-fanwei-shikong/)。
第三道:把格式化从 diff 里拿掉。 格式化冲突之所以恶劣,是因为它让真实改动淹没在几百行噪音里,你看不出哪一行是被吞掉的。彻底解法是先统一基线:项目根放 .editorconfig,把缩进、行尾、字符集定死;然后跑一次全量格式化,单独提交,这一次提交只做格式化,不带任何逻辑改动。之后每次 diff 就只有语义内容。行尾问题在跨平台协作里尤其常见,可以显式约定:
git config core.autocrlf false # 团队统一在 .gitattributes 里定,别各自配
git diff --ignore-all-space # 排查时先屏蔽空白差异看真实改动
在 AI 密集改动的这段时间里,我倾向于临时关掉”保存即格式化”。理由是它会在应用完成后立刻重写文件,把两次写入叠在一起,事后无法区分是谁动的。
第四道:分段确认,不要整体接受。 逐块过一遍是唯一能在落盘边界上拦住错位的动作:
git diff -- <路径> # 先整体扫一眼删除量
git add -p # 逐块决定进不进暂存区
git restore -p -- <路径> # 逐块丢弃工作区里不想要的改动
这三条的顺序不能颠倒。先看整体删除量是为了给自己一个预期值,否则逐块过的时候你没有判断基准,看到一个大删除块也只会觉得”大概是重构吧”就放过去。git restore -p 放最后,是因为丢弃是不可逆动作,得等确认过哪些块要留才动手。
git add -p 的价值不在于严谨,而在于它强迫你的眼睛扫过每一个删除块。被吞掉的代码在这里表现得非常明显:一个删除块里全是和本次意图无关的老代码。看到这种块就停手,不要试图在同一份改动里修补。
四、已经吞了:抢救顺序
关键动作只有一个:先冻结现场。 不要再让任何工具写这个文件,不要再让模型”帮你恢复”——它会凭印象重写,产出看起来像原文但细节不对的代码,那比丢了更危险。把当前目录整体复制一份到别处,再开始捞。
按恢复概率从高到低试:
第一,编辑器的撤销栈。刚发生、文件还开着、没关过窗口,撤销通常能一路退回到应用之前。很多编辑器还有本地文件历史,会在写入前留快照,路径和保留策略各家不同,值得先翻一眼。
第二,暂存区和 stash。如果你在应用之前 git add 过,那份内容已经变成仓库里的对象,工作区被覆盖不影响它。索引这一层要分两种情况,很多人混在一起找,结果两边都没找对。
情况一,暂存区里还是好的版本(应用之后你没再 git add 过)。直接看、直接取:
git status # 确认这个文件有暂存的改动
git show :<路径> # 打印暂存区里的那一版内容
git restore --worktree -- <路径> # 用暂存区的版本覆盖回工作区
git show :<路径> 这个冒号写法是”从索引里读”,不涉及任何提交,所以哪怕你从没提交过也能用。先 git show 看一眼再决定要不要覆盖,别直接 restore,万一暂存区里存的已经是被吞过的版本,覆盖就把工作区里仅剩的线索也盖掉了。
情况二,应用之后你又 git add 了一次,索引里的好版本被顶掉了。这时旧对象还在,只是没有任何引用指着它,也就是游离对象:
git fsck --dangling # 列出无人引用的对象
git cat-file -p <对象哈希> # 逐个看内容
注意成立条件:只有被顶替掉的旧版本才会变成 dangling blob。如果那一版还老老实实待在索引里,git fsck 是不会报它的——它还有引用,不算游离。这就是为什么情况一必须先用 git show :<路径> 排除,直接上 git fsck 找不到东西时容易误判成”没救了”。dangling 对象的数量可能不少,用内容片段和大小筛;git fsck --lost-found 会把它们落成文件放进 .git/lost-found/,量大时比逐个 cat-file 好翻。
如果你按第一道闸做过 git stash push,那更省事,stash 本身就是一次完整快照:
git stash list # 找到应用之前那条
git stash show -p stash@{0} # 先看内容,别急着 pop
先 show 再决定,是因为 pop 会把 stash 内容和当前这份已经出过事的工作区合到一起,可能直接撞出冲突标记,把现场搅得更乱。看清楚以后,只需要其中一个文件的话,用 git checkout stash@{0} -- <路径> 取单个文件比整体 pop 安全。
第三,提交历史。这是最可靠的一层,前提是你之前提交过:
git reflog # 找到出事前的位置
git diff HEAD@{1} -- <路径> # 对比那一刻和现在
git restore --source=HEAD@{1} -- <路径> # 只恢复单个文件
第四,构建产物和运行环境。编译过的中间文件、打包后的 bundle、容器镜像里的旧副本,有时能反推出丢失的逻辑。不能直接用,但能提醒你原来那段代码干了什么。
第五,重写。到这一步就老实重写,别在残缺文件上缝补。
五、什么情况下别再折腾
排查这类问题最大的浪费是不肯止损。给三条硬线:
同一个改动应用失败三次,就不是运气问题。 三次都错位,说明工具手里的文件版本和磁盘已经脱节,或者这个文件的结构(超长、重复片段多、生成代码)不适合模糊匹配。换路径:要么让模型输出完整文件内容你自己覆盖,要么让它只输出一段标准的统一 diff 你手动落盘,要么把文件先拆小。继续重试只会累积更多脏改动。
修复量接近重写量,就回滚重写。 判断依据很具体:你已经花了超过原本写这段代码所需时间的一半在恢复上,且还不确定拿回来的内容是否完整。这时回到基线提交重写,心理上难受,实际上更快,而且结果是干净的。
出现”越修越乱”的迹象就停。 每轮修复引入新的报错,报错点还在漂移,说明你和模型都已经在猜。这种循环的识别和退出方式见 [同一个 bug 让 AI 改了五轮还没好](/learn/ai-xiubuhao-siwei-xunhuan/)。
回滚点怎么定?就是你把这次改动交给 AI 之前的那个提交。这句话隐含一个要求:交出去之前必须有一个提交。如果没有,你就没有回滚点,所有讨论都是空的。这也是我认为在 AI 密集改动的项目里,提交粒度应该比过去更细的原因——提交在这里不只是记录,它是安全带。
顺带说一句关于工具选择:海外的编程助手对中国大陆有区域限制,官方不支持直连使用;市面存在第三方中转,但可靠性和数据合规都由你自己承担,我不背书也不给渠道。选型时把这一层的稳定性算进去,因为连接中断本身就是”改动只落一半”的常见诱因。
六、避坑清单
在未提交状态上叠 AI 改动。 为什么会踩:手上活没干完,正好想让 AI 顺手改点别的,觉得一起提交省事。踩了以后 AI 的错位改动和你的手改无法分离,只能整体丢。怎么避:应用前 git stash push 或先提交一个 WIP,把两者物理隔开。
让模型”接着上次继续改”。 为什么会踩:上一轮部分应用失败,你觉得补齐剩下的最快。但模型手里的文件状态已经不对,它会在错位的基础上继续错位。怎么避:任何一次部分失败,都先回退到干净状态,再重发一个完整的小请求。
开着保存即格式化做大改动。 为什么会踩:这是平时的好习惯,没人想到要关。结果格式化器在应用后立刻重写全文,diff 变成几百行,被吞掉的十几行藏在里面看不见。怎么避:改动期间关掉,或者先把格式化基线单独提交掉。
多个会话同时开着同一个文件。 为什么会踩:为了快,一个会话改接口一个会话改实现,觉得互不干扰。实际两边都基于旧版本写入,后写的覆盖先写的。怎么避:同一文件同一时间只有一个写入方,改完提交再切下一个。
直接点整体接受,靠看结果是否能跑来验收。 为什么会踩:能跑就等于对,这个直觉在有测试覆盖的地方成立,在没覆盖的地方完全不成立——被吞掉的往往正是没被测试触及的边界处理和错误分支。怎么避:git add -p 逐块过,重点看删除块。
用生成来”恢复”丢失的代码。 为什么会踩:模型语气很确定,产出看起来合理。但它是在编,细节(常量、边界、特殊分支)会和原文不一样,而这种不一样很难在评审时发现。怎么避:恢复只走版本控制和编辑器历史;确实找不回,就承认丢失并按新代码走完整评审,别当成”复原”。
把长文件整体丢给模型改。 为什么会踩:图省事,觉得给全上下文更准。但文件越长,重复结构越多,锚点唯一性越差,错位就越容易发生——这里没有可靠的量化关系可给,只能说方向明确。怎么避:先拆文件,或者明确只让它输出目标函数,其余部分不许出现在输出里。
收束
这类事故的性质是工程问题,不是模型能力问题。模型的输出质量决定改动对不对,而落盘环节的纪律决定改错的时候你损失多少。两者要分开治:前者靠约束和评审,后者靠版本控制和分段确认。
每次把改动交给 AI 之前,过一遍这五条:
git status干净,且有一个能回滚的提交。- 这次只改一个目标,范围我说得出来。
- 保存即格式化在这段时间是关掉的,或者格式化基线已单独提交。
- 应用后先看
git diff --stat,删除行数是否符合预期。 - 用
git add -p逐块过,逐个确认删除块都是我要删的。
五条都做到,吞代码这件事基本就不会再让你损失超过一次重做的成本。