ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Open-Code-Review实战:用CLI与LLM Agent自动化代码审查

Open-Code-Review实战:用CLI与LLM Agent自动化代码审查

1. 为什么我要自己搭一套 open-code-review 流程

团队里代码合并前的 review 环节,长期靠人肉盯。项目一多、提交一频繁,review 就变成走过场:要么拖到第二天才看,要么只扫一眼 diff 就点通过。我试过用现成的托管平台自带检查,规则死板,稍微复杂一点的逻辑问题根本发现不了。后来我把目光转向open-code-review这个思路——用 CLI 把 Git 的变更抓出来,交给 LLM Agent 做一轮结构化审查,再把结果回写到本地或流水线里。

这套东西解决的核心问题很明确:把重复、机械、容易漏的代码审查动作自动化,同时保留人对关键决策的最终判断权。它适合三类人:一是小团队里没有专职 review 角色的开发者;二是想给自己提交加一道自检关卡的个人开发者;三是已经在用 Git、想低成本引入 AI 辅助审查的工程团队。哪怕你之前只敲过git clone和git commit,跟着走也能搭起来。

我先把结论放前面:整套流程的关键不在于模型多强,而在于怎么把 diff 切得干净、怎么给 Agent 喂对上下文、怎么让输出可落地。下面按我实际搭建的顺序拆开讲。

2. 整体设计思路与方案选型

2.1 为什么是 CLI + LLM Agent 而不是纯平台方案

一开始我也想过直接用平台插件。问题是平台插件通常只能拿到仓库级别的信息,对本地未推送的改动、跨分支的临时对比、以及自定义的审查规则支持很弱。而CLI方案的好处是:它跑在开发者自己的机器上,能直接读工作区、暂存区、任意两个 commit 之间的差异,灵活性高一个量级。

再叠加LLM Agent,价值在于它能理解语义。传统的 lint 工具只能查格式和已知模式,比如变量未使用、括号不匹配。但像“这个函数在异常路径下没有释放资源”“这个循环边界可能差一”这类问题,规则引擎写起来极其痛苦,而 Agent 读一遍 diff 加周边代码就能给出提示。所以我的选型逻辑是:lint 管格式,Agent 管语义,CLI 管调度,三者各司其职。

至于为什么强调“open”,是因为我希望整个链路是可替换的:模型可以换、提示词可以改、输出格式可以自定义。闭源黑盒一旦不满足需求就只能等厂商更新,这在工程实践里太被动了。

2.2 核心组件拆解与职责边界

我把整套流程拆成四个组件,每个组件只干一件事:

组件职责常见实现
变更采集器从 Git 抓取待审查的 diffgit diff、git diff --cached、git diff A...B
上下文组装器把 diff 和周边代码、提交信息拼成提示词脚本 + 模板
审查 Agent调用 LLM 做语义分析并输出结构化结果任意支持函数调用的模型接口
结果处理器把结果写回终端、文件或流水线评论脚本 + 格式化

这里有个容易踩的坑:很多人一上来就把整个仓库塞给模型,结果 token 爆炸、成本失控、还容易让模型抓不住重点。我的做法是只送变更行加上下文窗口,比如每个改动块前后各取 20 行,这样既保留了语义环境,又把输入量压到可控范围。

2.3 与 Git 工作流的衔接方式

open-code-review 要真正用起来,必须嵌进现有的 Git 习惯里,而不是让开发者额外记一套命令。我设计了三种触发时机:

  • 提交前:用 Git 的钩子,在pre-commit阶段对暂存区做一次快速审查,只报严重问题,不阻塞提交。
  • 推送前:在pre-push阶段对本次要推送的 commit 范围做完整审查,输出报告。
  • 流水线中:在 CI 里对合并请求的 diff 做审查,结果作为评论回写。

这三种时机的审查深度和超时策略完全不同。提交前要快,控制在几秒内;推送前可以稍慢;流水线里则可以跑完整规则。这个分层设计是我踩过坑之后才定下来的——早期我把完整审查放在 pre-commit,结果每次提交都要等半分钟,团队直接把这个钩子禁用了。

