很久以前,代码评审还是一件挺有仪式感的事,评审人坐下来,逐行看diff,在评论区里逐条讨论。但迭代一快起来,需求一变多,PR堆积如山,评审就慢慢变成了流程里的一个负担——要么直接点个批准按钮了事,要么等评审人空闲时再来翻,一翻就是几百行改动,关键问题反而被淹没了。
后来我做了一个小项目,内部代号叫 open-code-review,本质上是一套自动化的代码评审工作流:每当开发者推送新的提交或创建合并请求时,系统自动拉取变更代码,交给语言模型进行审查,再把审查结果以评论的形式贴回合并请求页面。听起来不复杂,但真正落地时,从请求触发、代码差异提取、上下文组装,到提示词设计、评论格式规范、重复评论去重,每一步都有不少坑。
这篇文章就把我搭建这套系统时踩过的坑、最终采用的实现方案、以及一批可以直接抄走的配置和代码,完整地拆开讲一遍。如果你也在考虑给自己团队搭一套“不知疲倦的评审助手”,或者单纯对这项工作流感兴趣,那这篇实操记录应该能帮你省下不少走弯路的时间。
1. 项目思路与设计拆解
1.1 从哪里发现问题:人工评审的四个痛点
先说结论:这套工具不是要替代人工评审,而是要先把人工评审从“机械劳动”里解放出来。
我自己经历过一个阶段,团队从两三个人逐渐扩充到十几个开发,合并请求从每天几个涨到每天几十个。早期评审还能认真看完每行改动,后来就逐步变形了:简单逻辑错误和规范问题占了大头,但评审人却要花同样多的时间去读那些明显可以机器判断的代码。再往后,评审排队时间越来越长,代码合并速度被迫放慢,而代码风格不统一、死代码、潜在空指针这类基础问题反倒在最后时刻才被翻出来。
传统静态检查工具(比如各类Linter)能解决一部分规范问题,但它们只能做规则匹配,比如某个函数名单词拼错、某个文件缺少版权声明、某类错误没有捕获。它们理解不了代码的业务含义:一个异步请求后面接了一个同步阻塞操作,它不会告诉你这会造成主线程卡顿;一个多处重复的相似函数,它也不会提醒你“是不是该抽象一个公共模块出来”。
LLM代码评审正好补上了这一层:它能基于上下文理解代码意图,在“业务逻辑风险”和“代码规范问题”之间找到平衡。同时它能生产自然语言描述,告诉开发者“为什么这里要注意”“潜在影响是什么”,而不是冷冰冰地抛出一个错误码。
1.2 核心需求拆解:一个评审机器人真正要干什么
在做 open-code-review 之前,我先自己列了一份需求清单,明确这个项目的最简可用形态是什么样的:
- 它能监听代码仓库的事件,尤其是新PR、新提交这类触发点。
- 它能获取本次变更的diff信息,并且是“可读取的、结构化”的diff。
- 它能把这些变更交给模型,用一套稳定的提示词引导模型给出评审意见。
- 它能把这些意见写回仓库对应的讨论区,并且要被开发者看得懂、易回复。
- 它要保留“人在回路”的入口——机器评审意见只是参考,最终代码是否合并还是由人决定。
这套思路看起来简单,但实际操作中,“获取diff”这一步就会劝退很多人:很多仓库托管平台对diff有尺寸限制,大PR可能直接截断;多个commit的改动需要聚合、去重;二进制文件、锁文件、自动生成文件应该被过滤掉,否则模型会被垃圾输入干扰,评审质量直接下降。
1.3 为什么选择“平台事件触发 + 服务端分析 + 评论回帖”的结构
我当时也考虑过两种替代方案:
一是开发一个本地命令行工具,开发者自己把代码diff导出来,手动发送给模型看,再手动把结果粘贴回评论区。这种方式灵活,但不可持续——如果使用的人多了,平台怎么记录评审历史?每个人自己的API密钥怎么管理?这会把辅助工具硬生生做成一个运营负担。
二是做一个完整的独立评审平台,把仓库镜像过来,重做一套界面和流程。这种方式治理能力最强,但开发成本高,而且会割裂开发者的工作流。
最终选择了“平台事件触发 + 服务端分析 + 评论回帖”的组合方式。如果以GitHub为例,就是webhook(或项目管理用途的Pull Request事件)触发服务端函数,服务端调用模型分析diff,再调用接口回写评论。结构干净,开发量可控,又能自然融入团队已有的评审流程。
2. 基础设施与技术选型
2.1 托管环境的选择:从零部署到Serverless
对于这种轻到中量的自动化任务,我建议优先考虑无服务器函数或容器化任务,不要把整套服务常驻挂一台云主机上。原因很简单:事件触发是低频的,你不需要让一个进程24小时等着,按调用次数付费要比按小时付费划算得多。
我当时选用了一个FaaS平台,配置了一个HTTP触发器。代码仓库通过webhook把事件消息推送到这个HTTP接口,函数被临时拉起,处理完一件事就销毁。冷启动耗时也在可接受范围内,一般几百毫秒。
有一点需要提醒:如果你的仓库流量很大,合并请求频繁更新,同一个PR可能触发多次评审,因此服务端必须做幂等控制。简单做法是在函数入口判断这个PR的某个版本是否已经被评审过,如果评过了,直接返回旧的评审结果,而不是每次重新调用模型。
2.2 开发语言与依赖:Python的取舍
本项目我选了Python,没有选Node.js或Go。没有特别高深的原因,主要是生态里处理Git、处理HTTP请求、处理JSON的库都很成熟,而且如果要实验不同的提示词策略,Python脚本迭代起来更快。代价是启动慢一点、内存占用大一点,但这些对低频率的评审任务来说完全不是问题。
关键依赖就三个:
requests:发送HTTP请求,调用webhook或API;PyGithub(或你对应平台的SDK):操作PR的评论、获取diff内容;anthropic或openaiSDK:调用模型推理接口。
其实逻辑核心不在这些库,而在于你怎么设计提示词和过滤规则。这两个做好了,哪怕只用原生HTTP调用也能跑通。
2.3 模型选择:效果、成本、速度怎么权衡
我试过几类模型,总结出一个经验:代码评审对模型的“上下文长度”要求远高于“算术推理能力”。日常开发中的多数问题,比如空指针风险、错误处理缺失、资源泄漏、接口用法错误,都藏在上下文里,模型要能同时看到变更部分和周边相关函数才能得出准确判断。
所以,如果你的预算有限,不要选超大参数量的通用旗舰模型,而是用专为代码任务优化、上下文窗口足够大的中端模型。预算充足的话,可以设计成“两阶段评审”:先用轻量模型做格式和规范扫描,再用更强的模型做逻辑和架构层面的深度分析,两层结果合并输出。
关于API成本,我算过一笔账:一份200行变动的diff,拆分后大约5到10个分块,每个分块调用一次模型接口,大约消耗3000到8000个token。如果每天处理30个PR,折算下来一天几十块人民币左右。但你要注意,这是理想情况,如果提示词设计粗糙,把几万token一次性全塞给模型,成本会翻上好几倍。
2.4 提示词设计:这是整个项目最大的变量
提示词决定了评审意见是“有用的建议”还是“正确的废话”。我在《仓库级AI代码评审系统》那篇文章里详细讲过一些策略,这里提炼出我认为最经典的四个原则:
- 给模型设定角色和背景。不要只说“你是代码评审专家”,而是要说清楚你的团队用什么语言、什么框架、这次变更属于哪个模块、潜在风险区域在哪。
- 明确输出格式。我用JSON结构输出,字段包括“严重级别”“文件路径”“行号”“问题描述”“改进建议”。这比让模型输出大段自然语言更好解析,可以直接转成评论表格或逐行评论。
- 给模型提供“逆向示例”。就是告诉它“如果发现这种情况,不要说”。例如:“不要对没有完整上下文的片段做重构建议”“不要重复静态检查工具已能发现的问题”。
- 限定关注范围。一次评审不要同时追求“架构级评审”和“编码风格评审”,两者关注层级差太多,混在一起,模型会顾此失彼。
2.5 尝试过但放弃的方案:为什么不直接用Copilot的自动评审
市面其实已经有自动Pull Request评审产品,我最初也想直接引入。但用了之后发现两个问题:第一,这类产品通常是黑盒策略,我看不到判断依据,也没法在提示词层面做定向优化;第二,它的反馈多是结论式的,缺少可操作的改进路径,很多情况下只给出一句“此文件存在潜在性能问题”,却不说具体位置和原因。
自建 open-code-review 很大程度上就是为了获得“可控性”:由我自己决定模型看什么、不看什么;我可以推入团队特有的规范文档;我可以调整输出格式以对接我的评论工具。为此我多写了大概500行代码,但从长期维护来看,这个投入是值得的。
3. 核心模块与实现细节
3.1 PR事件监听与响应链路
整个工作流从一次Git操作开始。以合并请求场景为例:开发者推送分支,然后创建PR。仓库平台把PR发生的事件(触发点有 opened、synchronize、reopened等)以webhook形式通知给服务端。
我在服务端做了一份事件过滤器,只处理被“激活”的事件。对同一个PR,我按 head_sha(当前提交SHA)做了评审记录。如果再一次收到synchronize事件,并且head_sha和上次评审过的SHA一致,直接跳过。
这意味着一个完整的响应链路是:
- 接收webhook请求;
- 验证请求签名(防止伪造);
- 解析事件类型与PR元数据;
- 检查该PR的当前版本是否评审过;
- 拉取diff,执行评审逻辑;
- 回写评审结果;
- 记录评审版本。
3.2 Diff获取与分块策略
Diff获取是第一个大坑。直接用平台的API拉取一个大型PR的diff,大概率会得到一个超级长的文本,模型很快就糊了。我最终的做法是分三步:
第一步,拉取基础信息,包括变更文件列表、每个文件的增删行数。这一步用API的参数可以直接拿到。
第二步,过滤不需要评审的文件。你需要建立一个关键词黑名单:锁文件(package-lock.json、poetry.lock等)、编译产物(dist、build、out目录)、自动生成文件、图片或二进制文件、以及单纯重命名但无内容变更的文件。这些文件如果进入提示词,除了浪费token,还会干扰模型对主要逻辑的判断。
第三步,将剩余的diff按文件、按变更块(hunk)拆分成一组有序的小块。每个小块最多能容纳 30~40 行的上下文,超出部分再切。这样每个提示词都对应一个小块,模型能集中精力处理局部问题。
有一点要注意:拆分之后,要尽力保留“上下文中的函数签名和全局符号”。只给纯增量代码,模型根本不知道这个函数是干嘛的。我的做法是给每个分块的头部附一段简短的“文件定义信息”,比如这个文件是什么模块、有哪些关键函数。
3.3 评审提示词模板:一个可以直接复用的版本
我的默认为多格式JSON输出提示词设计如下(以Python项目为例,MINI版,工程版还需增加项目规范约束):
你是一名高级Python代码评审专家,被要求审查一段代码变更。 背景信息: - 仓库模块:用户认证模块 - 变更类型:新增登录接口 - 主要风险点:并发安全、token生成、异常处理 请分析以下diff片段: {diff_chunk} 评审要求: 1. 关注逻辑错误、资源泄漏、并发安全、异常处理、安全漏洞 2. 不要建议引入新依赖,除非确实必要 3. 不要重复静态检查工具能检测出的格式问题 4. 所有建议必须基于当前diff,不做无依据的重构建议 输出严格按以下JSON格式返回: {{ "review_comment": "总体的简要说明,不超过50字", "issues": [ {{ "severity": "critical|warning|suggestion", "file": "文件路径", "line_start": 20, "line_end": 24, "issue_type": "bug/security/performance/style", "description": "问题描述及发生原因", "suggestion": "具体改进建议,尽量给出代码示意" }} ] }} 不要输出JSON以外的任何内容。把这个模板发给模型之后,得到的输出再经过程序解析,把JSON里的issues字段按严重级别排序,再组装成一条Markdown评论。
另外,我一般会加一条规则,告诉模型:如果所有问题都低于“warning”级别,建议只输出“review_comment”,不输出issues数组。这能有效压制无效评论的数量。模型发太多“建议提取一个常量”之类的废话,会严重损伤开发者对工具的信任。
3.4 评审结果回写与展示优化
回写评论不是简单地把模型结果贴上去。我仔细研究过平台评论区在不同情况下的展示效果:如果一次贴出几十条问题,开发者很可能直接忽略;如果评论结构清晰、按文件分组、按严重级别排序,效果就完全不同。
我的评论模板如下:
## 自动化评审报告(commit xxx) - 整体结论:待改进 - 问题数量:4 (critical: 1 / warning: 2 / suggestion: 1) ### 文件:auth_service.py **critical** 行 56: 存在SQL注入风险 使用拼接字符串的方式处理用户输入,建议改为参数化查询。 **warning** 行 78: 异常被静默吞掉 捕获异常后没有处理,建议记录日志并给出提示。 --- *由 open-code-review 自动生成,内容仅供参考,请以人工评审结论为准。*这个结构在网页上渲染出来非常干净,开发者在评论区还能直接用回复功能和机器人对话,比如回复“忽略这条警告”,后续机器人就知道该屏蔽此类问题。
3.5 核心代码演示
我摘录了最核心的一段代码,做成了“最小可运行版”逻辑框架,你可以直接据此改造:
import hmac import json import hashlib from typing import Dict, Any def verify_signature(payload_body: bytes, secret: str, signature_header: str) -> bool: """校验 webhook 签名,防止伪造请求。""" if not signature_header: return False expected = hmac.new( secret.encode(), payload_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(f"sha256={expected}", signature_header) def load_reviewed_versions(redis_client, pr_id: str) -> str: """读取该PR最近一次评审的提交SHA。""" return redis_client.get(f"review:{pr_id}") def save_reviewed_version(redis_client, pr_id: str, sha: str) -> None: """记录本次评审的提交SHA。""" redis_client.set(f"review:{pr_id}", sha, ex=60 * 60 * 24 * 7) def filter_diff_files(changed_files: list) -> list: """过滤掉不需要评审的文件类型。""" ignore_patterns = ["package-lock.json", "poetry.lock", "*.min.js", "*.map", "dist/", "build/"] result = [] for f in changed_files: filename = f["filename"] if any(filename.endswith(pat) or filename.startswith(pat) for pat in ignore_patterns): continue result.append(f) return result def chunk_diff(diff_text: str, max_lines: int = 40) -> list: """将大段diff按文件按块拆分。""" chunks = [] current_lines = [] current_count = 0 for line in diff_text.splitlines(): current_lines.append(line) if line.startswith("@@") or current_count >= max_lines: chunks.append("\n".join(current_lines)) current_lines = [] current_count = 0 current_count += 1 if current_lines: chunks.append("\n".join(current_lines)) return chunks def build_prompt(diff_chunk: str, module_context: str) -> str: """构造发送给模型的评审提示词。""" return f""" 你是高级代码评审专家,上下文为空模块下的代码评审。 模块背景:{module_context} diff代码片段如下: {diff_chunk} 请按既定JSON格式输出评审意见。 """上面这段代码只是一个骨干。实际项目中,towards调用模型那一步,还需要处理超时、重试和JOSN解析失败的情况,以及评论内容超过平台长度限制需要截断的情况。
3.6 评审报告的聚合与通知
评审完成之后,要不要主动推送给开发者?我的建议是要,但别太吵。在PR流程里,评论本身就构成了通知。但如果团队里有人不常看评论,也可以考虑把概要发到即时通讯工具的机器人接口上。
具体做法是:先在PR页面贴完整评论,然后向即时通讯群推送一条摘要,包含PR编号、变更行数、严重问题数量、以及问题最多的事件频次最高的文件名。摘要里不展示具体内容,只提供一个直达链接,开发者在群内点击后就能进入PR页查看。这样既能保持群信息干净,又能保证重要问题不被错过。
我曾经做过一轮测试,群内只推摘要、不推全文之后,大家对评审报告的查看率反而提升了不少,因为一眼扫过去就能判断这个PR是不是需要自己立即关注。
4. 实操过程中遇到的高频问题与排查方法
4.1 模型总是给出“正确的废话”
最典型的一种结果:模型说“这段代码可能存在问题,需要进一步关注”,但具体是什么问题、怎么改,全都没有讲。这种意见等于没给。
我的应对方法是两条:
一是在系统提示词里明确禁止:不得输出无法定位到具体行号或函数名的问题。必须给出建议代码。举例说明,告诉模型“如果你不能给出修改后的代码示意,那说明你并没有理解这个问题,就不要输出”。
二是从输出解析端做二次过滤:对返回的JSON,检查每条issue的description是否包含具体变量名或函数名,如果一条都没有,直接丢弃并记录。用这个方法,废意见率能显著下降。
4.2 并发请求导致模型限流与超时
当同时收到好几个PR事件时,如果代码使用同步方式逐一调用模型,整体的处理时间就会拉得特别长。平台那边可能直接判定超时,把请求断掉。
解决方案是引入并发控制和重试机制。我的做法是:
- 用线程池限制并发数,默认上限3。
- 每个请求设置合理的超时时间,比如60秒。
- 对瞬时限流错误(429)做指数退避重试,最多重试3次,退避间隔设为2秒、4秒、8秒。
- 如果某一块diff经过全部重试仍然失败,就在最终报告中标记“该文件评审超时”,不要以空结果代替。
这里特别要说明:重试操作要小批次、勤探测,不要直接把并发数开到10以上。代码评审调用的是模型API,它本身的吞吐是有限制的,你并行推太猛,反而把整个队列堵死。
4.3 Diff太大,超过单次提交上限
有些开发者一个PR能改几十个文件、几千行代码。无论你按文件拆分还是按块拆分,总量都很大。这种情况下,一次性把所有块送进模型,肯定会被平台的上下文窗口限制拦下来。
我的建议是分级处理,而不是硬塞:
- 第一优先级处理:源码目录下的核心模块、涉及安全认证的部分、数据库连接相关、配置变更。
- 第二优先级处理:工具类文件、测试文件。
- 第三优先级处理:文档、示例代码,通常一带而过。
同时,要对每个变更块做个“信息量估算”:假如一个块只改了缩进或换行,没有实质性的逻辑变化,直接过滤掉不评审。
4.4 同一PR被重复评审
由于开发者会多次推送提交到同一分支,webhook事件会频繁触发,如果每次都重新整段评审,不仅烧钱,评论还会刷屏。
这个问题的根治方案是“按提交版本去重”。我把每次评审对应的head_sha存在缓存里,下次事件进来先比对。同时我还做了一个额外的措施:只对“最后一次提交的那次变更内容”做增量评审,而不是每次从基础分支开始重新整体评审。
有人会问:那历史提交中出现的问题不就漏掉了吗?
确实会漏。所以正确的节奏是:当开发者第一次提PR时,做一次全量评审;之后每次提交,只评增量部分和它可能影响到的周边函数。全量评审的结果存在报告文档里,持续更新,这样就不会漏。
4.5 评论被平台判定为垃圾信息
这个是真真切切遇到过的问题。如果用同一个机器人账号短时间内给几十个PR贴评论,某些托管平台会触发反垃圾频率限制,评论发不出去。
解决方案是拉长提交间隔,把要发表的评论先排进一个队列,每10秒处理一条。另外,每行评论的内容也不要全相同,最好加上本次PR编号的hash值作为水印,避免平台判定为大量重复内容。
4.6 模型对旧代码的误报
如果一个大型项目已经存在了五年,里面有很多历史遗留代码。当变更diff恰好触碰这些旧代码时,模型很容易因为看到了不规范的写法而给出“大规模重构”的建议。这类建议实际上不可执行,因为改动面太大,影响范围不可控。
我加了一条硬性规则:模型只评审本次变更涉及的行,对于未变更的上下文代码,只作为背景参考,不做评价。同时,在提示词里要求模型给建议时要给出“影响范围”字段,如果“影响范围”包含超过三个函数或两个文件,就列为“建议类”而不是必须执行的问题。
5. 进阶扩展与应用场景思考
5.1 支持多仓库、多分支策略
当团队有多个仓库、不同分支有不同保护策略时,不能一个机器人打天下。我在设计时用一份配置文件来描述规则:
- repo: backend-service branches: [main, release/*] ignore_files: ["*.md", "migrations/*"] model: medium comment_tag: "open-code-review" - repo: frontend-app branches: [develop] ignore_files: ["package-lock.json", "dist/*"] model: fast comment_tag: "open-code-review-frontend"这份配置的好处是,你可以针对不同仓库的代码风格调整提示词和模型,避免在某一个仓库里堆满噪音。同时还能在政策上切换:“后端仓库的评审报告必须由模型给出,任何warning级别都要阻塞合并;前端仓库的警告只是建议。”
5.2 与CI流水线的联动
如果只是贴评论,工具影响力是有上限的。我发现最有价值的用法是把评审结果作为CI的一个“乐观检查门禁”:如果评审报告存在critical级别问题,CI直接失败;如果只是warning或suggestion,CI仍然通过,但会把报告链接附在流水线结果里。
这种设计虽然名为“门禁”,但不僵硬:开发者在紧急修复线上问题时,可以手动输入特定指令强制绕过。毕竟机器评审永远只是辅助,把你监督权交还给人,这工具才能真正活在一个团队里。
5.3 使用本地模型作为降级方案
我一直认为云端的模型API是大多数团队的默认选择,但有些团队对代码隐私要求极高,不适合把代码送到第三方平台。这时一个可行的方案是部署一个本地模型。
本地模型对机器配置要求高,起始用7B级别模型做常识性的规范评审,再用130B级别模型做业务逻辑深度分析。需要说明的是,同一个提示词在本地模型输出稳定性上往往不如云端API,所以解析层要更温和:把格式不合格的输出靠正则做一次硬解析,尽量提取出文件名和行号。
5.4 生成评审结果汇总与团队度量报告
最后一件我觉得非常有价值的事,是每周生成一份“评审报告”。自动统计每个仓库的PR数量、模型评审发现的问题数量、低严重级别问题的重复出现频率、平均修复周期等。这些数据可以直接沉淀为团队技术债务的度量输入。
不过有一点我要提醒:千万别把“模型发现问题数量”直接当成团队代码质量的KPI,否则开发者会把大量精力花在迎合模型的提示词偏好上,完全偏离初衷。更合理的做法是拿这些数据做“趋势观察”——例如某个模块连续三周都在出现同类空指针问题,那就应该有人去复盘设计,而不是单纯地去“多写几个if条件”。
6. 一些想对后来者说的经验话
open-code-review从最早的一个几百行脚本,演化到后来能服务多个仓库的自动化服务,过程中最深刻的体会是:这类工具成败的关键不在模型本身,而在“噪声控制”。一个每周只提三个精准问题的机器人,远比一个每行代码都评论的机器人受欢迎且有效。
普通人拿到模型API,第一反应是让它“多看看、多说说”;但做代码评审恰恰相反,你要用提示词和过滤规则限制它“只看该看的、只说有用的”。每一次评审意见都消耗开发者的注意力,一次错误的提醒,需要人工真正去看代码、理解上下文后才能判定它不对,这个成本比机器生成意见的成本高得多。
如果你也想在自己的仓库中跑起类似的开源小工具,我建议你别一上来就追求完整功能。先满足一个最简流程:一个webhook入口、一个模型调用函数、一个评论回贴方法,让它对某一个测试仓库生效。运行两周,看看模型给出的意见分布,再用两周时间去打磨提示词和评审规则。当这个循环稳定之后,再考虑扩展到多仓库、门禁联动这些进阶能力。
最后再分享一个思路:工具本身只是代码,真正让它持续发挥效益的是“反馈闭环”。要持续挖掘那些被开发者忽略的低质量评论,定期把新的案例补充进提示词中。这个习惯比写任何完美代码都更重要。我自己就是靠着这份维护,让一个本来可能被丢进垃圾桶的自动化小项目,成了团队每天都在使用的依赖工具。