AI 改动把已有代码吞了:差异错位与部分应用,落盘前怎么拦

2026-07-28

数据截至 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 --statgit 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 之前,过一遍这五条:

  1. git status 干净,且有一个能回滚的提交。
  2. 这次只改一个目标,范围我说得出来。
  3. 保存即格式化在这段时间是关掉的,或者格式化基线已单独提交。
  4. 应用后先看 git diff --stat,删除行数是否符合预期。
  5. git add -p 逐块过,逐个确认删除块都是我要删的。

五条都做到,吞代码这件事基本就不会再让你损失超过一次重做的成本。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。