基于LLM的本地代码审查工具:open-code-review实战
2026/9/20 16:50:27 网站建设 项目流程

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

代码审查这件事,做过团队协作的人都有体会。理想状态下,每次提交都有人认真看、认真提意见,把问题拦在合并之前。现实往往是另一回事:项目赶进度,PR 堆了十几个没人理; reviewer 自己手头一堆活,扫两眼点个 approve;新人提交的代码没人带,问题反复出现。时间一长,代码质量全靠个人自觉,团队的技术债越滚越大。

我所在的团队规模不大,七八个人,前后端加数据都有。之前试过几种方案:定规矩要求每天固定时间集中 review,坚持了两周就散了;用一些在线的代码托管平台自带的审查功能,但那些工具更多是格式检查,对业务逻辑、边界条件、潜在 bug 基本无能为力。后来大模型的能力上来了,我就琢磨着能不能把 LLM 接进代码审查流程里,让它先过一遍,把明显的问题挑出来,人只需要看它标记的重点就行。

open-code-review这个项目就是在这个背景下折腾出来的。它的核心思路很直接:用命令行工具拉取 Git 仓库的变更,把 diff 内容喂给 LLM,让模型按照预设的审查规则输出意见,最后把结果整理成可读的报告。整套东西跑在本地或者 CI 里都行,不依赖特定的代码托管平台,Git 仓库在哪它就能在哪工作。

这套东西适合谁用?我觉得三类人比较合适。一是小团队的技术负责人,想给团队加一道自动化的质量关卡,但又不想引入太重的外部服务;二是独立开发者,自己写代码没人 review,让 LLM 帮忙看看能发现不少低级错误;三是对 LLM Agent 和 CLI 工具感兴趣、想动手实践的人,这个项目涉及 Git 操作、命令行参数解析、LLM API 调用、结果格式化等环节,是个不错的练手项目。

下面我会从整体设计、核心细节、实操过程、问题排查几个方面,把这套东西拆开讲清楚。里面涉及的代码和配置都是我实际跑过的,参数选择也会说明理由,你可以直接参考着搭一套自己的版本。

2. 整体设计与技术选型拆解

2.1 核心工作流是怎么串起来的

整套工具的运行逻辑可以拆成四步。第一步是确定审查范围,也就是搞清楚这次要看哪些改动。常见的方式有两种:一种是看当前分支相对于目标分支的差异,比如 feature 分支要合并到 main,那就对比这两个分支;另一种是看最近若干次提交,比如最近三次 commit 都改了啥。这两种场景在实际工作中都会遇到,所以工具得支持灵活指定。

第二步是获取 diff 内容。Git 本身提供了git diff命令,能输出两个提交之间的差异。但直接拿到的 diff 是纯文本,包含大量上下文行、文件头信息、行号标记,直接丢给 LLM 会浪费 token,还可能干扰模型判断。所以中间需要一层解析,把 diff 拆成按文件分组的变更块,每个块里只保留真正改动的行和必要的上下文。

第三步是调用 LLM 做审查。这里的关键是提示词的设计。不能简单地说“帮我看看这段代码有没有问题”,那样模型输出会很发散。需要给它明确的角色设定、审查维度、输出格式要求。比如要求它从正确性、边界条件、性能、可读性几个角度分析,每个问题给出文件位置、行号、问题描述、修改建议。

第四步是汇总输出。模型返回的结果可能是 JSON,也可能是 Markdown,需要解析后整理成统一的报告格式。如果发现问题,可以选择直接打印到终端,也可以写进文件,或者作为 CI 的评论发出去。

这四步串起来,就是一个完整的审查流程。每一步都有可以优化的空间,后面会细说。

2.2 为什么选 CLI 而不是 Web 服务

做这个工具的时候,我考虑过几种形态。做成 Web 服务,界面友好,但部署麻烦,还得考虑鉴权、并发、任务队列。做成 IDE 插件,用起来方便,但受限于 IDE 的生态,换个编辑器就用不了。最后选 CLI,理由有几个。

