1. 为什么“AI 写完代码”只是上半场,真正卡脖子的是合并那一下
我最近半年在团队里推 AI 辅助编码,从最早的补全插件一路用到现在的 Agent 式改代码,感受特别深:生成代码这件事,门槛已经被拉得很低了。你给一个足够清晰的提示词,让它读一下仓库结构,再丢一个git diff过去,它能在几分钟内吐出一版看起来相当像样的实现。但真正让我在评审会上犹豫的,从来不是“它写得对不对”,而是“我敢不敢点那个 Merge 按钮”。
这个标题里说的“敢不敢合并”,其实戳中了一个很现实的问题:AI 生成的代码,缺少一条可追溯的证据链。人写代码的时候,哪怕写得烂,你至少能通过提交信息、分支命名、关联的 issue、评审评论,拼出一个“他为什么这么改”的上下文。而 AI 改代码,很多时候你拿到的是一个孤零零的 diff,它告诉你“我改了这三个文件”,但不告诉你“我为什么改这三个文件、依据是什么、有没有漏掉边界、有没有引入新的依赖”。
所以我后来做了一件事:把“代码评审”本身封装成一个Skill,让 Agent 在提交合并请求之前,先自己跑一遍带证据链的评审流程。这个 Skill 不负责写代码,它只负责回答一个问题——这次改动,凭什么可以被合并。它输出的不是一句“LGTM”,而是一份结构化的评审报告:改了哪些文件、每个改动对应什么意图、风险点在哪、测试覆盖到什么程度、有没有触碰敏感配置。
这篇文章我想把这个 Skill 的设计思路、核心实现、实操步骤和踩过的坑完整拆一遍。适合两类人看:一类是已经在用 AI 写代码、但每次合并都心里没底的开发者;另一类是想把 Agent 能力落到工程流程里、而不是停留在“聊天玩具”阶段的团队。我会尽量把每个设计决策背后的“为什么”讲清楚,让你看完能直接照着搭一套。
2. 这个代码评审 Skill 到底解决什么问题
2.1 先搞清楚:普通 AI 评审和“有证据链”的评审差在哪
很多人已经在用 AI 做代码评审了,做法通常是:把git diff复制粘贴给模型,问一句“帮我看看这段代码有没有问题”。这确实有用,但它有几个致命短板。
第一,它没有仓库上下文。模型只看到 diff,看不到这个函数被谁调用、这个配置项在别处怎么用、这个接口的契约是什么。它给出的建议往往是“这里可以加个空值判断”,但那个空值在上游已经被保证了,属于无效建议。
第二,它没有意图对齐。diff 只说明“改了什么”,不说明“本来想改什么”。如果 AI 改代码时理解偏了,评审 AI 也会跟着偏,因为它俩看的是同一份 diff,没有独立的“需求来源”去交叉验证。
第三,它没有证据留存。评审完就完了,结论散落在聊天记录里。等到上线出问题回头查,你找不到“当时为什么认为这个改动是安全的”。
我做的这个 Skill,核心就是补上这三块:注入仓库上下文、对齐改动意图、输出可留存的证据链。它把评审从“一次对话”变成“一次可复现的流程”。
2.2 证据链具体指什么:四个必须回答的问题
我把“证据链”拆成四个必须回答的问题,Skill 的整个流程就是围绕这四个问题设计的:
- 改了什么:精确到文件、函数、行级别的变更清单,不是笼统的“修改了用户模块”。
- 为什么改:每个变更块对应的意图,来源可以是提交信息、关联任务、或者 Agent 自己的推理,但必须显式写出来。
- 风险在哪:识别出高风险改动,比如数据库 schema、鉴权逻辑、并发控制、外部依赖升级。
- 验证到什么程度:哪些改动有测试覆盖,哪些没有,没覆盖的部分靠什么保证。
这四个问题回答完,评审报告才算完整。你会发现,这其实和人类资深评审者脑子里转的东西是一样的,只是我们把它显式化、流程化了。
2.3 为什么用 Skill 而不是写个脚本
这里要解释一个选型问题。你完全可以用一个 Python 脚本调 API 来做这件事,为什么非要封装成 Skill?
因为Skill 的本质是“可被 Agent 调用的能力单元”,它和脚本的区别在于:脚本是你手动跑的,Skill 是 Agent 在需要的时候自己决定调用的。当你的编码 Agent 完成一次改动后,它可以自主触发这个评审 Skill,而不是等你记得去跑脚本。这个“自主触发”很关键,它把评审从“事后补救”变成了“流程内建”。
另外,Skill 通常带有明确的输入输出契约和描述信息,Agent 能理解“这个 Skill 是干嘛的、什么时候该用”。这比一个裸脚本对 Agent 友好得多。我在实际搭建时,就是把它注册成一个带description和参数 schema 的 Skill,Agent 在准备提交前会主动调用。
3. 核心设计拆解:证据链是怎么一步步建起来的
3.1 输入层设计:光有 git diff 远远不够
Skill 的输入设计是整个方案的地基。我最终确定的输入包含四部分,缺一不可:
{ "diff": "<git diff 的完整输出>", "commit_messages": ["<本次分支的所有提交信息>"], "repo_context": { "changed_files": ["<变更文件列表>"], "related_files": ["<与变更文件有引用关系的文件>"], "project_conventions": "<项目约定,如代码规范、目录结构说明>" }, "task_intent": "<这次改动对应的需求或任务描述>" }为什么要commit_messages?因为提交信息里往往藏着意图。哪怕写得不规范,也能提供线索。为什么要related_files?因为这是补上下文的关键——通过静态分析找出引用了变更函数、或者被变更文件引用的文件,让评审 AI 能看到改动的影响面。
task_intent是最容易被忽略但最重要的一项。如果没有它,评审 AI 只能判断“代码本身有没有毛病”,无法判断“这个改动是不是解决了它该解决的问题”。我一般会把任务描述、验收标准一起塞进去。
提示:
related_files不要无脑全量塞进去,否则 token 爆炸且引入噪音。我的做法是只取一层引用关系,并且按引用次数排序取前 20 个。
3.2 分析层设计:把评审拆成并行的四条线
一次性让模型评审整个 diff,效果往往一般,因为任务太杂。我把它拆成四条独立的分析线,每条线专注一件事,最后再汇总:
- 变更意图线:把 diff 按逻辑块切分,为每个块标注意图,并和
task_intent做对齐,找出“改了但和任务无关”或“任务要求了但没改”的部分。 - 风险识别线:扫描高风险模式,比如 schema 变更、权限判断、异常吞掉、硬编码密钥、依赖版本变动。
- 测试覆盖线:检查变更是否伴随测试改动,测试是否覆盖了新增分支,没覆盖的部分标记出来。
- 一致性线:检查改动是否符合项目约定,命名、目录、错误处理风格是否统一。
这四条线可以并行跑,每条线用相对聚焦的提示词,输出质量比一次性评审高很多。这也是我踩过坑之后的调整——早期我图省事用一个大提示词,结果模型经常顾此失彼,风险识别做了但测试覆盖就敷衍了。
3.3 输出层设计:评审报告必须能被人快速扫读
输出格式我改了好几版,最终定成“结论先行 + 证据支撑”的结构:
## 评审结论 - 可合并性:建议合并 / 有条件合并 / 不建议合并 - 一句话理由:... ## 变更清单与意图 | 文件 | 变更块 | 意图 | 与任务对齐 | |------|--------|------|-----------| ## 风险点 | 风险等级 | 位置 | 描述 | 建议 | ## 测试覆盖 - 已覆盖:... - 未覆盖:... ## 待确认问题 - ...这个结构的好处是,评审者三十秒内能抓到重点,需要深挖时再往下看。可合并性这个字段是给决策用的,待确认问题是给作者回应的,分工明确。
4. 实操搭建:从零把这个 Skill 跑起来
4.1 环境准备与依赖清单
我用的技术栈比较朴素,核心就是 Python 加一个支持工具调用的模型接口。依赖清单如下:
pip install pygit2 # 用于解析 git 仓库和 diff pip install tree-sitter tree-sitter-languages # 用于静态分析引用关系 pip install pydantic # 用于定义输入输出 schemapygit2比直接调git命令更适合程序化处理,能拿到结构化的 diff 对象。tree-sitter用来做跨文件的引用分析,这是补上下文的关键工具。如果你不想引入 tree-sitter,退而求其次可以用正则做粗粒度匹配,但准确率会下降。
模型接口这块,我建议选支持长上下文和结构化输出的。因为 diff 加相关文件很容易超过几万 token,上下文窗口不够会截断,证据链就断了。
4.2 第一步:把 git diff 解析成结构化变更块
原始 diff 是文本,直接丢给模型不是不行,但模型容易在长 diff 里迷失。我先把 diff 解析成结构化的变更块:
import pygit2 def parse_diff(repo_path, base_ref, head_ref): repo = pygit2.Repository(repo_path) base = repo.revparse_single(base_ref) head = repo.revparse_single(head_ref) diff = repo.diff(base, head) changes = [] for patch in diff: file_path = patch.delta.new_file.path for hunk in patch.hunks: changes.append({ "file": file_path, "old_start": hunk.old_start, "new_start": hunk.new_start, "lines": hunk.lines, }) return changes解析成块之后,每个块可以单独分析意图,最后再拼起来。这样做还有个好处:可以按块计算风险,而不是笼统地给整个 diff 打分。
4.3 第二步:用 tree-sitter 找出相关文件
这一步是补上下文的核心。思路是:对每个变更文件,解析出它定义和引用的符号,然后在仓库里找引用这些符号的文件。
from tree_sitter_languages import get_parser def find_related_files(repo_path, changed_files, max_related=20): parser = get_parser("python") # 1. 提取变更文件中定义的函数/类名 defined_symbols = set() for f in changed_files: with open(f, "rb") as fp: tree = parser.parse(fp.read()) # 遍历 AST 收集函数和类定义名 # ...(省略遍历细节) # 2. 全仓库扫描,找引用这些符号的文件 related = {} for root, _, files in os.walk(repo_path): for name in files: if not name.endswith(".py"): continue path = os.path.join(root, name) if path in changed_files: continue with open(path, "rb") as fp: content = fp.read() for sym in defined_symbols: if sym.encode() in content: related[path] = related.get(path, 0) + 1 # 3. 按引用次数排序取前 N 个 sorted_related = sorted(related.items(), key=lambda x: -x[1]) return [p for p, _ in sorted_related[:max_related]]这段代码是简化版,实际用的时候要处理多语言、忽略测试文件和生成文件。但核心逻辑就是“找引用”,让评审 AI 能看到改动的影响面。
注意:相关文件不要全量塞进提示词,我一般只把每个相关文件里“引用了变更符号的那几行”截取出来,而不是整个文件。这样既保留了上下文,又控制了 token。
4.4 第三步:四条分析线的提示词设计
提示词是这个 Skill 的灵魂。我分享四条线的核心提示词骨架,你可以直接改。
变更意图线:
你是一个代码评审专家。下面是一次代码变更的 diff 和对应的任务描述。 请把 diff 按逻辑块切分,为每个块标注: 1. 这个块做了什么改动 2. 这个改动对应任务描述的哪一部分 3. 是否存在与任务无关的改动 任务描述:{task_intent} Diff:{diff}风险识别线:
你是一个安全与稳定性评审专家。请扫描下面的 diff,识别高风险改动。 重点关注:数据库 schema 变更、鉴权与权限逻辑、并发与锁、异常处理、 外部依赖升级、硬编码配置、日志泄露。 对每个风险点给出等级(高/中/低)、位置、描述和建议。 Diff:{diff} 相关上下文:{related_context}测试覆盖线:
下面是代码变更和对应的测试变更。请判断: 1. 新增或修改的逻辑是否有对应测试 2. 测试是否覆盖了新增的分支和边界 3. 哪些改动没有测试覆盖 代码变更:{diff} 测试变更:{test_diff}一致性线:
下面是代码变更和项目约定。请检查改动是否符合项目约定: 命名规范、目录结构、错误处理风格、日志格式。 指出不符合的地方并给出修改建议。 项目约定:{project_conventions} Diff:{diff}四条线跑完,把结果汇总成前面说的报告结构。汇总这一步我建议也交给模型做,但给它明确的模板,避免它自由发挥。
4.5 第四步:把 Skill 注册给 Agent
最后一步是让 Agent 能调用它。我用的是带工具调用能力的接口,把 Skill 描述成这样一个工具:
{ "name": "code_review_with_evidence", "description": "在提交代码合并请求前,对当前分支的改动进行带证据链的评审。返回结构化的评审报告,包含变更意图、风险点、测试覆盖和可合并性建议。当 Agent 完成代码修改、准备提交时应该调用此工具。", "parameters": { "type": "object", "properties": { "base_ref": {"type": "string", "description": "对比的基准分支,如 main"}, "head_ref": {"type": "string", "description": "当前分支"}, "task_intent": {"type": "string", "description": "本次改动对应的任务描述"} }, "required": ["base_ref", "head_ref", "task_intent"] } }description里我特意写了“当 Agent 完成代码修改、准备提交时应该调用”,这是给 Agent 的触发提示。实测下来,写清楚触发时机,Agent 自主调用的准确率会高很多。
5. 实测效果与踩坑记录
5.1 一个真实案例:它抓出了我漏掉的权限问题
上周我用这个 Skill 评审一次改动,任务是“给导出接口加一个时间范围过滤”。AI 写的代码逻辑没问题,测试也加了。但风险识别线报了一个中风险:导出接口的权限判断被挪到了过滤逻辑之后。
我一看,确实是。原来的代码是先判断用户有没有导出权限,再执行导出。AI 重构时把权限判断和过滤逻辑合并了,导致过滤参数非法时会先抛异常,异常信息里可能泄露了内部字段名。这个问题人眼扫 diff 很容易漏,因为逻辑“看起来”是对的。这就是证据链的价值——它不只看逻辑对不对,还看改动有没有破坏原有的安全边界。
5.2 踩过的坑:相关文件找太多反而降智
早期我为了“上下文充分”,把相关文件全量塞进去,结果评审质量反而下降。模型被大量无关代码干扰,开始提一些莫名其妙的建议,比如“这个工具函数应该加类型注解”,而那个函数根本没在这次改动里。
后来我改成只截取“引用了变更符号的那几行”,并且限制在 20 个文件以内,质量立刻回升。这印证了一个经验:上下文不是越多越好,而是要精准。给评审 AI 的上下文,应该像给人类评审者的“变更影响面说明”,而不是把整个仓库倒给它。
5.3 踩过的坑:意图对齐不能只靠提交信息
我一开始以为提交信息够用了,结果发现团队里提交信息写得五花八门,有的就一个“fix”,根本提取不出意图。后来我强制要求调用 Skill 时必须传task_intent,并且这个字段从任务系统里自动拉取,不靠人手写。这样意图对齐的准确率从大概六成提到了九成以上。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 评审报告空洞,只说“建议加测试” | 测试覆盖线提示词太泛 | 在提示词里要求指出具体未覆盖的分支 |
| 风险识别漏报 | 相关上下文不足 | 检查 related_files 是否包含调用方 |
| 报告太长没人看 | 输出没做结论先行 | 强制第一段输出可合并性和一句话理由 |
| Agent 不主动调用 Skill | description 没写触发时机 | 在 description 里明确“准备提交时调用” |
| token 超限被截断 | 相关文件全量塞入 | 只截取引用行,限制文件数量 |
6. 关于“敢不敢合并”这件事,我的一点个人体会
搭完这套东西之后,我最大的感受是:AI 写代码的能力已经足够强,真正拖后腿的是我们对它的信任机制。信任不是靠“我觉得它写得不错”建立的,而是靠一条条可查的证据建立的。这个 Skill 做的,就是把信任从感觉变成证据。
我现在合并 AI 写的代码,流程是这样的:Agent 改完代码,自己触发评审 Skill,生成报告;我看报告里的可合并性结论和风险点;如果是有条件合并,我就重点看那几个待确认问题;确认没问题再合并。整个过程比之前快,而且心里踏实。因为我知道,如果出了问题,我能顺着报告里的证据链回溯,找到是哪一环的判断出了偏差。
这个 Skill 后续还能扩展。比如把评审报告自动附到合并请求的描述里,让人类评审者也能看到;比如把历史评审结论存起来,形成项目的“评审知识库”,下次遇到类似改动可以直接参考。我还在试的一个方向是,让评审 Skill 和测试 Skill 联动——评审发现测试没覆盖的分支,自动触发测试生成。这些等跑顺了再单独写。
如果你也在用 AI 写代码,我建议你先别急着追求“生成得更快”,先把“合并得更稳”这件事解决掉。毕竟代码写出来是要上线的,敢合并,才是真的生产力。