# 让每一步都能退回：AI 一次改十几个文件时，提交、检查命令和分支怎么组成安全网

从原子提交的判据、先绿再提交的顺序，到一条 make check 和隔离分支，把大改动关进可回退、可判定的循环里

> AI 编程协作 · 版本控制 · 长任务可靠性 · 约 7 分钟 · 10 月 21 日

## 本篇要点

1. AI 一次改十几个文件的核心风险不是“看着累”，而是回滚的最小单位被放大：一个大提交会让 revert 撤掉无关的正确改动，让 bisect 定位到一个内部仍是大杂烩的提交。
2. 提交粒度的判据是：这次提交里如果有一处错，你愿不愿意把剩下的部分一起丢掉；愿意就是合适的原子提交。
3. 必须先绿再提交，因为安全网的价值在于“退回的状态一定是好状态”；否则人就不敢回退，转而把新改动叠在坏状态上。
4. 一条检查命令（如 make check）把 lint、类型检查、测试收进同一个入口，用退出码给出通过或失败，AI 才能自己完成“改、跑、修”的循环。
5. 动手前要先确认基线全绿，否则无法区分这次引入的失败和原有的失败。
6. 提交解决“退一步”，分支解决“整条路线不要了”：整条分支丢弃的代价是零，而撤销一串提交不行。
7. 全绿只说明没有违反已有检查，不说明需求实现，也不说明测试本身有效；checkpoint 类回退功能只覆盖文件编辑工具，不能替代 git。

---

你让它加一个“导出 CSV”的功能。回来时 `git status` 列了十七个文件：新模块、改过的路由、两个配置文件、几个快照测试，还有一个你根本没提过的工具函数。

此刻你缺的不是读代码的耐心，是两样东西：一个能退回去的落点，和一个能当场判断“改坏了没有”的信号。这篇把这两样凑齐：用提交作为退回的落点，用一条检查命令作为判定的信号，用隔离分支兜住整条路线作废的情况。三样拼在一起才叫安全网，缺一样就只是心理安慰。

## 为什么“一次改十七个文件”是工程风险

两个退化，都比“看着累”严重。