CLI 的侵入性最低。它就是一个命令,你可以在本地终端跑,也可以在 CI 脚本里跑,还可以挂到 Git 的 hook 上。不需要改现有工作流,不需要团队每个人都装什么客户端。对于小团队来说,推广成本几乎为零。

CLI 的组合能力最强。Unix 哲学里,每个工具只做一件事,然后通过管道组合。这个审查工具输出报告,你可以用grep过滤,可以用tee存文件,可以重定向到其他工具做进一步处理。这种灵活性是 Web 服务给不了的。

CLI 的调试最直接。出问题了,加个--verbose看日志,或者直接在代码里打断点。不用去翻服务器日志,不用去猜请求链路。对于这种还在快速迭代的工具,开发效率很重要。

当然 CLI 也有缺点,比如对不熟悉命令行的同事不太友好。但代码审查这个场景,使用者本来就是开发者,命令行是基本功,这个门槛可以接受。

2.3 LLM 选型与接入方式的考量

模型这块,我试过几种接入方式。最直接的是调各家厂商的 API,比如 DeepSeek、通义千问、智谱这些国内可访问的模型服务。优点是稳定、速度快、按量计费透明。缺点是得管理 API Key,而且不同厂商的接口格式有差异,换模型要改代码。

另一种方式是本地部署开源模型,比如用 Ollama 跑 CodeLlama 或者 Qwen 的代码版本。好处是数据不出本地,适合对代码保密性要求高的场景。坏处是对机器配置有要求,推理速度也慢一些,大仓库的 diff 可能跑不动。

我最后的方案是做成可插拔的。抽象出一个 LLM Provider 接口,定义review(diff_content, prompt)方法,具体实现可以是 OpenAI 兼容接口、可以是 Ollama、也可以是其他任何服务。配置里指定用哪个 provider,代码里通过工厂模式创建实例。这样换模型只需要改配置,不用动核心逻辑。

关于 Agent 和 LLM 的区别,这里顺带说一句。LLM 是底层模型,你给它输入它给你输出,本身没有行动能力。Agent 是在 LLM 之上加了一层决策和工具调用能力,它能根据任务自己决定下一步做什么,比如先读文件、再搜索、再修改。这个审查工具严格来说不算 Agent,因为它流程是固定的:拿 diff、调模型、出报告。但如果后续想加自动修复功能,比如让模型直接改代码然后提交,那就往 Agent 方向走了。

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

3.1 Git diff 的获取与解析

获取 diff 看起来简单,实际有不少细节。最基本的命令是git diff main...feature,三个点表示对比两个分支的合并基,两个点表示直接对比两个分支的末端。大多数情况下用三个点更符合直觉,因为它只看 feature 分支引入的改动,不会把 main 分支后来的提交也算进来。

如果要看最近几次提交,可以用git diff HEAD~3 HEAD,表示对比当前提交和三次之前的提交。或者用git log -p -3直接输出最近三次提交的补丁内容。这两种方式输出的格式略有不同,解析的时候要注意兼容。

diff 的输出里,每个文件以diff --git a/path b/path开头,然后是index行、---+++行,接着是若干个 hunk。每个 hunk 以@@ -old_start,old_count +new_start,new_count @@开头,后面跟着上下文行(以空格开头)、删除行(以-开头)、新增行(以+开头)。

解析的时候,我按文件分组,每个文件保留路径和它的所有 hunk。hunk 里的行号信息要保留,因为后面要让模型指出问题在哪个文件的哪一行。但纯上下文行可以适当裁剪,比如每个 hunk 只保留改动行前后各三行,这样能大幅减少 token 消耗。

有个坑要注意:如果文件是二进制或者被重命名了,diff 的输出格式会不一样。二进制文件直接跳过,重命名的文件要特殊处理,否则解析会出错。我在代码里加了个判断,遇到Binary files开头或者rename from/to的行就做相应处理。