3. 核心细节解析与实操要点

3.1 diff 采集:把变更切得干净是第一步

diff 采集看着简单,其实细节很多。最基础的三条命令你得先分清楚:

# 工作区与暂存区的差异(还没 add 的改动) git diff # 暂存区与最后一次提交的差异(已 add 未 commit) git diff --cached # 两个提交之间的差异(常用于分支对比) git diff main...feature

注意main...feature里的三个点,它表示“从共同祖先到 feature 的变更”,这才是 review 分支时真正想看的东西。如果用两个点main..feature,会把 main 上后来新增的提交也算进去,审查范围就错了。这个细节我在早期搞混过,导致 Agent 报了一堆跟本次改动无关的问题。

还有一个坑是重命名和移动。Git 默认能识别重命名,但 diff 输出里可能只显示少量改动行,Agent 会误以为这个文件被大改。我的处理办法是在采集时加上-M参数显式开启重命名检测,并在提示词里说明“本次 diff 包含重命名,请结合文件路径变化理解”。

git diff --cached -M --stat git diff --cached -M -U20

-U20是把上下文行数从默认的 3 行扩到 20 行。这个值我调过好几轮:3 行太少,Agent 看不到函数签名;50 行太多,token 浪费严重。20 行是个比较平衡的点,能覆盖大多数函数的头部信息。

3.2 上下文组装:给 Agent 喂对信息比喂多信息重要

组装提示词是整套流程里最考验经验的部分。我的模板结构是这样的:

你是一名资深代码审查者。请审查以下 Git 变更。 【提交信息】 {commit_message} 【变更文件列表】 {changed_files} 【变更内容】 {diff_content} 【审查要求】 1. 只针对变更行及其直接影响范围提出问题 2. 按严重程度分级:blocker / major / minor / nit 3. 每个问题给出文件、行号、原因、修改建议 4. 如果没有问题,明确说明“未发现需要修改的问题”

这里有几个关键设计。第一,明确要求分级,否则模型会把所有问题都写成“建议优化”,你根本没法判断优先级。第二,要求给出修改建议,只指出问题不给方案的审查等于把活又推回给人。第三,允许说没问题,这点很重要——如果不给这个出口,模型会为了“显得有用”而强行挑刺,制造大量噪音。

我还试过把整个文件的完整内容也塞进去,想让 Agent 有全局视野。实测下来效果并不好:token 消耗翻了好几倍,但问题发现率提升有限,反而因为信息过载导致模型注意力分散。后来改成只送变更块加固定上下文窗口,性价比最高。

3.3 审查规则的可配置化

不同项目对审查的容忍度完全不同。基础库可能要求零 nit,业务代码则更关注逻辑正确性。所以我把审查规则做成可配置的,放在仓库根目录的一个配置文件里:

review: severity_threshold: major max_issues: 20 rules: - name: error_handling enabled: true description: 检查异常路径是否被正确处理 - name: resource_leak enabled: true description: 检查文件、连接、锁是否在异常路径释放 - name: naming enabled: false description: 命名规范检查(交给 lint)

severity_threshold控制只报告某个级别以上的问题。max_issues防止模型输出过长,超过就截断并提示“问题过多,建议拆分提交”。这个上限很有必要,我有一次遇到一个巨型提交,Agent 输出了上百条问题,终端直接刷屏,根本没法看。

提示:规则描述要写得具体,不要写“检查代码质量”这种空话。模型对具体指令的响应远好于抽象指令。

3.4 输出格式与落地方式

审查结果如果只是一堆文字,用起来很痛苦。我要求 Agent 输出结构化数据,然后由脚本渲染成不同格式:

{ "summary": "本次变更共发现 3 个问题", "issues": [ { "file": "src/service.py", "line": 42, "severity": "major", "reason": "异常分支未释放已获取的锁", "suggestion": "在 except 块中调用 lock.release()" } ] }

有了结构化输出,就能做很多事:在终端里用颜色区分严重程度、在流水线里转成评论、在本地生成 Markdown 报告。我个人的习惯是本地跑的时候直接看彩色终端输出,流水线里则回写成评论,方便留痕。

