AI 改了代码,你怎么验收?用 Git Diff 生成一份变更审查清单

📅 2026/7/9 2:03:15 👁️ 阅读次数 📝 编程学习
AI 改了代码,你怎么验收?用 Git Diff 生成一份变更审查清单

摘要

AI 编程工具能帮你改代码,但改完之后最关键的不是“看起来能跑”,而是如何验收。本文给出一套基于 Git Diff 的审查流程和可运行 Python 脚本,自动生成AI_CHANGE_REVIEW.md,把改动文件、风险等级、测试建议、人工 Review 和回滚确认整理成清单。


正文

AI 编程工具越强,开发者越容易遇到一个新问题:

它确实把代码改完了,但我怎么知道这次改动能不能合并?

以前我们让 AI 写一个函数、补一段 SQL、改一个正则,最多肉眼看几行代码。

现在不一样了。

一次任务可能会改:

  • 3 个接口文件;
  • 2 个类型定义;
  • 1 个配置文件;
  • 1 份测试文件;
  • 甚至顺手改了 README、锁文件、CI 配置。

OpenAI Codex 的官方更新说明里,已经出现了在对话中创建、搜索、打开、fork 和管理 Codex tasks 的能力,也加入了 staged、unstaged、branch、last-turn changes 等变更过滤与分支比较控制。换句话说,AI 编程工具正在从“写一段代码”进入“管理一个任务的完整改动”阶段。
来源:https://developers.openai.com/codex/changelog

这时候,开发者最不该做的一件事是:

AI 说改好了 → 随便跑一下 → git add . → git commit → 合并

真正稳的流程应该是:

AI 完成改动 → 先看 Git Diff → 自动生成变更审查清单 → 找出高风险文件 → 跑最小必要测试 → 人工 Review → 确认回滚方案 → 再提交

这篇文章给一套可以直接落地的做法:用 Git Diff 生成一份AI_CHANGE_REVIEW.md

它不替你 Review 代码。

它帮你把“该 Review 什么”先列出来。


一、AI 改代码最怕的不是改错,而是你不知道它改了哪里

很多人让 AI 改代码时,关注点只放在最终回答:

已完成。 已修复。 已优化。 已补充测试。

但真正应该看的,是 Git 里发生了什么。

Git 官方文档对git diff的定位很直接:它可以展示工作区与暂存区、暂存区与提交、两个提交之间等不同对象的变化。也就是说,AI 改完代码后,git diff才是你确认真实改动范围的第一手依据。
来源:https://git-scm.com/docs/git-diff

尤其是下面几类情况:

情况为什么危险
AI 顺手改了无关文件可能引入额外风险
AI 改了锁文件可能影响依赖版本
AI 改了权限逻辑可能产生越权问题
AI 改了数据库迁移可能影响线上数据
AI 删除了旧兼容逻辑可能破坏调用方
AI 只改代码没补测试后续回归容易漏
AI 改了 CI/CD 配置可能影响部署流程

还有一种情况很常见:同一个文件可能同时存在 staged 和 unstaged 两种状态。Codex App 的 Review 文档也提醒过,Git 可以在同一文件里同时表示 staged 和 unstaged changes,这种情况下看起来像“同一个文件出现两次”,这是正常 Git 行为。
来源:https://developers.openai.com/codex/app/review

所以,AI 代码验收的第一步不是问 AI:

你确定改好了吗?

而是问 Git:

这次到底改了哪些文件?每个文件改了多少?有没有明显风险?


二、这份审查清单要解决什么问题

AI_CHANGE_REVIEW.md不是代码审查报告,也不是自动测试报告。

它的定位更像一个“合并前检查单”。

它至少要回答 5 个问题:

1. 这次改了哪些文件? 2. 每个文件是新增、修改、删除,还是重命名? 3. 哪些文件属于高风险? 4. 每类文件建议跑什么验证? 5. 合并前还有哪些人工确认项?

例如 AI 说:

我已经修复了用户登录失败的问题。

你真正要看的不是这句话,而是:

M src/auth/login.ts M src/middleware/permission.ts M src/types/user.ts A src/auth/login.test.ts M package-lock.json

这组改动里,authpermissionpackage-lock.json都值得重点看。

不是说它一定错,而是这几个位置本来就风险更高。