3.2 提示词工程:让模型输出可用的审查意见

提示词这块我改了很多版。最开始写得很简单,就是“请审查以下代码变更,指出潜在问题”。结果模型输出一大段散文,有时候还跑题去讨论代码风格,根本没法用。

后来我改成结构化提示词,分几个部分。第一部分是角色设定,告诉模型它是一个资深代码审查员,有十年以上经验,擅长发现边界条件、并发问题、资源泄漏这类隐蔽 bug。第二部分是审查维度,明确列出要检查的项:逻辑正确性、边界条件、错误处理、性能影响、可读性、安全性。第三部分是输出格式,要求每个问题用 JSON 对象表示,包含filelineseveritymessagesuggestion五个字段。

这样改完之后,输出稳定多了。但还有个问题:模型有时候会编造行号,或者把上下文行当成改动行。解决办法是在提示词里强调“只针对以+开头的行提出问题”,并且在解析结果时做校验,行号不在 diff 范围内的直接丢弃。

另一个经验是给模型提供项目背景。比如这是个什么项目、用什么语言、有什么编码规范。这些信息可以放在系统提示里,也可以从仓库里读 README 或者配置文件自动提取。有了背景信息,模型的建议会更贴合实际,不会给出“建议用 Python 的 f-string”这种在 Java 项目里毫无意义的意见。

3.3 结果解析与报告生成

模型返回的 JSON 不一定总是合法的。有时候会多一个逗号,有时候字符串没转义,有时候干脆返回 Markdown 代码块包裹的 JSON。所以解析的时候不能直接json.loads,得先做清洗:去掉代码块标记、修复常见语法错误、尝试提取 JSON 部分。

解析成功后,我按严重程度排序,criticalhigh的排前面,mediumlow的排后面。然后生成两种格式的报告:一种是终端友好的彩色输出,用 ANSI 转义码给不同严重程度上色;另一种是 Markdown 格式,方便贴到 PR 评论或者存成文件。

终端输出大概长这样:先打印一个汇总,显示总共发现多少个问题、各严重程度各多少个。然后按文件分组,每个文件下列出具体问题,包含行号、描述、建议。最后如果有问题,退出码设为非零,这样 CI 里能感知到审查不通过。

Markdown 报告则更适合存档和分享。我会在开头加一个表格,列出所有问题的概览,然后每个问题一个二级标题,详细展开。如果模型给了修改建议,会用代码块展示建议的改法。

3.4 配置管理与密钥安全

配置这块我用的是 YAML 文件加环境变量覆盖的方式。YAML 里放非敏感的配置,比如默认用哪个模型、审查哪些文件类型、严重程度阈值。API Key 这种敏感信息走环境变量,不写进配置文件,避免不小心提交到仓库。

配置文件的结构大概是这样:顶层分llmreviewoutput三个部分。llm里指定 provider、model、base_url、temperature 等参数。review里指定要忽略的文件模式、最大 diff 行数、是否启用某个审查维度。output里指定输出格式、是否写文件、文件路径。

环境变量覆盖的规则是:OCR_LLM_API_KEY覆盖llm.api_keyOCR_LLM_MODEL覆盖llm.model,以此类推。这样在 CI 里可以通过环境变量注入密钥,本地开发则可以用.env文件加载。

有个细节要注意:.env文件一定要加进.gitignore,否则密钥泄露就是分分钟的事。我在项目模板里默认就加好了,并且加了一个检查,如果发现.env被追踪就报警告。

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

4.1 环境准备与依赖安装

先说要准备什么。Git 是必须的,这个不用多说,安装教程网上到处都是,Windows 上装 Git for Windows 或者用 winget 装都行,装完记得配一下用户名和邮箱,不然提交会报错。Python 环境建议用 3.10 以上,因为用了一些新语法。包管理用 pip 或者 uv 都可以,我习惯用 uv,速度快。