4. 实操过程与核心环节实现

4.1 环境准备与 Git 基础配置

先把 Git 装好配好,这是所有后续操作的前提。Windows 上直接去官网下载安装包,一路默认即可,安装完在终端里验证:

git --version

能打印出版本号就说明装好了。接着配置身份信息,这两条是必须的:

git config --global user.name "你的名字" git config --global user.email "你的邮箱"

如果你用 Gitee 之类的平台,还需要配置密钥。生成密钥的命令是:

ssh-keygen -t ed25519 -C "你的邮箱"

一路回车,然后把公钥内容复制到平台的密钥设置里。这里有个细节:私钥文件绝对不能提交到仓库,我见过有人把.ssh目录整个 add 进去的,那等于把钥匙挂在门上。

4.2 搭建审查脚本的完整步骤

我用的是一段 Python 脚本做调度,核心逻辑分四步。第一步,采集 diff:

import subprocess def get_staged_diff(): result = subprocess.run( ["git", "diff", "--cached", "-M", "-U20"], capture_output=True, text=True ) return result.stdout

第二步,组装提示词。把 diff 和提交信息拼进模板:

def build_prompt(diff, commit_msg): template = open("prompt_template.txt").read() return template.format( commit_message=commit_msg, diff_content=diff )

第三步,调用模型接口。这里我用的是通用的 HTTP 调用方式,具体接口地址和参数按你选用的服务填:

import requests def call_agent(prompt): resp = requests.post( AGENT_ENDPOINT, json={"prompt": prompt, "temperature": 0.2}, headers={"Authorization": f"Bearer {API_KEY}"} ) return resp.json()["content"]

温度参数我设成 0.2,因为审查任务需要稳定和一致,不需要创造性。设太高会导致同一个 diff 每次审查结果都不一样,没法复现。

第四步,解析并渲染结果。把模型返回的 JSON 解析出来,按严重程度排序后打印:

def render(issues): order = {"blocker": 0, "major": 1, "minor": 2, "nit": 3} issues.sort(key=lambda x: order.get(x["severity"], 9)) for issue in issues: print(f"[{issue['severity']}] {issue['file']}:{issue['line']}") print(f" 原因: {issue['reason']}") print(f" 建议: {issue['suggestion']}")

4.3 接入 Git 钩子的具体配置

脚本写好后,挂到 Git 钩子上。在.git/hooks/目录下创建pre-commit文件(没有扩展名),内容如下:

#!/bin/sh python3 path/to/review_script.py --mode staged

然后给它执行权限:

chmod +x .git/hooks/pre-commit

这里有个团队协作的坑:.git/hooks/目录不会被提交,每个人都要手动配一遍。我的解决办法是把钩子脚本放在仓库里一个hooks/目录,然后写一个安装脚本,新成员克隆后跑一次就配好了。或者用git config core.hooksPath hooks把钩子目录指到仓库内,这样就能跟着版本走。

注意:pre-commit 钩子里不要跑耗时超过 5 秒的审查,否则会严重影响提交体验。我的做法是 pre-commit 只跑 blocker 级别的快速检查,完整审查放到 pre-push。

4.4 参数调优的实际记录

我拿一个真实的中型项目做了几轮参数对比,记录如下:

上下文行数平均 token 消耗问题发现数误报数
3120041
10210061
20340082
50780084

可以看到,上下文从 20 行加到 50 行,问题发现数没有提升,但误报翻倍、token 翻倍。所以 20 行是我实测下来的甜点值。误报主要来自模型看到了太多无关代码,开始对没改动的地方指手画脚。

另一个参数是max_issues。我一开始不设上限,结果遇到大提交时输出几百条,其中大量是重复的同类问题。后来设成 20,并在提示词里加一句“如果同类问题超过 3 处,合并为一条并说明涉及范围”,输出质量明显提升。

5. 常见问题与排查技巧实录

5.1 模型输出格式不稳定怎么办