三、先准备一个干净的验收流程

在跑脚本前,建议先养成这个习惯:

gitstatus

如果工作区已经有你自己未提交的改动,先别急着让 AI 动手。

更稳的做法:

gitcheckout-bai-fix-login-timeout

让 AI 在独立分支里改。

改完后再执行:

gitdiff--name-status HEADgitdiff--numstatHEADgitdiff--checkHEAD

这里三个命令各有作用。

命令作用
git diff --name-status HEAD看哪些文件被新增、修改、删除、重命名
git diff --numstat HEAD看每个文件增加和删除了多少行
git diff --check HEAD检查空白符错误、冲突标记等问题

Git 文档说明,--numstat会以更适合机器读取的方式输出新增行、删除行和路径;--name-only/--name-status则适合快速获取变更文件名称和状态。
来源:https://git-scm.com/docs/git-diff

这三个命令就够我们生成第一版审查清单。


四、可直接运行:生成AI_CHANGE_REVIEW.md的 Python 脚本

新建文件:

generate_ai_change_review.py

粘贴下面代码:

#!/usr/bin/env python3from__future__importannotationsimportargparseimportsubprocessfromdataclassesimportdataclassfrompathlibimportPath HIGH_RISK_KEYWORDS=("auth","permission","role","security","payment","order","checkout","refund","migration","schema","database","db",".github/workflows","dockerfile","docker-compose","nginx","package-lock.json","pnpm-lock.yaml","yarn.lock","poetry.lock","requirements.txt",".env",)TEST_HINTS={".py":"运行单元测试、类型检查或关键脚本:pytest / mypy / ruff",".ts":"运行类型检查和测试:npm test / pnpm test / tsc --noEmit",".tsx":"运行组件测试、类型检查和构建:pnpm test / pnpm build",".js":"运行单元测试、lint 和构建:npm test / npm run lint",".jsx":"运行组件测试、lint 和构建:npm test / npm run build",".java":"运行 Maven/Gradle 测试:mvn test / gradle test",".go":"运行 Go 测试:go test ./...",".rs":"运行 Rust 测试:cargo test / cargo clippy",".sql":"在测试库验证迁移、回滚和数据兼容性",".md":"检查文档是否和代码行为一致",".json":"检查配置格式、schema、锁文件和依赖变更",".yml":"检查 CI/CD、部署、权限和环境变量引用",".yaml":"检查 CI/CD、部署、权限和环境变量引用",}@dataclassclassChangedFile:status:strpath:stradded:str="0"deleted:str="0"@propertydeftotal_changed(self)->int:try:returnint(self.added)+int(self.deleted)exceptValueError:return0defrun_git(args:list[str],allow_fail:bool=False)->tuple[int,str]:result=subprocess.run(["git",*args],text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,check=False,)ifresult.returncode!=0andnotallow_fail:raiseSystemExit(result.stdout.strip())returnresult.returncode,result.stdout.strip()defensure_git_repo()->None:code,_=run_git(["rev-parse","--is-inside-work-tree"],allow_fail=True)ifcode!=0:raiseSystemExit("当前目录不是 Git 仓库,请在项目根目录执行。")defdiff_args(base:str,staged:bool)->list[str]:args=["diff","--find-renames"]ifstaged:args.append("--cached")args.append(base)returnargsdefparse_name_status(raw:str)->dict[str,ChangedFile]:files:dict[str,ChangedFile]={}forlineinraw.splitlines():ifnotline.strip():continueparts=line.split("\t")status=parts[0]path=parts[-1]files[path]=ChangedFile(status=status,path=path)returnfilesdefattach_numstat(files:dict[str,ChangedFile],raw:str)->None:forlineinraw.splitlines():ifnotline.strip():continueparts=line.split("\t")iflen(parts)<3:continueadded,deleted,path=parts[0],parts[1],parts[-1]item=files.get(path)ifitemisNone:item=ChangedFile(status="?",path=path)files[path]=item item.added=added item.deleted=deleteddefattach_untracked(files:dict[str,ChangedFile])->None:_,raw=run_git(["ls-files","--others","--exclude-standard"])forpathinraw.splitlines():ifnotpath.strip():continueifpathnotinfiles:files[path]=ChangedFile(status="??",path=path,added="?",deleted="0",)defrisk_level(item:ChangedFile)->str:path_lower=item.path.lower()ifany(keywordinpath_lowerforkeywordinHIGH_RISK_KEYWORDS):return"高"ifitem.total_changed>=250:return"高"ifitem.total_changed>=80:return"中"ifitem.status.startswith(("D","R")):return"中"return"低"defreview_focus(item:ChangedFile)->str:path=item.path suffix=Path(path).suffix.lower()lower=path.lower()focuses:list[str]=[]if"test"notinlowerandsuffixin{".py",".js",".ts",".tsx",".java",".go",".rs"}:focuses.append("确认是否需要新增或更新测试")ifany(wordinlowerforwordin("auth","permission","role","login")):focuses.append("重点检查鉴权、权限边界和越权风险")ifany(wordinlowerforwordin("payment","order","refund","checkout")):focuses.append("重点检查金额、幂等、状态流转和回滚方案")ifany(wordinlowerforwordin("migration","schema",".sql")):focuses.append("重点检查迁移顺序、兼容性和回滚脚本")ifsuffixinTEST_HINTS:focuses.append(TEST_HINTS[suffix])else:focuses.append("根据文件职责补充人工 Review 和验证命令")return";".join(dict.fromkeys(focuses))defmarkdown_report(base:str,staged:bool,files:list[ChangedFile],check_output:str,)->str:lines=["# AI_CHANGE_REVIEW","","## 1. 基础信息",f"- Diff 基准:`{base}`",f"- Diff 范围:`{'staged changes'ifstagedelse'working tree / current branch changes'}`",f"- 变更文件数:`{len(files)}`","","## 2. 变更总览","","| 风险 | 状态 | 文件 | 增加 | 删除 | 审查重点 |","|---|---|---|---:|---:|---|",]foriteminfiles:lines.append(f"|{risk_level(item)}| `{item.status}` | `{item.path}` | "f"{item.added}|{item.deleted}|{review_focus(item)}|")lines.extend(["","## 3. Git diff --check 结果","",])ifcheck_output:lines.extend(["```text",check_output,"```"])else:lines.append("- 未发现 Git 可直接识别的空白符或冲突标记问题。")lines.extend(["","## 4. 人工验收清单","","- [ ] 是否只改了本次任务要求的文件?","- [ ] 是否存在未解释的大范围重构、删除或格式化?","- [ ] 高风险文件是否已单独 Review?","- [ ] 是否补充或更新了对应测试?","- [ ] 是否跑过最小必要验证命令?","- [ ] 是否检查了接口、配置、数据库、权限和日志影响?","- [ ] 是否准备了回滚方式?","","## 5. 给 AI 的二次审查提示词","","```text","下面是 AI 生成代码后的 Git Diff 审查清单。","请不要继续改代码,先只做审查。","","请按以下格式输出:","1. 你认为最需要人工 Review 的 3 个文件;","2. 每个高风险文件的风险原因;","3. 可能遗漏的测试;","4. 可能影响的调用方、配置或数据;","5. 建议先运行的验证命令;","6. 不确定的信息请标记为“待确认”,不要编造业务规则。","```","",])return"\n".join(lines)defmain()->None:parser=argparse.ArgumentParser(description="Generate an AI code-change review checklist from Git diff.")parser.add_argument("--base",default="HEAD",help="Diff base, such as HEAD, main, origin/main.",)parser.add_argument("--staged",action="store_true",help="Review staged changes only.",)parser.add_argument("--output",default="AI_CHANGE_REVIEW.md",help="Output markdown file.",)args=parser.parse_args()ensure_git_repo()base_args=diff_args(args.base,args.staged)_,name_status=run_git([*base_args,"--name-status"])_,numstat=run_git([*base_args,"--numstat"])_,check_output=run_git([*base_args,"--check"],allow_fail=True)files=parse_name_status(name_status)attach_numstat(files,numstat)ifnotargs.staged:attach_untracked(files)report=markdown_report(base=args.base,staged=args.staged,files=sorted(files.values(),key=lambdaitem:(risk_level(item),item.path),),check_output=check_output,)Path(args.output).write_text(report,encoding="utf-8")print(f"已生成:{args.output}")if__name__=="__main__":main()