依赖主要有几个:click用来做命令行参数解析,pyyaml读配置文件,httpx发 HTTP 请求调 LLM API,rich做终端彩色输出,pydantic做数据校验。这些都是成熟库,装起来没什么坑。

安装命令大概是这样:

pip install click pyyaml httpx rich pydantic

如果用 uv:

uv add click pyyaml httpx rich pydantic

装完之后,把项目 clone 下来,进入目录,跑一下python -m open_code_review --help,能看到帮助信息就说明环境没问题。

4.2 核心代码结构说明

项目目录结构我按功能划分:

open_code_review/ ├── __main__.py # 入口,命令行解析 ├── config.py # 配置加载与合并 ├── git_utils.py # Git 操作封装 ├── diff_parser.py # diff 解析 ├── llm_provider.py # LLM 接口抽象与实现 ├── reviewer.py # 审查逻辑编排 ├── reporter.py # 报告生成 └── prompts/ └── default.txt # 默认提示词模板

__main__.py里用 click 定义了几个子命令:review是主命令,config用来查看和校验配置,version打印版本。review命令接受几个参数:--base指定基准分支,--head指定目标分支,--commits指定看最近几次提交,--format指定输出格式,--output指定输出文件。

git_utils.py封装了get_diffget_current_branchget_repo_root等函数。get_diff里根据传入的参数拼出对应的git diff命令,用subprocess执行并捕获输出。这里要注意处理命令执行失败的情况,比如分支不存在、仓库没初始化等,给出友好的错误提示。

diff_parser.py是核心之一。它把原始 diff 文本解析成FileDiff对象列表,每个对象包含文件路径、变更类型、hunk 列表。hunk 里又包含起始行号、结束行号、变更行列表。解析逻辑用正则匹配文件头和 hunk 头,然后逐行处理。

llm_provider.py定义了LLMProvider抽象基类,以及OpenAICompatibleProviderOllamaProvider两个实现。前者适用于任何兼容 OpenAI 接口的服务,后者专门适配 Ollama 的 API。每个 provider 实现review方法,接收 diff 文本和提示词,返回模型输出的字符串。

reviewer.py把上面这些串起来:加载配置、获取 diff、解析 diff、构造提示词、调用 provider、解析结果、返回问题列表。reporter.py负责把问题列表渲染成终端输出或 Markdown。

4.3 关键参数的计算与选择

有几个参数需要根据实际情况调整。第一个是max_diff_lines,控制单次审查的最大 diff 行数。太少了会漏掉改动,太多了会超出模型上下文限制。我的经验值是 2000 行左右,对于大多数 PR 够用。如果超过这个数,就按文件拆分,分批审查。

第二个是temperature,控制模型输出的随机性。审查代码这种任务,需要稳定、可复现的结果,所以 temperature 设低一点,0.1 到 0.3 之间比较合适。设成 0 也可以,但有些模型在 temperature 为 0 时表现反而奇怪,所以留一点余量。

第三个是max_tokens,控制模型输出的最大长度。审查意见一般不会太长,设 2000 到 4000 就够了。设太大浪费,设太小可能截断。如果发现输出被截断,可以适当调大,或者让模型分批输出。

第四个是severity_threshold,控制报告里显示哪些严重程度的问题。默认是low,也就是全部显示。如果觉得噪音太多,可以调到medium,只显示中等问题及以上。

这些参数都可以在配置文件里改,也可以在命令行用--set临时覆盖,方便调试。

4.4 完整运行流程演示

假设你有一个仓库,当前在 feature 分支,想审查它相对于 main 分支的改动。操作步骤是这样:

第一步,确保在仓库根目录,或者用--repo指定仓库路径。第二步,配置好 API Key,比如export OCR_LLM_API_KEY=your_key_here。第三步,运行命令:

python -m open_code_review review --base main --head feature --format markdown --output review.md