一是审查退化成模式匹配。上一篇[审 AI 生成的代码](https://pangzhengboyin.com/articles/reviewing-ai-generated-code-plausible-diff-d168d6b2)的基本动作是先看这次改了什么、再核对它对项目的假设，而这取决于 diff 的大小：改动一旦超出你能同时握在手里的范围，审查就变成“扫一眼觉得说得通”。

二更关键：**回滚的最小单位被放大了。** git 能退回去的粒度就是提交。十七个文件压成一个提交，出问题时 `git revert` 会把这次的全部改动一起撤掉，包括本来做对的部分。想定位是哪一处改坏了？`git bisect` 能二分出“问题就在这次提交”，但你打开它，面对的仍是功能、测试、重构、格式化、依赖升级混在一起的一堆改动，根因还是没定位出来 [3][5]。

所以提交粒度直接决定了出问题时的修复成本。

## 提交：把“能退回的工作状态”切成几份

判断粒度只有一句话：**这次提交里如果有一处是错的，你愿意把剩下的部分一起丢掉吗？** 愿意，粒度就对了；不愿意，说明它塞了两件以上的事。

按这个标准，“加导出功能”“顺手重排 import”“升级一个依赖”“格式化整个文件”必须分开；接口定义和它的实现可以放一起，因为那是一件概念。这种提交有个通行名字，叫**原子提交**（atomic commit）：一个提交只表达一个能独立解释、独立回滚的意图，并且在这个点上代码能跑、能过检查 [2][4]。

比粒度更容易漏的是顺序：**先绿，再提交。**

理由是一条推理链。安全网的全部价值在于“我退回去的那个状态一定是个好状态”。提交里混着没验证的中间态，回退就只是从一种坏法换到另一种坏法。经历过一两次，人就不敢退了——改成在原地继续打补丁，错误一层层叠在同一个未提交的堆上。所以有条实践规则很直接：未提交的改动不得堆叠，检查没过就先修好，别在坏状态上加新改动 [4]。坏状态上叠三层，最后你要面对三个互相影响的错误同时爆发。

## 一条检查命令：把“改坏没有”变成一个退出码

把“这个状态是好状态”变成机器能回答的问题：把项目里所有自动检查收进一条命令，只记这一个入口。

```makefile
check:
	ruff check .
	mypy src
	pytest -q
```

（`make` 只是给几条命令起名字的老办法，`make check` 就是把这三行依次跑一遍。用什么工具不重要，重点是它是**一条**命令。）

第一，用退出码说话：跑完，0 就是通过，非 0 就是失败。这不只是给你省事，更是给 AI 一个能读的信号。官方实践文档把这条列在最前：有东西可跑，它就能做、跑检查、读结果、自己迭代到通过；没有，“看起来做完了”就是它唯一能拿到的信号，于是你变成验证回路本身，每个错误都等你发现 [1]。命令散在你脑子里，它就会漏跑一个，而漏跑的那个往往正好是能发现问题的。

第二，先把基线跑绿。动手前在没改动的原状态上跑一次。这一步不能省，因为失败有两种：这次引入的，和本来就有的。基线是红的，你就无法区分，AI 也会把已有的红当背景继续往上加代码。基线红时，先记下哪几条在失败，再开工。

第三，便宜的放前面：静态检查和类型检查几秒出结果，单元测试次之，集成测试最慢，失败得越早越省事。

但记住它管到哪儿：**它只回答“有没有违反我已写下的检查”。** 它不回答需求是否实现，也不回答这些测试本身会不会变红——后者是上一篇[用变异检查撞坏代码](https://pangzhengboyin.com/articles/ai-written-tests-oracle-and-mutation-check-79424d12)解决的问题。一条烂测试组成的检查命令，跑一万次也全绿。

## 分支：给“整条路线不要了”留退路

提交解决“退一步”，分支解决另一步：**这次实验整体方向就错了。**

在 `main` 上小步提交，做到第七步发现方案走不通，你要么留下一串提交（再产生一串 revert，顺序有依赖，可能冲突），要么重写历史。一开始就在 `ai/csv-export` 分支上做，收场只有两步：

```bash
git switch main
git branch -D ai/csv-export
```

代价是零，历史里也不留痕迹。提交是路上的存档点，分支是这条路本身，互不替代。

顺带一条硬规矩：AI 不该有直接推 `main` 的权限。分支保护、必须过状态检查才能合并，防的不是人，是“一次没看就混进去了” [5]。

## 接起来：一块任务一个循环

```mermaid
flowchart LR
  A["跑 make check<br/>确认基线绿"] --> B["开分支 ai/任务名"]
  B --> C["让 AI 只改这一块"]
  C --> D{"git diff --stat<br/>只碰任务内文件？"}
  D -->|"否"| E["让它收回范围外的改动"]
  E --> C
  D -->|"是"| F["跑 make check"]
  F --> G{"绿了？"}
  G -->|"否"| H["退回上一个绿点<br/>不要继续叠改动"]
  H --> C
  G -->|"是"| I["提交，写清为什么这么改"]
  I --> J{"还有下一块？"}
  J -->|"有"| C
  J -->|"没有"| K["整条合入，或整条丢弃"]
```

**发任务时就说清只做一块。**“这次只加导出函数和它的测试，不动路由、不动配置、不格式化无关文件。改完先跑 `make check`，把输出贴回来，先别提交。”需求那篇讲的边界，在这里变成可检验的边界：`git diff --stat` 列出的文件，就是它实际动过的地方。

**提交权留在你手上。** 让它在循环里反复跑检查、自己修，这是效率来源；但“这算做完、可以存档了”是个判断，得你确认。提交信息写清为什么这么改，而不是“AI 修改”。

**红了就退，不要叠。** 检查没过时最容易犯的错是接着喊“还是红，你再改改”，在一个未知状态上叠第二层改动。此时回到上一个已知良好的点（`git restore .` 丢掉未提交的改动），再重新提要求。这和报错那篇的停手判据是同一条：现场变复杂了就停。

**已经压成大提交也能拆。** 用 `git reset --soft HEAD~1` 把提交拆开（改动都还在暂存区），再用 `git add -p` 一块块挑着提交；也可以让它整理提交历史，但你要核对最终内容与整理前一致 [2]。只在没有 push 出去的提交上这么做。

## 三个让安全网变成心理安慰的地方

**把全绿当验收通过。** 检查只核对断言里写了的东西，绿色不说明功能对。另外本地全绿和 CI 全绿不是一回事，同一把尺要在两处都量。

**把提交切得太碎。** 每个小步都提交，历史成了流水账，bisect 找到的提交本身没有语义，等于没定位。判据还是那句：能不能独立解释、独立回滚。

**把工具自带的回退当成 git。** 有些助手提供 checkpoint、`/rewind`，能撤销它自己改过的文件，但官方文档写得很明确：它只覆盖通过文件编辑工具做的改动，用 Bash 命令或外部进程产生的影响不在其中，不能替代 git [1]。

三样东西回答的是同一个问题的两面：**每一步都要有一个已知良好的落点，和一个能自动判定的信号。** 前者靠提交和分支，后者靠那条命令。少了任一半，你就只能盯着十七个文件的 diff，祈祷没漏看。

## 术语表

- 原子提交：一次提交只装一个能独立解释、独立回滚的意图，并且在这个提交点上代码能通过检查。
- 检查命令：把 lint、类型检查、测试收进同一条命令，用退出码 0 或非 0 给出通过或失败，让人和 AI 都只看一个结果。
- 基线全绿：动手前先在原状态跑一遍检查，确认原本是通过的，否则分不清失败是新引入的还是早就有的。
- 隔离分支：为一个任务单开的分支，出问题时整条丢弃，代价为零，用来兜住方向性错误。

## 来源

1. [Best practices for Claude Code — 给 AI 一条能自己跑的检查；checkpoint 只覆盖文件编辑工具的改动，不能替代 git](https://code.claude.com/docs/en/best-practices)
2. [Reviewing and Testing Code — Cursor Learn — 建议小而语义明确的提交，并可让 agent 事后整理提交历史](https://cursor.com/cn/learn/reviewing-testing)
3. [Committing AI-Generated Code — DeployHQ — 单一巨型提交让评审更难、bisect 几乎不可行、回滚痛苦](https://www.deployhq.com/git/committing-ai-generated-code)
4. [Small, atomic commits — The Agentic Developer Cookbook — 改动必须通过构建与测试再提交，坏改动不得堆叠](https://agenticdevelopercookbook.com/guidelines/implementing/code-quality/atomic-commits)
5. [AI 时代的 Git 版本管理 — 巨型提交让 revert、bisect 定位失效；用分支保护禁止直接推送 main](https://developer.volcengine.com/articles/7636965064381038619)

---

原文：https://pangzhengboyin.com/articles/rollback-safety-net-for-ai-code-changes-7ac118ad

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
