1. 为什么我要自己搭一套 open-code-review
团队里代码评审这件事,说多了都是泪。人少的时候靠自觉,人多的时候靠吼,PR 堆到十几个没人看是常态,等到合并上线出了事故再回头翻 diff,那种感觉就像考试交卷后才发现答题卡涂错行。我所在的团队大概二十来人,后端前端加起来每天能开出十五到二十个合并请求,靠两三个资深同学轮流盯,根本盯不过来。更麻烦的是评审质量参差不齐:有人只看变量命名,有人只关心有没有死循环,安全漏洞、边界条件、异常吞掉这类问题经常漏过去。
open-code-review这个项目,就是在这种背景下我自己动手攒出来的一套命令行代码评审工具。它的核心思路很直接:把 Git 仓库里的变更内容抓出来,交给一个 LLM Agent 去分析,然后把评审意见按文件、按行号结构化输出,既能直接在终端看,也能落成 Markdown 报告贴到 PR 评论里。整套东西跑在 CLI 里,不依赖任何特定平台的网页界面,本地、CI、服务器上都能用。
它解决的问题可以拆成三层。第一层是覆盖率,机器不会累,每个 PR 都能过一遍,不会因为“今天太忙”就跳过。第二层是一致性,同一套评审规则对所有人一视同仁,不会因为跟谁熟就放水。第三层是可追溯,每次评审的输入、输出、用的模型、耗时都留痕,出了问题能复盘。
适合谁来参考?我觉得三类人最合适。一是中小团队里负责工程效能的同学,想低成本搞一套自动化评审;二是对 LLM Agent 感兴趣、想找个真实场景练手的开发者;三是已经在用各种 CLI 工具、想把它接进自己工作流的极客。哪怕你只是想搞清楚agent、llm、embedding这几个词到底啥区别,跟着这套东西走一遍也能明白个七七八八。
2. 整体设计与技术选型拆解
2.1 为什么是 CLI 而不是网页服务
一开始我也想过做个网页版,前端传 diff、后端调模型、页面展示结果。但真动手前我列了一下使用场景,发现网页版全是坑。第一,评审这件事天然发生在开发者的终端里,写完代码git commit完顺手就想跑一下,切到浏览器登录再粘贴 diff,这个动作链路太长了,没人愿意干。第二,CI 环境里根本没有浏览器,你要在流水线里做卡点评审,只能走命令行。第三,网页服务要考虑部署、鉴权、并发、存储,一套下来运维成本比工具本身还高。
CLI 的好处是组合性。它可以被 shell 脚本调用,可以被 Git hook 触发,可以被 CI 的 step 直接执行,输出还能管道给grep、jq做二次处理。我实测下来,一个open-code-review --staged命令跑完,结果直接打到终端,比任何网页都顺手。这也是为什么现在codex cli、claude cli、trae cli这类工具越来越多,CLI 才是开发者的主战场。
2.2 LLM Agent 和普通 LLM 调用的区别
这里得先把概念理清楚,因为热词里问“agent 和 llm 和 ai 模型有什么区别”的人特别多。简单说,LLM 是模型本身,比如 DeepSeek、GPT 系列、Claude 系列,它们本质是“输入文本、输出文本”的函数。你给它一段 diff,它给你一段评论,这是一次普通的 LLM 调用。
Agent 是在 LLM 外面套了一层循环和工具。它不只是问一次答一次,而是能自己决定“我要先读哪个文件”“我要不要再跑一次 grep 确认这个函数在哪被调用”“我发现信息不够,再调一次模型补充”。Agent 有目标、有记忆、有工具调用能力。open-code-review里我用的是轻量 Agent 模式:先让模型做一轮整体扫描,识别出高风险文件,再针对这些文件做第二轮深度分析,两轮之间把上下文拼起来。这比单次调用准确率高不少,代价是 token 消耗翻倍,所以我在配置里做了开关,小 PR 走单轮,大 PR 走双轮。
至于embedding,它是把文本转成向量的技术,主要用来做相似度检索。比如我想让评审工具参考团队历史评审意见,就可以把过去的评论做 embedding 存起来,新 PR 来了先检索相似的几条作为 few-shot 示例。这个我目前没上,因为维护成本高,但架构上留了口子。
2.3 模型选型的取舍
模型这块我踩过不少坑。最早用的是某海外大模型,效果确实好,但网络和费用都是问题,团队里有人跑一次评审要等半分钟。后来换成国产模型,DeepSeek 系列在代码理解上表现相当能打,尤其是 diff 这种结构化文本,它对上下文的理解比我想象中好。我做过一轮对比测试,拿同一个 PR 分别喂给三个模型,让它们找 bug,结果如下:
| 模型 | 命中真实 bug 数 | 误报数 | 单次耗时 | 备注 |
|---|---|---|---|---|
| 模型 A(海外大模型) | 7/10 | 3 | 28s | 质量高但慢 |
| 模型 B(DeepSeek 系) | 6/10 | 2 | 9s | 性价比最优 |
| 模型 C(小参数本地模型) | 3/10 | 5 | 4s | 误报太多 |
最后我选了模型 B 作为默认,模型 A 作为可选高精度模式。这里的关键经验是:代码评审不需要模型无所不知,它需要的是稳定、快、误报低。误报比漏报更致命,因为开发者被误报烦几次之后就会彻底无视这个工具。
2.4 Git 集成方式的选择
评审的输入从哪来?我支持三种模式。第一种是--staged,读暂存区,适合提交前自查。第二种是--range A..B,读两个 commit 之间的 diff,适合 CI 里评审整个 PR。第三种是--file path,直接读某个文件,适合单文件快速过一遍。
底层用的是git diff加--unified=0参数,只取变更行和极少量上下文,避免把整个文件塞给模型浪费 token。这里有个细节:git diff默认会做重命名检测和空白处理,我在命令里显式加了-c diff.mnemonicprefix=false -c core.quotepath=false --no-optional-locks这几个配置,目的是让输出稳定、路径不乱码、不因为锁文件冲突卡住。这些参数看着琐碎,但在 CI 里跑批量任务时能省掉一堆诡异问题。
3. 核心细节解析与实操要点
3.1 diff 抓取与预处理
抓 diff 这一步看着简单,其实最容易出问题。我最初直接git diff HEAD一把梭,结果发现几个坑。第一,二进制文件也会被带进来,模型看到一堆乱码直接懵。第二,超大文件(比如自动生成的 lock 文件)会把 token 撑爆。第三,重命名文件会显示成删除加新增,评审意见驴唇不对马嘴。
我的处理方案是三步过滤。第一步,用git diff --numstat先拿到每个文件的增删行数,超过阈值的文件直接跳过并在报告里标注“文件过大,已跳过”。第二步,用git diff --name-only配合后缀白名单,只保留代码文件,.lock、.min.js、图片、二进制一律排除。第三步,对每个文件单独跑git diff,这样能精确控制上下文行数。
预处理还有一个关键动作是行号映射。模型返回的意见通常是“第 42 行有问题”,但这个 42 行是 diff 里的行号还是文件里的行号?如果不做映射,贴到 PR 上就错位了。我的做法是解析 diff 的 hunk header,比如@@ -10,5 +10,7 @@,把新增行的文件行号算出来,建立一个 diff 行号到文件行号的映射表,模型返回后再反查。
提示:diff 预处理阶段一定要做文件大小和类型过滤,否则一个几万行的 lock 文件就能让你的 token 账单翻好几倍,而且模型对这类文件的分析基本没有价值。
3.2 Prompt 工程:怎么让模型说人话
Prompt 是这套工具的灵魂。我前后改了十几版,总结出几条硬经验。
第一条,角色要具体。不要写“你是一个代码评审助手”,要写“你是一个有十年经验的后端工程师,正在评审一个准备合并到主干的 PR,你关注的是正确性、安全性和可维护性,不关注代码风格”。角色越具体,输出越聚焦。
第二条,输出格式要强约束。我要求模型必须返回 JSON,每个问题包含file、line、severity、category、message、suggestion六个字段。severity 分blocker、major、minor、nit四档,category 分bug、security、performance、readability、test五类。格式固定了,后面才能做统计和卡点。
第三条,给正反例。我在 prompt 里塞了两个示例,一个是好的评审意见(指出具体问题、给出修改建议、说明为什么),一个是差的(“这里可能有问题,建议检查一下”这种废话)。模型模仿能力很强,给了例子之后输出质量明显提升。
第四条,明确禁止项。我明确告诉模型:不要评论代码格式(那是 linter 的活)、不要重复描述代码做了什么、不要给没有依据的猜测。这三条一加,误报率降了大概四成。
3.3 结果结构化与终端渲染
模型返回 JSON 之后,要做校验和渲染。校验这块我用了一个简单的 schema 检查,字段缺失或者 severity 不在枚举里的,直接丢弃并记日志。这一步很重要,因为模型偶尔会抽风返回半截 JSON,不校验的话下游全崩。
终端渲染我用了彩色输出,blocker 红色、major 黄色、minor 蓝色、nit 灰色,一眼就能看出轻重缓急。每个问题下面跟一行代码片段和修改建议,方便直接对照。如果加了--output report.md参数,就额外生成一份 Markdown 报告,格式适配主流代码托管平台的评论语法,可以直接复制粘贴。
这里有个小心机:我在报告末尾加了一个“本次评审统计”区块,列出各 severity 的数量、各 category 的分布、总耗时和 token 消耗。这个统计看着不起眼,但团队用久了能看出趋势,比如某个模块 security 类问题一直高发,那就说明这块需要专门做培训或者重构。
3.4 配置管理:别把密钥写死在代码里
配置这块我吃过亏。最早图省事,把模型 API 的密钥直接写在脚本里,结果有一次不小心提交到了仓库,虽然及时发现删了,但那种后背发凉的感觉至今记得。后来改成三层配置:默认配置写在代码里,用户配置放~/.open-code-review/config.yaml,环境变量优先级最高。
配置项大概长这样:
model: provider: deepseek name: deepseek-coder api_key_env: OCR_API_KEY max_tokens: 4096 temperature: 0.2 review: mode: auto max_file_lines: 2000 exclude_patterns: - "*.lock" - "*.min.js" - "dist/**" severity_threshold: minor output: format: terminal color: trueapi_key_env这个设计是关键,配置文件里只写环境变量的名字,真正的密钥通过环境变量注入。这样配置文件可以随便提交、随便分享,密钥永远不进仓库。temperature 我设成 0.2,因为评审需要稳定,不需要创意。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
先把基础环境搭起来。这套工具是 Python 写的,需要 Python 3.9 以上。Git 是必须的,Windows 用户如果还没装,去官网下载安装包一路下一步就行,安装时记得勾选“Add Git to PATH”,否则命令行里敲git会提示找不到命令。装完在终端里跑git --version能看到版本号就说明成了。
依赖安装就一行:
pip install open-code-review如果你想像我一样改源码,那就 clone 下来用可编辑模式装:
git clone https://example.com/open-code-review.git cd open-code-review pip install -e .装完之后跑ocr --version验证一下。如果提示命令找不到,八成是 Python 的 Scripts 目录没加到 PATH 里,Windows 上一般是%USERPROFILE%\AppData\Local\Programs\Python\Python3x\Scripts,手动加一下就好。
4.2 模型接入配置
模型接入是第一个要配的东西。以 DeepSeek 为例,先去控制台申请一个 API key,然后设置环境变量。Linux 和 macOS 下:
export OCR_API_KEY="你的密钥"Windows PowerShell 下:
$env:OCR_API_KEY="你的密钥"想永久生效就写进 shell 的配置文件或者系统环境变量里。配好之后跑一次连通性测试:
ocr doctor这个命令会检查 Git 是否可用、密钥是否配置、模型接口是否通、网络是否正常,四项全绿就说明环境没问题。我建议每次换机器或者换模型都先跑一遍 doctor,能省掉大量“为什么跑不通”的排查时间。
4.3 第一次评审:从暂存区开始
最简单的用法是评审暂存区。你先改几个文件,git add之后跑:
ocr review --staged工具会抓取暂存区的 diff,过滤、预处理、调模型、渲染结果。我第一次跑的时候拿了一个故意写了 bug 的文件测试,模型准确指出了空指针风险和资源未释放的问题,还给了修改建议。那一刻的感觉是:这东西真能用。
输出大概长这样:
[BLOCKER] src/service/user.py:42 问题:这里对 user 对象直接取属性,但上游查询可能返回 None 建议:在访问前加 if user is None 判断,或使用 getattr 提供默认值 [MAJOR] src/service/user.py:58 问题:数据库连接在异常路径下没有关闭 建议:改用 with 语句管理连接生命周期4.4 接入 CI:让每个 PR 自动过一遍
单机用只是第一步,真正的价值在 CI 里。以常见的流水线为例,加一个 step:
- name: Code Review run: | pip install open-code-review ocr review --range origin/main...HEAD --output report.md --fail-on blocker env: OCR_API_KEY: ${{ secrets.OCR_API_KEY }}--range origin/main...HEAD表示评审当前分支相对主干的全部变更,三个点表示取分叉点之后的变更,比两个点更准确。--fail-on blocker表示只要出现 blocker 级别的问题就让流水线失败,起到卡点作用。--output report.md生成报告文件,后续可以上传成构建产物或者贴到 PR 评论。
这里有个实操细节:CI 环境里 Git 默认是浅克隆,origin/main可能不存在。解决办法是在 checkout 那一步设置fetch-depth: 0,把完整历史拉下来。这个坑我踩过,当时排查了半天才发现是浅克隆导致的。
4.5 用 Git hook 做提交前拦截
CI 卡点的问题是反馈太晚,代码都推上去了才告诉你不行。更早的拦截点是 Git hook。在.git/hooks/pre-commit里写:
#!/bin/sh ocr review --staged --fail-on blocker if [ $? -ne 0 ]; then echo "存在 blocker 级别问题,提交已阻止" exit 1 fi记得给这个文件加执行权限chmod +x .git/hooks/pre-commit。这样每次git commit之前都会自动跑一遍评审,有问题直接拦下来。我建议只拦 blocker,major 和 minor 放行但提示,否则开发者会被烦到直接--no-verify绕过,那就失去意义了。
4.6 多分支并行场景下的处理
团队里经常有人同时开好几个分支,这时候评审范围要算清楚。我一般用git worktree来管理并行分支,每个 worktree 一个独立目录,互不干扰。评审的时候在对应目录里跑ocr review --range main...HEAD,范围就是当前 worktree 的分支相对 main 的变更。
如果分支落后主干太多,diff 里会混入大量别人的变更,评审结果就不准了。我的做法是评审前先git rebase main或者git merge main,把主干最新代码合进来,再跑评审。这一步多花几十秒,但能让评审聚焦在自己真正改的东西上。
5. 常见问题与排查技巧实录
5.1 模型返回格式错误怎么办
这是最高频的问题。模型偶尔会返回带 markdown 代码块包裹的 JSON,或者字段名拼错,或者干脆返回一段自然语言。我的处理是三层防御。第一层,prompt 里明确要求“只返回 JSON,不要任何额外文字”。第二层,解析前先做清洗,把json 和这类包裹去掉。第三层,解析失败时自动重试一次,重试时在 prompt 里追加“上次返回格式错误,请严格按 JSON 格式返回”。三层下来,格式错误率从最初的 15% 降到了 1% 以下。
如果重试还是失败,工具会记录原始返回内容到日志,方便排查。我遇到过模型把severity写成level的情况,这种就是 prompt 里字段说明不够醒目,把字段名加粗、加引号之后就再没出现过。
5.2 token 超限怎么处理
大 PR 很容易超 token。我的策略是分而治之:先按文件切分,每个文件单独评审,最后汇总。如果单个文件还是太大,就按 hunk 切分。切分的时候要注意保留足够的上下文,否则模型看不懂变更的意图。我一般给每个 hunk 前后各留 5 行上下文,实测下来这个数量在准确率和 token 消耗之间平衡得最好。
还有一个技巧是摘要压缩。对于超大 PR,先让模型对每个文件的变更做一句话摘要,然后把所有摘要拼起来做一轮整体评审,识别出高风险文件,再对这些文件做深度评审。这样既控制了 token,又保证了重点文件的分析深度。
5.3 误报太多怎么调
误报是劝退开发者的头号杀手。我总结了几个降误报的手段。第一,提高 temperature 的稳定性,设成 0.1 到 0.2 之间。第二,在 prompt 里明确“只报告你有把握的问题,不确定的不要报”。第三,加一个后处理过滤,把 message 里包含“可能”“也许”“建议检查”这类模糊措辞的意见降级或丢弃。第四,收集开发者反馈,把被标记为“误报”的案例整理成反例,定期更新到 prompt 里。
我做过统计,经过这几轮优化,误报率从最初的 30% 降到了 8% 左右。剩下的 8% 里,有一部分其实是模型发现了真问题但表述不够准确,这种可以通过人工复核保留。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方向 | 解决办法 |
|---|---|---|---|
| 命令找不到 | PATH 未配置 | 检查 Python Scripts 目录 | 手动加入 PATH |
| 密钥无效 | 环境变量未生效 | echo $OCR_API_KEY | 重新设置并重启终端 |
| diff 为空 | 没有暂存变更 | git status确认 | 先git add |
| 评审超时 | 网络或模型负载 | 检查网络连通性 | 换模型或加超时时间 |
| 行号错位 | diff 映射错误 | 对比报告和实际文件 | 检查 hunk header 解析 |
| 中文乱码 | 编码不一致 | 检查终端编码 | 设置 UTF-8 |
| CI 里 origin/main 不存在 | 浅克隆 | 检查 fetch-depth | 设为 0 |
| 提交被 hook 拦截 | 存在 blocker | 看评审报告 | 修复后重新提交 |
5.5 几个我踩过的坑
第一个坑是路径分隔符。Windows 上 Git 返回的路径是反斜杠,Linux 上是正斜杠,如果不统一处理,文件匹配就会失败。我的做法是拿到路径后统一转成正斜杠再处理。
第二个坑是中文文件名。Git 默认会对非 ASCII 文件名做转义,显示成\344\270\255\346\226\207这种。解决办法是在 Git 配置里设core.quotepath=false,让 Git 直接输出原始字符。这个配置我在抓 diff 的命令里已经显式加上了。
第三个坑是并发调用限流。CI 里如果同时跑多个评审任务,很容易触发模型的速率限制。我的做法是加一个简单的令牌桶限流,每个任务之间间隔固定时间,或者用队列串行处理。宁可慢一点,也不要因为限流导致任务失败。
第四个坑是评审结果缓存。同一个 commit 反复评审是浪费。我用 commit hash 加配置指纹做 key,把结果缓存到本地,命中缓存直接返回。这个优化让重复评审的耗时从十几秒降到毫秒级,在 CI 里效果尤其明显。
6. 扩展方向与个人实践体会
这套工具跑了大半年,团队里的接受度比我想象中高。最开始大家觉得是“又一个形式主义工具”,用了两个月之后,有同学主动跟我说“这个评审帮我抓到了一个我完全没注意到的并发问题”。这种反馈比任何指标都有说服力。
后续我打算往几个方向扩展。一是接入团队知识库,把历史评审意见、编码规范、事故复盘做成 embedding 索引,评审时检索相关条目作为上下文,让意见更贴合团队实际。二是多模型投票,对 blocker 级别的问题用两个模型交叉验证,降低误报。三是评审质量追踪,记录每条意见是否被采纳,定期统计采纳率,用数据驱动 prompt 迭代。
最后分享一个我个人的使用习惯:我把ocr review --staged绑成了一个 Git alias,叫git ocr,每次提交前顺手跑一下,已经成了肌肉记忆。工具这东西,只有融进日常工作流,才能真正发挥价值。如果你也在为代码评审发愁,不妨从最简单的暂存区评审开始试起,跑通之后再往 CI 和 hook 上接,一步一步来,别一上来就搞大而全的方案。