工具会先打印一行“正在获取 diff...”,然后“正在调用模型审查...”,最后“审查完成,发现 5 个问题”。终端会显示彩色报告,同时把 Markdown 报告写到review.md

如果想看最近三次提交的审查,可以这样:

python -m open_code_review review --commits 3 --format terminal

如果只想审查特定文件,可以加--include "*.py"或者--exclude "tests/*"

跑完之后,如果发现问题,退出码是 1,没问题退出码是 0。这样在 CI 里可以直接用退出码判断是否通过。

4.5 接入 CI 的配置示例

以 GitHub Actions 为例,在.github/workflows/review.yml里写:

name: Code Review on: [pull_request] jobs: review: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: '3.11' - run: pip install -r requirements.txt - run: python -m open_code_review review --base origin/main --head HEAD --format markdown --output review.md env: OCR_LLM_API_KEY: ${{ secrets.OCR_LLM_API_KEY }} - uses: actions/upload-artifact@v4 with: name: review-report path: review.md

这里fetch-depth: 0很重要,默认的浅克隆拿不到完整历史,diff 会算错。API Key 放在 secrets 里,不会泄露。

如果想让审查结果直接评论到 PR 上,可以再加一步,用 GitHub 的 API 把review.md的内容发出去。不过这一步需要额外的 token 权限,配置稍微复杂一点,可以根据需要决定要不要加。

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

5.1 模型输出格式不对怎么办

这是最常见的问题。表现是模型返回的内容不是合法 JSON,或者 JSON 结构跟预期不符。排查思路分几步。

先看原始输出。在代码里加个--debug选项,把模型返回的原始字符串打印出来。很多时候一看就知道问题在哪:可能是模型加了 Markdown 代码块标记,可能是 JSON 里用了单引号,可能是某个字段名拼错了。

如果是代码块标记,解析前先做清洗,用正则把```json```去掉。如果是单引号,可以尝试用ast.literal_eval代替json.loads,它对单引号更宽容。如果是字段名问题,可以在提示词里再强调一遍字段名,或者解析时做字段名映射。

如果模型经常不按格式输出,可以考虑用 function calling 或者 JSON mode。现在很多模型服务都支持强制 JSON 输出,开启之后格式稳定性会好很多。DeepSeek 的 API 就支持response_format参数,设成{"type": "json_object"}就能强制 JSON。

还有一个技巧是在提示词里给一个示例输出。模型看到示例,模仿的概率会高很多。示例不用太长,一个完整的 JSON 对象就够了。

5.2 diff 太大导致超时或截断

大 PR 的 diff 可能上万行,直接丢给模型要么超时,要么被截断。解决办法是分批处理。

按文件分批是最自然的。每个文件的 diff 单独审查,最后合并结果。这样每个请求的输入都小很多,模型也能更专注。缺点是跨文件的问题可能发现不了,比如一个函数在 A 文件定义、在 B 文件调用,改动不一致。但对于大多数场景,按文件分批已经够用。

如果单个文件的 diff 还是太大,可以按 hunk 分批。每个 hunk 独立审查,但这样会丢失 hunk 之间的关联信息。折中方案是相邻的几个 hunk 合并成一批,保证上下文连贯。

另一个思路是先用模型做一轮粗筛,让它指出哪些文件值得细看,然后只对这部分文件做详细审查。这样能大幅减少 token 消耗,但多了一次模型调用,总体成本不一定低。

我目前的策略是:diff 小于 2000 行直接整体审查,2000 到 10000 行按文件分批,超过 10000 行先按文件分批再按 hunk 分批。这个阈值可以根据模型上下文长度调整。

5.3 API 调用失败与重试策略

网络请求失败是常态,尤其是调外部 API。常见的失败原因有:网络超时、限流、密钥无效、服务端错误。

处理策略是分层重试。对于超时和 5xx 错误,重试三次,每次间隔翻倍,比如 1 秒、2 秒、4 秒。对于 429 限流,读响应头里的Retry-After,按指示等待。对于 401 和 403,直接报错退出,重试没意义。

