
作为一个天天泡在代码评审里的开发者我越来越觉得“PR 里的 comments”是一场大型的耐心博弈。改十行代码等三个小时然后收到二十条格式意见。这倒不是说团队协作有什么问题而是大量基础性的、重复性的 review 工作本不该消耗这么多人的注意力。这也是我做open-code-review这个开源项目的最初动机——把代码审查这件事里可以被自动化的部分彻底自动化让人把精力留给真正需要“人脑判断”的地方。open-code-review是一套可自托管、开源、面向 PR/MR 工作流的自动化代码审查工具。它不只是一个简单的 lint 聚合器而是把静态规则扫描、diff 上下文分析、LLM 语义级审查、结果聚合与机器人评论整合到了同一个流程里。简单说你提交一个 PR它自动把改动拆开、扫描、分析然后在 PR 底下直接留下结构化评论告诉你有哪里可能出错、哪里写法不够好、哪里测试没覆盖到。整个过程不需要把代码上传到任何第三方闭源服务所有逻辑都可以跑在自己的 CI 或服务器上对代码安全有要求的团队尤其合适。这篇文章我把整个项目从思路到落地都拆开讲为什么要这么做、核心模块怎么设计、如何快速接入 GitHub Actions / GitLab CI以及我在实际使用中踩到的坑。对于正在搭建自己团队的自动化 review 体系或者单纯想从“重复性评审”里解脱出来的朋友应该会有帮助。1. 项目定位与整体设计思路1.1 先想清楚一个问题代码审查到底在审什么我自己的经验是一次正经的代码审查通常包含这么几层内容。第一层是风格和规范比如缩进、命名、import 顺序、是否违反团队 lint 规则第二层是明显的逻辑问题和边界情况比如空指针、数组越界、并发安全、资源泄漏第三层是设计和语义层面比如这个方案架构上是否合理、接口抽象是否恰当、是否有更简洁的实现方式第四层是测试覆盖和变更影响比如改动是否触及了关键链路、有没有引入回归风险。传统工具解决得最好的是第一层像是 ESLint、Stylelint、SonarQube 都做得很成熟。第二层和第三层过去主要靠人工 review但人工的问题是慢、依赖个人经验、状态不稳定。open-code-review的思路就是把第一层继续交给规则引擎同时用 LLM 补上第二、第三层的一部分能力再把第四层用准确的 diff 分析和影响面计算做提示。它不是替代人而是把人的注意力从“找茬”转移到“决策”。1.2 为什么没有直接选商业方案或现成 GitHub App先说结论不是商业工具不好而是很多人没意识到“代码上云”这件事本身就是一种成本。我见过不少团队想接入 AI 代码审查结果往 GitHub 上装一个第三方 App代码就得到第三方服务器跑一遍。代码安全要求高的团队根本过不了合规那一关就算合规允许团队里也有人会担心“我的私有仓库被拿去训练模型了”。所以我在设计open-code-review的第一天就定了个原则所有分析都跑在自己手里LLM 调用也可以指向任意兼容 OpenAI 协议的接口包括本地部署的模型。代码不出内网这是底线。另外一个考虑是工作流整合。很多静态分析工具跟 PR 评论的打通能力很弱或者评论质量很差——要么一次评论轰炸几十条要么只在 summary 里含糊其辞。我就想要一个评论风格接近“资深工程师同事”的工具有定位、有严重级别、有可落地的修法说明而不是冷冰冰的“这里违反了规则 X”。这需要很细致的 prompt 工程和结果聚合逻辑不是简单调一下 API 就行的。1.3 整体模块划分四个部件互相配合我最后把项目拆成了四个核心模块每一个都可以独立使用也可以整体组装。diff 解析与变更范围裁剪模块负责分析 PR 的变更提取出“新增/修改/删除”的代码块和对应文件生成结构化的代码变更上下文供后续模块消费。静态规则引擎内置一套基于 AST 的规则集合同时支持自定义规则可以不依赖 LLM 就完成低延迟的快速检查。LLM 语义审查模块把 diff 内容、文件路径、语言类型、仓库上下文组织成 prompt调用后端模型进行语义级审查负责发现接口设计、异常处理、逻辑边界等问题。结果聚合与通知模块把规则引擎输出和 LLM 输出统一成一致的数据结构合并去重、按严重级别排序然后以 PR review comment 或消息通知的形式投递出去。这四个模块不是强耦合的。比如你只想用规则引擎做本地检查那可以直接跑 CLI你只想用 LLM 模块做语义分析也可以单独调用。这种设计让项目在不同团队里的落地路径很灵活——有人从 CI 接入开始有人从本地命令行开始最后都会慢慢统一到完整流程里。2. 核心实现拆解每个环节都做了什么2.1 diff 裁剪先搞清楚“只审改了什么”LLM 是有上下文窗口限制的而且 token 就是钱。把一个 PR 的全部文件内容都丢给模型不仅贵还会严重稀释重点。所以 diff 裁剪是整个流程里性价比最高的一步。我实测下来一个 500 行改动的小型 PR如果不做裁剪直接塞全文模型经常把注意力放在背景代码上反而忽略真正被改动的几行裁剪之后模型能非常准确地指出问题所在的函数和边界条件。这块我用了类似这样的流程先拿git diff origin/main...HEAD拿到原始 patch然后用一个增强解析器去解析 hunk 头定位到具体文件和行号。接下来对每个变更文件提取出变更的行号列表再加上变更行所在函数的上下文。这个“函数上下文提取”特别关键因为只看三行改动往往不知道它们属于哪个逻辑块得带着外层函数的签名、参数、返回逻辑一起送进去。具体实现上我用的是 tree-sitter 来做语言的原生 AST 解析。tree-sitter 的优势是解析速度极快而且对语法错误有容忍度——PR 的代码经常解析不了完整的 AST这时容错能力就显得很重要。拿到 AST 之后我按行号反查节点找到包含变更行的最小作用域节点比如函数声明或者类型定义再把整段子树序列化出来。# 伪代码按变更行提取函数上下文 import tree_sitter def get_surrounding_function(ast_root, target_line): # 遍历 AST找到包含 target_line 的最小函数节点 stack [ast_root] best_match None while stack: node stack.pop() start_line node.start_point[0] 1 end_line node.end_point[0] 1 if start_line target_line end_line: if node.type in FUNCTION_NODE_TYPES: best_match node # 继续向下找更精确的匹配 stack.extend(node.children) return best_match这一层做完每个变更文件会产出一个 JSON 结构包含文件路径、语言、变更类型、变更行号、相关函数/类的代码片段、变更前后的行内容。这比裸的 diff 文本友好得多后续无论是规则引擎还是 LLM 模块都能直接消费这些结构化数据。2.2 静态规则引擎为什么还需要“无脑扫”很多人会问都用 LLM 做审查了还要规则引擎干嘛我的回答是规则引擎负责两件事——拦截确定性问题和给 LLM 减负。比如某个包是不允许使用的、某个函数必须加超时参数、某类密钥不允许出现在代码里这些都是确定性规则LLM 反而可能因为上下文不同而漏判。规则引擎跑起来只要几百毫秒而且不会受模型随机性影响结果是 100% 可复现的。还有一个容易被忽略的价值规则引擎可以给 LLM 提供“额外证据”。当我发现 diff 里有一行调用了某枚举里的危险方法时规则引擎会先报一条静态检查结果我在组织 LLM prompt 时会把这条结果作为“可疑点提示”塞进去。这样 LLM 不只是凭自己的常识在猜而是会针对规则引擎怀疑的点重点展开分析。实测下来结合静态规则提示的 LLM 审查误报率能下降不少。我内置的规则主要有几类密钥和敏感信息扫描、禁止使用的 API 模式检测、import 黑名单、危险的正则表达式模式、潜在的资源未释放模式等。所有规则都支持通过配置文件开关和自定义团队可以根据自己的技术栈积累内部规则。2.3 LLM 评审模块prompt 是一半的工程LLM 评审是open-code-review里最有挑战也最好玩的部分。先说结论模型能力很重要但 prompt 的组织方式和结果的后处理同等重要甚至在很多场景下更重要。我设计了一套名为“review context pack”的 prompt 组织结构主要包含三块内容角色与任务说明、变更信息、以及必须遵守的输出格式约束。角色说明不是简单写“你是一个代码审查专家”而是明确告诉模型这是一个 PR review 场景输入是 diff输出是结构化 JSON。变更信息按文件分组每个文件附带文件路径、变更行号列表、相关函数代码片段和对应的静态规则扫描结果。输出约束则是最关键的——我要求模型严格输出 JSON每条评论必须包含file、line、severity、category、title、suggestion六个字段并且没有找到问题时必须输出空数组而不是编造内容。{ reviews: [ { file: src/auth/token.go, line: 87, severity: warning, category: error_handling, title: token 过期错误被静默吞掉, suggestion: 这里捕获了 ErrTokenExpired 却只 break 不返回上层会误认为认证成功。建议返回明确错误或重定向到刷新流程。 } ] }另外我还加了一个“不确定就闭嘴”的设置。很多 AI review 工具的致命缺陷就是喜欢硬找问题一段很正常的代码也能给你挑出两个“建议优化”。结果就是开发者在 PR 里看到十条评论有七条是废话从此再也不看机器人评论了。为了对抗这一点我把 prompt 里的一个关键短语写得很死“如果你认为某处没有问题就不要输出任何内容。检查不确定时选择什么都不说。”这在一定程度上牺牲了召回率但换来了极其宝贵的信噪比用户信任度完全不一样。模型的调度上我默认走得是 OpenAI 兼容的 chat completions 接口所以你可以在配置里填一个内部网关地址也可以填本地 vLLM 的地址甚至可以填一个面向大厂云的兼容端点。不同的模型对复杂任务的跟随能力有差异我用相同 prompt 实测过大模型的输出兼容性明显好于轻量模型但轻量模型配合规则引擎在性价比上也很能打——一句话不要迷信模型要根据预算选。2.4 结果聚合与 PR 评论过滤属于 review 师的尊严LLM 的原始输出直接发到 PR 里效果其实很灾难。我会做三层处理。第一层是格式清洗。LLM 偶尔会输出 Markdown 混排的 JSON必须做一次强解析和字段补齐解析失败就丢弃该条结果而不是报错。第二层是去重合并。规则引擎和 LLM 可能会对同一行提出问题比如规则引擎说“这里调用了危险函数”LLM 说“这个函数没有检查返回值”这两个可以合并成一条综合评论让最终评论列表更干净。第三层是严重级别加权。合并完的结果按error→warning→suggestion排序并且我会把每条评论限制在 120 字以内的摘要加上可选的折叠详情避免刷屏。评论的投递方式上open-code-review支持两种主要模式。第一种是直接以 GitHub review comment 的形式提交每条评论定位到具体代码行第二种是整份报告以单一评论发布包含所有问题的 summary 列表和定位信息。前者适合 CI 流程已经跑通、MR 较小的情况后者适合首次接入时的试跑毕竟一上来就几十条行内评论会把人吓跑。3. 实操记录从零开始接入你的仓库3.1 环境准备与安装我以最常见的 GitHub Actions 接入方式举例。首先准备一个 Linux 环境的 runner建议内存至少 2GB因为我同时跑了规则引擎和 LLM 调用如果你还要在本机起 LLM 模型那内存建议 16GB 起步但大多数团队会直接调内网已有的模型网关。CLI 的安装很简单我提供了预编译的二进制文件也可以直接用 Docker 跑# 下载最新 release wget https://github.com/yourorg/open-code-review/releases/latest/download/open-code-review-linux-amd64.tar.gz tar -xzf open-code-review-linux-amd64.tar.gz sudo mv open-code-review /usr/local/bin/ # 或者 docker 方式 docker pull open-code-review:latest安装完后先跑一次open-code-review --version确认环境没问题。接着需要配置两个东西一个是模型的访问凭证另一个是仓库的访问凭证。模型凭证可以直接放在 CI 环境的 secret 里也可以通过环境变量传递仓库访问凭证主要用于 POST review comment需要一个有pull_requests: write权限的 token。3.2 配置文件一份能直接拿来改的模板在项目根目录创建一个.open-code-review.yml内容大致如下# .open-code-review.yml version: 1 languages: - go - python - typescript diff: context_lines: 5 function_context: true rules: enabled: true custom_config: .review/rules.yaml llm: endpoint: ${OPEN_CODE_REVIEW_LLM_ENDPOINT} api_key: ${OPEN_CODE_REVIEW_LLM_API_KEY} model: deepseek-v3 temperature: 0.1 max_tokens: 2000 timeout: 60 review: max_comments: 20 min_severity: warning comment_mode: inline notify: on_summary: true summary_title: open-code-review 报告这里有几个参数需要特别说明。diff.context_lines控制了 diff 上下文行数我建议设置成 5太少看不到逻辑全貌太多浪费 token且可能跨函数产生误导。llm.temperature我强烈建议压低到 0.1~0.2审查场景不需要发散稳定输出比创造性更重要。max_comments是保险阀防止某个文件被 LLM 反复挑刺导致评论爆炸根据我的经验 20 条以内是团队能接受的极限。规则引擎的独立配置在.review/rules.yaml比如你可以定义一条禁止使用exec的规则rules: - id: no-exec-usage pattern: exec( message: 禁止在生产代码中使用 exec 执行外部命令请使用标准库的安全替代方案。 severity: error languages: [python]3.3 接入 GitHub Actions完整 workflow 示例我把整个流程固化成一个 workflow 文件这样每次 PR 都会自动触发。核心逻辑是checkout 代码 → 计算 diff → 运行open-code-review→ 脚本收集输出 → 调用 GitHub API 发评论。name: open-code-review on: pull_request: types: [opened, synchronize] jobs: review: runs-on: ubuntu-latest permissions: contents: read pull-requests: write steps: - uses: actions/checkoutv4 with: fetch-depth: 0 - name: Run open-code-review env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} OPEN_CODE_REVIEW_LLM_ENDPOINT: ${{ secrets.LLM_ENDPOINT }} OPEN_CODE_REVIEW_LLM_API_KEY: ${{ secrets.LLM_API_KEY }} run: | open-code-review review \ --repo ${{ github.repository }} \ --pr ${{ github.event.pull_request.number }} \ --base ${{ github.event.pull_request.base.sha }} \ --head ${{ github.event.pull_request.head.sha }} \ --format json \ --output review_result.json - name: Post review comments uses: actions/github-scriptv7 with: script: | const fs require(fs); const result JSON.parse(fs.readFileSync(review_result.json, utf8)); for (const review of result.reviews) { await github.rest.pulls.createReviewComment({ owner: context.repo.owner, repo: context.repo.repo, pull_number: context.issue.number, commit_id: context.payload.pull_request.head.sha, path: review.file, line: review.line, body: **${review.severity}** · ${review.category}\n\n${review.title}\n\n${review.suggestion} }); }这个 workflow 跑起来后PR 页面会逐渐出现机器人的行内评论。特别提醒一下GitHub API 对同一 PR 的 review comment 有限制如果评论很多我的脚本会自动把多余的评论折叠成 summary 而不是硬塞。这也是max_comments参数在发挥作用。3.4 本地命令行模式不提交 PR 也能用有时候你只是想在本地写代码的过程中快速查一下当前分支有没有明显问题那就不需要走 CI。open-code-review提供了一个本地模式直接对比当前工作区和目标分支open-code-review scan \ --base origin/main \ --format terminal \ --rules .review/rules.yaml \ --llm-endpoint http://localhost:8000/v1 \ --llm-model qwen2.5-coder:14b输出是这样的[error] src/auth/token.go:87 token 过期错误被静默吞掉 建议捕获 ErrTokenExpired 后应返回 401而不是 break 继续执行。 [warning] src/api/user.go:132 缺少对 req.Limit 的范围校验 建议如果 Limit 大于 100建议截断或直接返回 400。这种模式特别适合写代码过程中的“自查”也适合在提交前扫一遍自己的改动省得 PR 一开就被机器人挑刺。我自己的使用习惯就是本地先跑一遍修复掉明显的问题再提交到了 CI 阶段评论量就会少很多也不会觉得机器人很吵。4. 参数调优与性能优化4.1 LLM 参数里最容易犯的错很多人刚接入的时候会把temperature保持默认的 0.7 或者 1.0然后发现同一个 PR 重新跑一次评论内容完全变了甚至有时候说有问题有时候说没问题。这就是温度太高带来的随机性。审查场景必须低温度我目前生产配置是temperature: 0.1偶尔用到 0.2但绝不会更高。max_tokens也是个坑。如果设小了模型的回复会被截断很可能输出一个不完整的 JSON 或者写到一半的评论。我建议至少 2000如果改动文件很多可以到 4000。timeout建议 60 秒起步因为很多长 diff 的推理时间会超过 30 秒如果超时太短会大量失败重试反而更慢。还有一个很多人会忽略的参数是top_p我建议保持在 0.9 左右或者干脆不设置。配合低温度它能进一步压缩模型的输出多样性让结果更稳定。4.2 规则引擎和 LLM 的分工怎么调每家的代码库特点和团队规范都不一样所以open-code-review允许你调整“哪些问题交给规则引擎哪些交给 LLM”。我的建议是凡是能被确定性规则描述的问题一律交给规则引擎凡是需要读代码理解语义才能判断的问题才交给 LLM。这样做的原因很简单——规则引擎不花钱LLM 花钱。能省钱的地方就省钱。实践中我发现一个非常有效的工作流第一版先打开所有规则引擎内置规则收集一个月内它报出的问题然后看哪些类型的规则经常误报关掉或调整同时把误报率高的类型加入到 LLM prompt 的提示里让模型在语义判断时多留个心眼。这是一套不断迭代的闭环而不是一个静态配置。4.3 性能瓶颈与缓存策略在大的 monorepo 仓库里扫描 100 个文件的 diff 很容易耗时几分钟其中大头就是 LLM 调用。我的策略是先跑规则引擎做快速扫描再对可疑文件做 LLM 审查。规则引擎没有命中的文件就不进 LLM 队列。另一个优化是文件级缓存。如果某个文件在本次 diff 中的内容跟上次审查时一致就直接用上次的结果不再调用模型。并发度也需要控制。直接给同一个模型网关同时发 20 个并发请求很容易触发限流。我实测下来并发控制在 3~5 是最稳的既不会触发 429也不会让总耗时长到离谱。如果仓库很大建议使用消息队列把审查任务异步化CI 里只发一个 pending 状态审查完再更新 PR。5. 常见问题与排查技巧实录5.1 高频问题速查表我把自己和早期用户在使用过程中遇到的问题整理成了一份速查表覆盖了绝大多数情况。症状可能原因排查方法评论没有出现在 PR 页面GITHUB_TOKEN 权限不足检查 workflow 的permissions确认pull-requests: writeLLM 返回大量空数组prompt 里“不确定就闭嘴”起效或模型输出格式不匹配先降低要求看原始响应再调整模型评论定位的行号不准确diff 基于三路合并行号和 PR 显示不一致检查是否用了正确的 base/head sha规则引擎没有生效自定义规则语法错误本地跑open-code-review rules --validate验证流式输出导致 JSON 截断max_tokens太小调大到 4000 再试大 PR 长时间卡住LLM 并发太低且文件太多开启文件级缓存或者调大并发到 5同一个问题被重复报告规则引擎和 LLM 同时命中检查合并去重逻辑必要时升级版本本地扫描时 SSL 报错自建模型网关证书不受信任配置insecure_skip_verify: true或用受信证书5.2 三个让我印象深刻的踩坑实例第一个坑是行号偏移问题。刚开始我直接用git diff的旧行号当作 PR 评论的行号结果评论全部错位——有的指向了完全不相干的代码有的指向了空行。后来我把base sha和head sha明确传进命令并且统一以head sha对应的文件行号为准才彻底解决。如果你也遇到评论位置不准优先检查这里。第二个坑是 prompt 的“过度怀疑”倾向。有一版 prompt 我为了提高召回率写了一句话“请指出任何你认为可能改进的地方。”结果模型对每段代码都输出两三条建议整个 PR 评论像瀑布一样。后来我把这句话删了换了更严谨的表述“只报告确定有问题的点优化建议一律不要提。”效果立刻不一样——评论量下降了 70%但被保留的评论价值提升了好几倍。第三个坑是自建模型的并发问题。我们当时用 vLLM 部署了一个开源模型open-code-review用默认并发 2 跑得很稳后来把并发拉到 10 想提速结果网关直接 OOM服务挂了。后来我加了每文件一分钟的全局超时并把并发稳定在 4问题再也没出现。跑 AI 审查这种事情稳定比快更重要因为 CI 挂掉的代价远高于多等一分钟。5.3 独家避坑技巧聊几个文档里不会写但实际特别有用的技巧。第一配置notification_on_error: true让工具在异常时主动在 PR 里留一条“审查器遇到内部错误请稍后重试”而不是静默失败。很多 CI 工具都是挂得无声无息然后开发者以为“审查器没问题”其实什么都没查。这个开关能避免误判。第二把规则引擎和 LLM 的评论分开推送别合并成一条。我一开始图省事把静态检查和语义检查的结果合并成一套数据再发结果所有问题混在一起让人分不清哪些是代码事实、哪些是 AI 建议。后来我改成“静态规则命中用普通评论LLM 分析用 review summary 单独解释”整体接受度一下子就上来了。第三如果你用的是开源模型建议在 prompt 里附带一个 few-shot 示例给模型看一条“好的 review 评论”长什么样。很多小模型如果没有参考输出经常是空泛的套话。我在配置里加了一个可选的few_shot_examples字段效果非常显著——模型输出的针对性立刻高了很多。6. 未来扩展与一些个人体会项目跑起来之后我最大的感受不是“AI 替我 review 了多少代码”而是“团队 review 的文化变了”。因为机器人承担了那些让人烦躁的格式检查、基础逻辑检查开发者自己 review PR 的时候心态更放松了反而更容易关注到架构层面的问题。以前那种动辄 30 条 comments 的 PR 迎来了它的终结。扩展方向上我接下来最想做的是“历史模式分析”——把每次审查的结果和时间关联起来看同一个文件的历史问题趋势这样能帮助团队识别出哪些模块常年存在问题、需要重构而不是只做点对点的修改。另外一个方向是支持自定义 review 规则的带内反馈机制让开发者在评论里回复“ignore”或者“不是问题”系统自动学习并降低同类问题的报告优先级。项目还很年轻但它已经解决了让我最头疼的问题代码审查不再是一场比拼耐心的消耗战。如果你也在被重复性 review 折磨不妨自己搭一套哪怕是先用规则引擎跑起来再一点点加上 LLM 能力和团队自己的规则这个过程本身就会让你对你代码库的“体质”有完全不同的认识。