这段脚本只依赖 Python 标准库和本机 Git。

我用一个临时 Git 仓库验证过:它可以识别已修改文件,也能把未追踪的新文件纳入清单,例如 AI 新建了auth_service.py但还没git add,脚本会显示为??,避免新文件被漏掉。


五、运行方式

在项目根目录执行:

python generate_ai_change_review.py--baseHEAD

如果你只想审查已经暂存的改动:

python generate_ai_change_review.py--baseHEAD--staged

如果你想比较当前分支和主分支:

python generate_ai_change_review.py--basemain

输出文件默认是:

AI_CHANGE_REVIEW.md

如果想换文件名:

python generate_ai_change_review.py\--basemain\--outputREVIEW_BEFORE_MERGE.md

六、输出大概长什么样

假设 AI 改完代码后,项目里有这些变化:

M src/auth/login.ts M src/middleware/permission.ts A src/auth/login.test.ts M package-lock.json ?? scripts/debug_temp.py

生成的AI_CHANGE_REVIEW.md可能类似这样:

# AI_CHANGE_REVIEW ## 1. 基础信息 - Diff 基准:`HEAD` - Diff 范围:`working tree / current branch changes` - 变更文件数:`5` ## 2. 变更总览 | 风险 | 状态 | 文件 | 增加 | 删除 | 审查重点 | |---|---|---|---:|---:|---| | 高 | `M` | `src/auth/login.ts` | 38 | 12 | 确认是否需要新增或更新测试;重点检查鉴权、权限边界和越权风险;运行类型检查和测试:npm test / pnpm test / tsc --noEmit | | 高 | `M` | `src/middleware/permission.ts` | 22 | 8 | 确认是否需要新增或更新测试;重点检查鉴权、权限边界和越权风险;运行类型检查和测试:npm test / pnpm test / tsc --noEmit | | 高 | `M` | `package-lock.json` | 120 | 40 | 检查配置格式、schema、锁文件和依赖变更 | | 低 | `A` | `src/auth/login.test.ts` | 96 | 0 | 运行类型检查和测试:npm test / pnpm test / tsc --noEmit | | 低 | `??` | `scripts/debug_temp.py` | ? | 0 | 确认是否需要新增或更新测试;运行单元测试、类型检查或关键脚本:pytest / mypy / ruff | ## 3. Git diff --check 结果 - 未发现 Git 可直接识别的空白符或冲突标记问题。 ## 4. 人工验收清单 - [ ] 是否只改了本次任务要求的文件? - [ ] 是否存在未解释的大范围重构、删除或格式化? - [ ] 高风险文件是否已单独 Review? - [ ] 是否补充或更新了对应测试? - [ ] 是否跑过最小必要验证命令? - [ ] 是否检查了接口、配置、数据库、权限和日志影响? - [ ] 是否准备了回滚方式?