重试的时候要注意幂等性。审查请求本身是幂等的,重试不会产生副作用,所以可以放心重试。但如果后续加了自动修复功能,重试就要小心了,避免重复修改。

日志要记清楚。每次请求的 URL、状态码、耗时、重试次数都记下来,出问题的时候好排查。我用的是 Python 的logging模块,配置成输出到 stderr,这样不会干扰正常的报告输出。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
提示找不到 Git 仓库不在仓库目录或未初始化运行git status确认进入仓库目录或用--repo指定
diff 为空分支名写错或没有差异手动运行git diff验证检查分支名,确认有改动
模型返回空API Key 无效或额度用完查看日志中的状态码检查密钥,确认账户余额
JSON 解析失败模型输出格式不对--debug看原始输出清洗输出或开启 JSON mode
审查结果行号不对diff 解析有误对比原始 diff 和解析结果修复解析逻辑,处理边界情况
运行超时diff 太大或网络慢看日志中的耗时分批处理,调大超时时间
中文乱码编码不一致检查终端和文件编码统一用 UTF-8
退出码总是 0问题列表为空检查严重程度阈值调低阈值或检查模型输出

5.5 几个踩过的坑和独家技巧

第一个坑是 Git 的core.quotepath配置。默认情况下,Git 会把非 ASCII 文件名转义成八进制,导致解析出来的文件路径是乱码。解决办法是在命令里加-c core.quotepath=false,或者在仓库配置里设成 false。这个坑我踩了好几次才找到原因。

第二个坑是 Windows 上的换行符。Windows 用 CRLF,Linux 用 LF,diff 的时候可能会把整个文件都标成改动。解决办法是在仓库里加.gitattributes,统一换行符,或者在 diff 命令里加--ignore-cr-at-eol

第三个技巧是用git worktree来隔离审查环境。审查的时候需要 checkout 到目标分支,如果直接在开发目录操作,会打断当前工作。用git worktree add /tmp/review-branch feature可以创建一个独立的工作目录,审查完删掉就行,不影响主目录。

第四个技巧是缓存模型结果。同样的 diff 审查两次,结果应该是一样的。可以把 diff 的哈希作为 key,模型输出作为 value,存到本地文件或者 Redis 里。下次遇到同样的 diff 直接读缓存,省时省钱。不过要注意缓存失效策略,模型升级或者提示词改了,缓存就得清掉。

第五个技巧是给审查结果加置信度。让模型对每个问题给出一个 0 到 1 的置信度,低于阈值的直接过滤掉。这样能减少误报,提高信噪比。实测下来,置信度低于 0.6 的问题,大部分确实是误报。

6. 后续可以怎么扩展

这套工具目前能跑通基本流程,但离好用还有距离。我接下来想做的几个方向,也供你参考。

一个是加自动修复。模型指出问题之后,让它直接生成修复后的代码,人工确认后一键应用。这个就有点 Agent 的味道了,需要模型能理解代码上下文、能生成补丁、能验证补丁不破坏现有功能。技术上可行,但风险也大,得加严格的确认机制。

另一个是加历史追踪。把每次审查的结果存到数据库,分析哪些文件经常出问题、哪些类型的问题反复出现。时间长了能看出团队的技术短板,有针对性地做改进。

还有一个是支持多模型投票。同一个 diff 让多个模型分别审查,取交集作为高置信度问题,取并集作为待确认问题。这样能降低单个模型的偏差,但成本会翻倍。

最后是提示词的持续优化。不同项目、不同语言、不同团队,适合的提示词不一样。可以做成模板库,让用户按场景选择,也可以让用户自己写提示词存进去。

这套东西我还在持续迭代,代码放在本地仓库里,等稳定了再考虑开源。如果你也在折腾类似的东西,欢迎交流。踩过的坑、试过的方案,都可以聊。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询