最常见的问题是模型不按 JSON 格式输出,夹杂一堆解释性文字,导致解析失败。我的处理分三层:第一层,在提示词里明确“只输出 JSON,不要任何额外文字”;第二层,解析时用正则先提取 JSON 块,容忍前后有杂音;第三层,如果还是失败,把原始输出存到日志文件,人工看一眼再调整提示词。

实测下来,光靠第一层就能解决 80% 的问题。剩下 20% 通常是 diff 太长导致模型“忘了”格式要求,这时候缩短输入或拆分提交就能解决。

5.2 审查结果误报太多怎么收敛

误报是劝退团队的最大杀手。我总结了几个收敛手段:

  • 提高严重程度阈值:只报 major 以上,nit 直接过滤掉。
  • 在提示词里加负面示例:明确告诉模型“不要报告纯格式问题、不要报告命名风格问题,这些交给 lint”。
  • 按文件类型过滤:自动生成的代码、第三方库、测试快照文件直接跳过审查。

我踩过最深的一个坑是:早期没排除自动生成的文件,Agent 对着一个几千行的生成文件报了几十条“建议重构”,团队看到报告直接对这套流程失去信任。后来加了文件白名单和黑名单,问题立刻消失。

5.3 大提交导致超时或截断

一个提交改了 50 个文件、几千行,直接送进去必然超时或截断。我的策略是按文件拆分审查:把 diff 按文件切块,逐块送审,最后合并结果。这样单次输入可控,还能并行处理加速。

但拆分也有代价:跨文件的关联问题可能漏掉。比如 A 文件改了函数签名,B 文件改了调用处,拆开看各自都没问题,合起来才发现参数不匹配。所以我的做法是:先做一次全局的变更文件列表分析,再做逐文件细审。全局那一步只送文件名和改动行数,让 Agent 判断有没有跨文件影响,有的话再针对性送相关文件。

5.4 常见问题速查表

现象可能原因解决方向
解析 JSON 失败模型输出夹杂文字强化提示词,加正则提取
审查超时单次输入过大按文件拆分,设 max_issues
误报泛滥规则太宽、上下文过多提高阈值,加负面示例
钩子不生效权限或路径问题检查执行权限和 hooksPath
结果每次不同温度参数过高降到 0.2 以下
漏掉跨文件问题拆分审查增加全局预分析步骤

5.5 几个我踩过的独家坑

第一个坑:别在钩子里调用需要联网的接口而不做超时处理。有一次网络抖动,pre-commit 卡了整整两分钟,我差点以为 Git 坏了。后来所有网络调用都加了 10 秒超时,超时就跳过审查并提示“审查服务不可用,已跳过”。

第二个坑:API 密钥不要硬编码在脚本里。我早期图省事直接写在代码里,结果差点跟着仓库提交上去。现在统一用环境变量读取,脚本里只留os.environ.get("REVIEW_API_KEY")。

第三个坑:审查结果要能一键忽略。有些问题开发者判断后认为不用改,但每次提交都重复报。我加了一个忽略机制:在代码行尾加// review-ignore注释,脚本采集时自动跳过这些行。这个小功能极大提升了团队接受度。

6. 让这套流程真正跑起来的几个经验

搭起来容易,让它长期活下去难。我最大的体会是:审查流程的价值不在于发现多少问题,而在于不制造额外负担。如果每次提交都要等、每次报告都一堆噪音、每个问题都要手动确认,团队很快就会绕过它。

所以我的原则是宁可少报,不可误报。blocker 级别的问题必须准,major 级别可以稍宽,minor 和 nit 默认关闭。让开发者先信任这套流程,再逐步放开规则,比一上来就全量审查效果好得多。

另外,Agent 的审查结果永远只是建议,最终判断权必须留给人。我在报告开头会明确写一句“以下为自动审查建议,请结合业务上下文判断”。这既是技术上的谨慎,也是让团队接受 AI 辅助审查的心理前提。

后续如果还想扩展,可以考虑把审查历史存下来,做趋势分析——比如某个文件反复出问题,说明它可能需要重构;某类问题高频出现,说明团队需要补充规范文档。这些数据积累起来,比单次审查本身更有价值。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进