这份清单最大的价值,不是告诉你“代码对不对”。

而是提醒你:

哪些文件不能一眼扫过去,必须重点看。


七、风险分级不要迷信,但要有默认规则

上面脚本里的风险分级很简单:

权限、鉴权、安全、支付、订单、数据库、迁移、CI/CD、依赖锁文件 → 高风险 变更行数超过一定范围 → 中高风险 删除、重命名 → 中风险 普通小文件改动 → 低风险

这不是绝对标准。

不同项目应该按自己的情况调整。

比如一个内容站项目里,Markdown 文档可能是低风险;但如果你做的是文档驱动 API 平台,文档变化反而可能影响 SDK、页面、接口示例和客户集成。

再比如package-lock.json,有时候只是正常安装依赖后的锁文件更新;有时候却意味着依赖树大面积变化,甚至引入安全风险。

所以风险等级不是结论。

它只是提醒你:

这里要多看一眼。 这里要单独跑测试。 这里要问清楚 AI 为什么改。 这里合并前要准备回滚。


八、AI 改完代码后,可以这样追问它

生成AI_CHANGE_REVIEW.md后,不要立刻让 AI 继续改。

先让它做二次审查。

可以直接把清单和核心 diff 给它,然后问:

下面是 AI 生成代码后的 Git Diff 审查清单。 请不要继续改代码,先只做审查。 请按以下格式输出: 1. 你认为最需要人工 Review 的 3 个文件; 2. 每个高风险文件的风险原因; 3. 可能遗漏的测试; 4. 可能影响的调用方、配置或数据; 5. 建议先运行的验证命令; 6. 不确定的信息请标记为“待确认”,不要编造业务规则。

