1. 为什么我要自己动手做一个 open-code-review 工具
代码审查这件事,做过团队协作的人都有体会。理想状态下,每次提交都应该有人认真看一遍,指出命名问题、边界条件、潜在的空指针、重复逻辑。但现实是,评审人自己手里一堆活,打开 diff 扫两眼,点个 approve 就过去了。时间一长,代码库里的技术债越堆越高,等到某天线上出问题回头翻提交记录,才发现当初那个明显的疏漏根本没人注意到。
市面上当然有商业化的代码审查服务,也有各种 IDE 插件。但我在实际项目里用下来,总有几个绕不过去的坎:一是代码要上传到第三方平台,公司内部项目根本不允许;二是规则固定,想让它按团队自己的规范来检查,配置起来非常别扭;三是费用按人头算,小团队还好,人一多成本就上来了。所以我一直想做一个能跑在自己机器上、完全本地化、规则可定制的代码审查工具,这就是open-code-review的由来。
这个工具的核心思路很直接:用CLI的方式调用,读取Git仓库的变更,把 diff 内容交给LLM Agent去分析,最后输出一份结构化的审查报告。它不依赖任何云端服务,所有代码和模型调用都在本地或你指定的环境里完成。适合的对象也很明确——中小团队的技术负责人、独立开发者、以及对代码质量有要求但不想引入重型平台的工程师。哪怕你只是一个人写项目,用它做提交前的自检,也能挡掉不少低级错误。
我把它定位成一个"能塞进现有工作流"的小工具,而不是一个需要专门学习的新平台。你可以在提交前手动跑一次,也可以挂到 Git 的 pre-commit 钩子里自动触发。下面我会把整个设计思路、关键实现、踩过的坑,以及怎么跟现有工具链配合,完整地讲一遍。
2. 拆解 open-code-review 的核心工作链路
2.1 从 Git diff 到结构化审查任务的转换
整个工具最基础的一环,是把 Git 的变更内容变成 LLM 能理解的输入。这里有个容易被忽略的细节:git diff的输出格式对人类友好,但对模型来说信息密度不均匀。一个典型的 diff 包含文件路径、变更块头部(hunk header)、增删行标记,还有上下文行。如果直接把原始 diff 丢给模型,它很容易被那些@@ -12,7 +12,9 @@的标记干扰,把注意力放在行号计算上而不是代码逻辑上。
我的做法是做一层预处理。先用git diff --unified=3拿到标准输出,然后按文件切分,每个文件提取出变更块。对于每个变更块,我保留增删行的实际内容,把上下文行压缩成简短的定位信息。这样模型看到的是"在某个函数的某个位置,删掉了什么,新增了什么",而不是一堆带+-前缀的文本。实测下来,这种预处理能让模型对变更意图的判断准确率提升不少。
具体实现上,我用的是git diff --no-color --no-ext-diff来保证输出干净,避免终端颜色码混进去。然后写了一个简单的解析器,按diff --git分割文件,按@@分割变更块。每个变更块记录三样东西:变更前后的行号范围、删除的行内容、新增的行内容。这个结构后面既可以喂给模型,也可以用来做本地规则检查,比如检测是否删除了某个关键函数的调用。
提示:如果你的仓库里有大量二进制文件或者自动生成的代码,建议在
.gitattributes里标记出来,或者在工具里加一个忽略列表。否则模型会浪费大量 token 去分析那些根本不需要审查的内容。
2.2 LLM Agent 在审查流程里到底扮演什么角色
这里要先厘清一个概念。很多人把Agent和LLM混着用,其实两者不是一回事。LLM 是底层的语言模型,比如 DeepSeek、GPT 系列、Claude 系列,它们本质上是"输入文本、输出文本"的函数。而 Agent 是在 LLM 之上加了一层调度逻辑,让它能调用工具、维护状态、分步骤完成任务。在 open-code-review 里,LLM 负责理解代码和生成审查意见,Agent 负责决定"先看哪个文件""要不要再查一下这个函数的定义""审查完了要不要生成摘要"。
我设计的 Agent 流程是这样的:第一步,接收预处理后的 diff 列表;第二步,对每个文件的变更块逐个分析,生成初步意见;第三步,把所有意见汇总,去重、排序、按严重程度分级;第四步,输出最终报告。这个流程里,LLM 的调用是多次的,每次只处理一个变更块,而不是一次性把整个 diff 塞进去。这样做的好处是每次调用的上下文更聚焦,模型不容易"看花眼",而且单个变更块出错不会影响整体。
另一个关键点是提示词的设计。我试过好几种写法,最后稳定下来的版本包含这几部分:角色设定(你是一个严格的代码审查者)、审查维度(正确性、可读性、性能、安全性)、输出格式(JSON,包含文件、行号、严重程度、问题描述、修改建议)、以及几条硬性约束(不要评论代码风格偏好、不要建议重写整个函数、如果没问题就返回空数组)。这几条约束非常重要,没有它们,模型会变得非常"话痨",把每个变量命名都点评一遍,报告根本没法看。
2.3 CLI 交互设计与退出码约定
既然是 CLI 工具,交互体验就得符合命令行用户的习惯。我定的规则是:默认输出人类可读的报告,带颜色高亮;加--json参数输出机器可解析的 JSON;加--quiet只输出有问题的条目;加--strict时,只要发现严重问题就返回非零退出码。这个退出码约定很关键,因为它决定了这个工具能不能挂到 CI 流程里。比如在 pre-commit 钩子里,如果审查发现严重问题,退出码非零,提交就会被阻止。
命令设计上,我参考了常见 CLI 工具的习惯:
# 审查当前工作区未提交的变更 open-code-review # 审查最近一次提交 open-code-review --commit HEAD # 审查两个提交之间的差异 open-code-review --range main..feature # 输出 JSON 格式,方便其他工具消费 open-code-review --json # 严格模式,有严重问题就退出码非零 open-code-review --strict参数解析我用的是 Python 的argparse,没有引入额外的 CLI 框架。原因很简单:这个工具的参数不多,argparse足够用,而且它是标准库,不增加安装负担。如果你用 Node.js 写,commander或yargs都是不错的选择,但要注意别为了几个参数引入一个庞大的依赖树。
3. 把 LLM 接进来:模型选型与调用策略
3.1 本地模型和远程 API 的取舍
这是整个项目里我最纠结的一个决策点。用远程 API,比如 DeepSeek 或者 GPT 系列,好处是模型能力强、不需要本地算力、接入简单。坏处是代码要发出去,虽然大多数 API 提供商承诺不保留数据,但对于公司内部项目,这个风险很多团队不愿意承担。用本地模型,比如通过 Ollama 跑一个代码能力不错的开源模型,好处是数据完全不出本机,坏处是对硬件有要求,而且模型能力参差不齐。
我最后的方案是做成可切换的。配置文件里指定provider,支持openai-compatible和ollama两种。openai-compatible可以对接任何兼容 OpenAI 接口的服务,包括 DeepSeek 的 API、公司内部部署的推理服务等。ollama则走本地。这样用户可以根据自己的情况选。对于代码审查这个场景,我的经验是:如果只是做基础的正确性检查,本地跑一个 7B 到 14B 的代码模型就够用了;如果要检测更微妙的逻辑问题,远程的大模型效果明显更好。
配置文件的格式我用的是 TOML,放在项目根目录的.open-code-review.toml,也支持放在用户主目录下作为全局配置。内容大概长这样:
[llm] provider = "openai-compatible" base_url = "https://api.example.com/v1" model = "deepseek-chat" api_key_env = "OCR_API_KEY" max_tokens = 2048 temperature = 0.2 [review] ignore_patterns = ["*.lock", "dist/*", "*.min.js"] severity_threshold = "warning" max_files = 20api_key_env这个设计是为了避免把密钥写进配置文件。工具启动时从环境变量读取,这样配置文件可以安全地提交到仓库里,团队成员共享同一份配置,但各自的密钥独立管理。
3.2 提示词工程:让模型说人话而不是套话
提示词这块我踩了不少坑。最开始我写的提示词是"请审查以下代码变更,指出所有问题"。结果模型返回的东西五花八门,有的用中文,有的用英文,有的列了一堆无关紧要的风格建议,有的干脆把整个函数重写了一遍。后来我逐步收紧,形成了现在这个版本:
你是一个经验丰富的代码审查者。请审查以下代码变更,只关注以下维度: 1. 正确性:是否有逻辑错误、边界条件遗漏、空值处理缺失 2. 安全性:是否有注入风险、敏感信息泄露、权限校验缺失 3. 性能:是否有明显的性能问题,如循环内查询、不必要的重复计算 4. 可维护性:是否有难以理解的逻辑、过长的函数、重复代码 不要评论:变量命名风格、缩进、注释多少、个人编码偏好。 输出格式为 JSON 数组,每个元素包含: - file: 文件路径 - line: 行号 - severity: "error" | "warning" | "info" - message: 问题描述,用中文,一句话说清楚 - suggestion: 修改建议,用中文,具体可操作 如果没有发现问题,返回空数组 []。这个提示词的关键在于"不要评论"那一段。没有它,模型会忍不住对每个变量名发表意见。另外severity的分级也很重要,它决定了报告的可读性。我把error定义为"必须修复,否则可能出 bug",warning定义为"建议修复,有潜在风险",info定义为"可以关注,但不紧急"。这样用户在--strict模式下可以只拦截error级别的问题。
3.3 处理模型返回的脏数据
即使提示词写得很清楚,模型偶尔还是会返回不符合格式的内容。比如 JSON 外面包了一层 markdown 代码块标记,或者某个字段缺失,或者severity返回了"critical"这种我没定义的值。这些情况必须在代码里处理掉,否则工具会直接崩溃。
我的处理策略是分三层。第一层,解析前先做清洗,把json 和这种标记去掉,把首尾空白去掉。第二层,用json.loads解析,如果失败,尝试用正则提取出最外层的[...]再解析。第三层,解析成功后逐条校验字段,缺失的字段填默认值,severity不在枚举范围内的降级为info。经过这三层处理,基本上不会因为模型返回格式问题导致工具挂掉。
注意:不要假设模型每次都会返回合法 JSON。我实测下来,大概每二十次调用会有一次返回格式有问题,比例不高但足以让工具在关键时刻掉链子。所以容错逻辑是必须的,不是可选的。
4. 和 Git 工作流的深度整合
4.1 挂到 pre-commit 钩子上的正确姿势
把 open-code-review 挂到 pre-commit 钩子上,是最自然的用法。但这里有个坑:pre-commit 钩子执行时,Git 的索引状态和正常工作区不完全一样。如果你直接用git diff,拿到的是工作区和暂存区的差异,而不是暂存区和 HEAD 的差异。正确的做法是用git diff --cached来获取即将提交的内容。
钩子脚本大概长这样:
#!/bin/sh # .git/hooks/pre-commit open-code-review --cached --strict --quiet if [ $? -ne 0 ]; then echo "代码审查发现严重问题,提交已阻止。" echo "如需跳过,请使用 git commit --no-verify" exit 1 fi这里--cached参数让工具去读暂存区的内容,--strict让它在有error级别问题时返回非零,--quiet让输出简洁一些。最后那个提示信息很重要,它告诉用户怎么绕过检查。虽然我们不希望用户绕过,但必须给他们这个选项,否则遇到紧急情况会很麻烦。
另外要注意,pre-commit 钩子默认不会被克隆到新仓库里。所以我在工具里加了一个open-code-review install-hook命令,自动把钩子脚本写到.git/hooks/目录下,并设置可执行权限。这样新加入项目的成员跑一次这个命令就能配好。
4.2 在 CI 流水线里做增量审查
除了本地钩子,CI 里也可以跑。但 CI 环境和本地不一样,它通常是对整个 PR 做审查,而不是单次提交。这时候用--range参数指定目标分支和当前分支的差异比较合适。比如在 GitHub Actions 里:
- name: Run code review run: | open-code-review --range origin/main..HEAD --json > review.json env: OCR_API_KEY: ${{ secrets.OCR_API_KEY }} - name: Check review results run: | python -c " import json with open('review.json') as f: issues = json.load(f) errors = [i for i in issues if i['severity'] == 'error'] if errors: print(f'发现 {len(errors)} 个严重问题') for e in errors: print(f\" {e['file']}:{e['line']} - {e['message']}\") exit(1) "这个流程的好处是审查结果可以存档,也可以作为 PR 评论自动发出来。不过 CI 里跑要注意 token 消耗,如果 PR 很大,diff 内容多,调用次数会上去。我在工具里加了max_files配置,超过这个数量的文件就只审查前 N 个,并在报告里注明"因文件数量限制,部分文件未审查"。
4.3 处理大仓库的性能问题
大仓库是另一个挑战。一个几千文件的仓库,如果每次提交都全量审查,那工具就没法用了。我的策略是只审查变更涉及的文件,而且只审查变更块附近的内容。但即使这样,如果一次提交改了五十个文件,调用次数还是很多。
优化手段有几个。第一,并发调用。LLM 的 API 调用是 IO 密集型的,用线程池并发跑能显著缩短总时间。我用concurrent.futures.ThreadPoolExecutor控制并发数,默认 4 个,可以通过配置调整。第二,缓存。同一个变更块如果之前审查过且代码没变,结果可以缓存起来。我用变更块的哈希值作为 key,把结果存到本地 SQLite 里。第三,跳过。对于纯文档变更、配置文件变更、测试文件变更,可以通过ignore_patterns跳过,不浪费调用。
实测下来,一个中等规模的提交(10 个文件左右,每个文件几十行变更),用并发加缓存,总耗时能控制在 30 秒以内。这个速度对于 pre-commit 钩子来说可以接受,如果嫌慢可以改成只在 CI 里跑。
5. 踩过的坑和对应的解决方案
5.1 模型把上下文行当成变更内容
这是最早遇到的一个问题。Git diff 里,变更块会包含几行上下文,用来帮助定位。这些上下文行在 diff 里没有+或-前缀,但模型有时候分不清,会把上下文行也当成变更内容来审查,然后报告说"这行代码有问题",实际上这行根本没改。
解决方案是在预处理阶段就把上下文行剥离掉,只保留增删行。但完全剥离又有个问题:有些变更只有理解了上下文才能判断对错。比如删掉了一行return result,如果不知道这个函数在做什么,就没法判断这个删除是否合理。所以我的做法是保留上下文,但在提示词里明确标注哪些是变更、哪些是上下文。具体格式上,我用[REMOVED]和[ADDED]标记增删行,上下文行不加标记,并在提示词里说明"只有带标记的行是本次变更,其他行仅用于提供上下文"。
5.2 行号对不上导致报告无法定位
另一个坑是行号。模型返回的行号是基于它看到的输入文本的行号,而不是文件里的真实行号。如果直接把模型返回的行号写到报告里,用户点过去会发现位置完全不对。
解决办法是在预处理阶段就记录每个变更块在文件中的真实起始行号,然后在模型返回结果后,把模型的行号映射回真实行号。具体来说,我在每个变更块的输入里加上一行[FILE: path] [HUNK: start_line],让模型知道这个块对应文件的哪一行开始。模型返回行号时,我再用这个起始行号做偏移计算。这个映射逻辑要小心处理,因为模型可能返回相对于变更块的行号,也可能返回相对于文件的行号,需要在提示词里明确约定。
5.3 API 调用失败和重试策略
网络请求失败是常态,尤其是在并发调用的时候。我遇到过几种失败:超时、限流、服务端 500、返回内容被截断。每种的处理方式不一样。
超时和 500 可以直接重试,我设置了最多 3 次重试,每次间隔指数退避。限流(通常是 429 状态码)需要等更久,我读到响应头里的Retry-After字段,按它指定的时间等待。返回内容被截断比较麻烦,通常是因为max_tokens设小了,模型还没输出完就被切断了。这种情况重试也没用,需要调大max_tokens或者把变更块拆得更小。我在工具里加了一个检测:如果返回的 JSON 解析失败且内容长度接近max_tokens,就在报告里提示"可能因 token 限制导致输出不完整"。
提示:并发调用时,建议把并发数控制在 4 到 8 之间。太高容易触发限流,太低又起不到加速效果。具体数值取决于你用的 API 服务的限制,可以先从 4 开始试。
5.4 误报和漏报的平衡
代码审查工具最怕两种极端:一种是误报太多,用户被淹没在无意义的提示里,最后干脆不看;另一种是漏报太多,用户觉得这工具没用,还不如自己看。平衡这两者是个持续调优的过程。
我的经验是,宁可漏报也不要误报。因为误报会消耗用户的信任,而漏报至少不会造成干扰。所以在提示词里我加了一条:"如果不确定某个问题是否真实存在,不要报告。"这条约束显著降低了误报率。另外,对于info级别的问题,我默认不在--quiet模式下显示,只在完整报告里列出。这样用户日常使用时看到的是真正需要关注的问题。
还有一个技巧是让模型给出置信度。我在输出格式里加了一个confidence字段,取值 0 到 1。然后在报告生成时,把置信度低于 0.6 的条目过滤掉。这个字段模型给得不一定准,但作为一个额外的过滤维度还是有用的。
6. 扩展方向与个人实践体会
这个工具目前能满足我日常的代码审查需求,但还有不少可以扩展的地方。比如支持自定义规则,让团队把自己的编码规范写成规则文件,工具在调用 LLM 之前先跑一遍本地规则检查,这样既省 token 又能保证规范落地。再比如支持审查历史记录,把每次审查的结果存下来,过一段时间可以分析哪些类型的问题反复出现,帮助团队改进。
另一个有意思的方向是和 IDE 集成。虽然 CLI 已经够用,但如果能在 VS Code 里直接看到审查结果,体验会更好。不过这个优先级不高,因为 CLI 已经能覆盖大部分场景,而且 IDE 插件需要维护额外的代码库。
我在实际使用中最大的体会是:不要指望这个工具能替代人工审查。它的定位是"第一道防线",帮你挡掉那些明显的、机械性的问题,让人工审查能聚焦在架构设计、业务逻辑这些更需要判断力的地方。把它当成一个不知疲倦的初级审查者,而不是一个资深架构师。心态摆正了,用起来就顺了。
最后分享一个小技巧:如果你用的是远程 API,建议在项目初期先用本地模型跑一段时间,把提示词和规则调稳定了再切到远程。因为调提示词阶段需要反复试错,用远程 API 会消耗不少额度。等提示词稳定了,再切到能力更强的远程模型,效果和成本都能兼顾。