这段提示词里最关键的是:

请不要继续改代码,先只做审查。

很多开发者让 AI 改完之后,一发现不放心,就继续说:

你再帮我检查一下,有问题就修。

结果 AI 又开始改第二轮。

第二轮改动叠在第一轮上,Diff 更难看,问题也更难定位。

更稳的做法是先冻结修改,让它只分析。

如果确实需要第二轮修改,也要重新生成一次AI_CHANGE_REVIEW.md


九、建议把验收拆成四层

AI 改代码后,不能只靠一份脚本。

我更建议分成四层验收。

1. 文件范围验收

先看:

gitdiff--name-status HEAD

确认有没有无关文件。

尤其注意:

.env secrets/ config/ package-lock.json pnpm-lock.yaml yarn.lock docker-compose.yml .github/workflows/ migration/ schema/ auth/ permission/ payment/ order/

这些文件不是不能改,而是不能“顺手改”。

2. 行数和重构范围验收

再看:

gitdiff--numstatHEAD

如果一个简单 Bug 修复,突然出现:

800 added, 600 deleted

那就要问清楚:

为什么需要这么大范围变更? 是不是格式化了整个文件? 是不是把旧逻辑删掉了? 是不是顺手重构了无关模块?

3. 语法和格式验收

至少跑:

gitdiff--checkHEAD

再按项目类型跑命令。

例如:

pnpmtestpnpmlintpnpmbuild

或者:

pytest ruff check.mypy.

或者:

gotest./...

这些命令不能保证业务正确,但能先挡掉一批低级问题。

4. 业务风险验收

最后才是人工 Review。

重点看:

  • 是否改变接口返回结构;
  • 是否破坏旧调用方;
  • 是否绕过权限判断;
  • 是否影响数据库字段;
  • 是否遗漏并发和幂等;
  • 是否改变日志内容;
  • 是否把敏感信息打印出来;
  • 是否缺少回滚方案。

AI 能帮你整理审查点,但不能替你承担上线风险。


十、这套方法适合什么场景

适合:

  • AI 改了多个文件;
  • AI 新增了测试;
  • AI 改了接口字段;
  • AI 调整了权限逻辑;
  • AI 补了配置;
  • AI 更新了依赖;
  • AI 做了小范围重构;
  • AI 生成了迁移脚本;
  • 合并前想快速生成 Review 清单。

不适合直接替代:

  • 人工 Code Review;
  • 单元测试;
  • 集成测试;
  • 安全扫描;
  • 权限审计;
  • 数据库迁移演练;
  • 灰度发布和回滚验证。

尤其是支付、权限、登录、订单、数据库迁移、CI/CD 这些位置,即使脚本标了“高风险”,也只是第一层提醒。

真正该做的是单独 Review。


十一、一个更稳的 AI 编程工作流

我现在更推荐这样的顺序:

1. 开新分支 2. 让 AI 先输出修改计划 3. 人确认计划 4. AI 小步修改 5. 生成 AI_CHANGE_REVIEW.md 6. 让 AI 只做审查,不继续改 7. 人工 Review 高风险文件 8. 跑测试和构建 9. 必要时第二轮小步修改 10. 再生成审查清单 11. 确认回滚方案 12. 提交

这套流程看起来比“让 AI 一口气改完”慢一点。

但它更适合真实项目。

因为真实开发里,最贵的不是多看两分钟 Diff。

最贵的是把一个没看清楚的 AI 改动合进去,等线上报错后再回头找它到底改了哪里。


十二、最后总结

AI 改代码以后,最重要的动作不是立刻合并,而是先验收。

git diff是事实来源。

AI 的解释只是辅助。

更稳的顺序是:

看文件范围 → 看增删行数 → 检查高风险文件 → 跑最小测试 → 让 AI 只做审查 → 人工确认 → 再提交

这份AI_CHANGE_REVIEW.md的意义,就是把“我大概看了一下”变成“我知道该重点看哪里”。