去年我把一个内部用了很久的代码审查工具整理成了开源项目起了个名字叫open-code-review。本意很简单代码审查这件事不应该只靠人肉盯着 diff 来回看也不应该被商业工具锁死。它在 GitHub 上挂了几个月反馈比我预期的好不少人问部署方式、架构设计还有怎么接入现有 CI。今天就把这个项目的完整思路、核心实现和实操过程一次说清楚。这个项目适合谁如果你团队还在用“拉分支→群里催review→人工过diff→改完再看一遍”的纯人力流程或者你正打算引入 AI 辅助代码审查但从何下手再或者你想自己搭一套内网可用的审查系统那我建议你花十几分钟把这篇读完。里面包含了我从零到一搭建这套体系的全部关键决策和踩坑记录。1. 项目整体设计与思路拆解1.1 为什么选择自己做开源代码审查工具先说下为什么要造这个轮子。团队之前用过的方案大概分两类一类是 SaaS 代码托管平台自带的基础审查功能能看 diff、留评论、提交检查清单但它只是个流程壳子不会帮你分析代码质量问题。另一类是商业代码质量管理工具功能确实全一套下来License费用不低而且很多规则粒度不是你想要的样子。真正让我下定决心自己做的触发点是团队引入 AI 辅助编程之后代码产量上来了但是 review 变成了瓶颈。每次 MR 动辄几百上千行靠人一遍遍翻累不说效果还很差。我需要一个能自动分析 diff、跑规则检查、给出审查意见、还能跟现有代码托管平台打通的工具。于是 open-code-review 的定位就清楚了开源的、可私有化部署的代码审查助手能自动分析合并请求里改动的代码内置规则引擎处理确定性检查项支持接入大模型做语义层面的智能分析审查结果能通过插件推送到代码托管平台、IM 工具它不是一个替代人工 review 的方案而是一个把人工 review 效率拉高的辅助系统。**人还是要做最终的判断但重复性的、确定性的、低层次的问题机器先帮你扫掉。**这一点从一开始就明确下来也决定了后面整个架构的走向。1.2 规则引擎加 AI 双通道的架构决策项目最早期的原型只有一个规则引擎解析 diff 文件根据预先定义的规则模式匹配代码片段命中就输出一条问题记录。这套东西跑起来以后效果是有的但暴露出一个明显短板——规则只能识别“写的模式不对”识别不了“这段代码是不是真正缺了一个边界判断”。前者是语法层面的后者是语义层面的。所以在 1.0 版本之后我引入了第二条分析通道LLM 通道。diff 内容经过裁剪和格式化之后发送给大模型让模型从设计、安全性、可维护性几个维度输出审查意见。两条通道的结果在报告聚合层合并去重之后统一格式化输出。之所以不做成纯 LLM 方案有一个很现实的考虑审查成本和响应速度。规则引擎是本地执行的毫秒级出结果这是任何模型调用都比不了的。团队里每天几十个合并请求如果全部走模型成本完全不受控。让规则引擎先把低级问题筛掉把剩下的值得“思考”的片段交给模型这才是合理的资源分配方案。2. 核心技术细节与实现要点2.1 Diff 解析的细节处理和补丁归一化解析 diff 是整个项目的根基最开始我直接用字符串匹配去读 diff很快发现不可行。不同代码托管平台返回的 diff 格式有细微差异比如 GitHub 的补丁格式里包含了index行和--- a/xxx、 b/xxx的行前缀GitLab 在某些场景下返回的格式略有不同。另外行尾处理必须统一否则带 CRLF 的 Windows 项目能让你解析结果错位。经过几个版本的迭代我把 diff 解析抽成了独立模块核心规则如下统一将换行符归一化为\n统一使用 UTF-8 编码读取文件内容按文件单元切分大 diff每个文件内部再按 hunk 切分解析 -start,count start,count 头建立新旧行号的映射关系删除行-开头和新增行开头分别记录上下文行保留供后续分析行号映射这个点特别重要。规则引擎最后要输出“哪个文件的第几行有问题”如果行号对不上审查结果就是废的。GitHub 和 GitLab 对评论行号的传递机制也不一样需要针对不同平台做适配层。2.2 规则引擎的设计思路规则引擎的核心是一组预定义的规则函数。每个规则函数接收一个上下文对象上下文里包含当前文件路径、语言类型、hunk 内容、行号映射表等。规则函数返回一个或多个问题对象。我选了 Python 作为项目主语言所以规则就是普通的 Python 函数用装饰器注册到规则中心。比如一个典型的防止调试代码残留的规则rule_register.register( rule_idPY001, namedebug_breakpoint_check, file_types[*.py], severitywarning, ) def check_breakpoint(ctx): issues [] for line_no, line in ctx.new_lines: stripped line.strip() if stripped.startswith(breakpoint()) or stripped.startswith(pdb.set_trace()): issues.append(Issue( rule_idPY001, lineline_no, message检测到调试断点残留请在提交前移除。, suggestion删除调试语句或改用日志方式输出调试信息。 )) return issues定义规则机制的花样很多关键不是写规则本身而是让规则“好写、好测、好维护”。每一条规则都对应一个独立的测试用例构造最小的 diff 片段来验证命中与不命中两种情况所有规则必须覆盖这两个分支。这个设计让后来新增规则的成本变得很低——复制一个已有规则文件改逻辑、加用例跑一遍测试就完事。到目前为止项目内置了覆盖 Python、JavaScript、Java、Go 四门语言的四十多条基础规则。2.3 LLM 通道的上下文裁剪策略LLM 通道是项目最复杂也最容易出错的地方。最开始我尝试过把整个 MR 的 diff 全量塞给模型结果很快就遇到了两个问题一是大 MR 的 diff 能有好几万 token模型输入长度直接爆掉二是上下文太长之后模型对重点的聚焦能力明显下降给出的意见开始泛泛而谈。后来我采用了分层裁剪策略。第一步在文件级别做裁剪把超过设定阈值的大文件过滤掉只保留最值得审查的改动文件。第二步在文件内部做裁剪只保留新增行和带有上下文的关键片段删除大量纯移动代码的冗余行。第三步在对话级别做裁剪给模型的任务描述保持精简让它的注意力集中在代码本身。当前版本我用的请求模板分为三个部分系统角色定义说明你是资深代码审查专家输出格式必须为 JSON代码变更输入裁剪后的 diff 内容输出约束必须返回包含问题列表、行号、严重级别、建议修复方案的 JSON 结构模型选型上私有化部署用本地模型追求效果可以直接接入商用模型 API。无论哪种都要在请求参数中把temperature调低我一般设置在 0.2 到 0.3 之间确保输出稳定性优先。2.4 审查报告与去重合并机制两条通道的结果出来之后不能直接拼在一起就完事因为很可能出现同一处问题被两边重复报。合并去重逻辑我做了两个层面。第一层是锚点去重以“文件路径 行号区间”为锚点如果规则引擎和 LLM 命中了同一个锚点区间优先保留规则引擎的结果它更精确且附带规则文档链接。第二层是语义去重这个问题难一点因为模型报出来的行号和实际行号可能有偏差。我实现了一个模糊匹配算法将 LLM 结果中的行号与规则引擎命中的行号做邻近匹配偏差在三行以内视为同一条问题再进行内容相似度比对相似度超过阈值的合并为一条。报告最终的输出格式我定为 Markdown。之所以不用 JSON 或 XML是因为这份报告最终要展示给人看Markdown 最自然既能在终端里直接读也能转成 HTML。3. 实操过程与核心环节实现3.1 整体架构与模块划分写到这里有必要把整个项目的架构做一个总览。open-code-review 是一个典型的管道式架构数据流是单向的代码托管平台事件 → 拉取 diff → 解析归一化 → 双通道分析 → 合并去重 → 报告生成 → 插件分发。项目目录结构大致如下open-code-review/ ├── core/ │ ├── diff_parser.py │ ├── rule_engine.py │ ├── llm_channel.py │ ├── report_merger.py │ └── models.py ├── rules/ │ ├── python_rules.py │ ├── javascript_rules.py │ ├── java_rules.py │ ├── go_rules.py │ └── common_rules.py ├── integrations/ │ ├── cli.py │ ├── github_app.py │ ├── gitlab_webhook.py │ ├── local_git.py │ └── webhook_server.py ├── plugins/ │ ├── console_output.py │ ├── markdown_report.py │ ├── github_comment.py │ ├── gitlab_note.py │ └── feishu_sender.py ├── config/ │ ├── default_config.yaml │ └── rule_catalog.yaml └── tests/ ├── test_diff_parser.py ├── test_rules_python.py ├── test_report_merger.py └── test_plugins.py这个划分方式的思路是核心分析逻辑和外部对接彻底分离。换平台只动integrations/目录加审查能力只动core/和rules/改通知方式只动plugins/。后来接的几个平台都验证了这个决策是对的改动范围完全被限制住了。3.2 最小完整可复现的本地接入步骤如果你把这个仓库拉下来想最快速度跑通一次完整的代码审查我的建议是先不要接任何远程平台直接用本地 Git 仓库做测试。这种方式排除掉一切网络和环境干扰方便验证核心逻辑。操作流程如下创建虚拟环境并安装依赖python3 -m venv venv source venv/bin/activate pip install -r requirements.txt准备一个测试用的本地 Git 仓库在里面创建几个故意包含问题的文件比如在 Python 文件里写一个breakpoint()在 JavaScript 文件里不用而用在 Go 文件里把错误忽略掉修改配置中心的local_git段填入仓库路径和待检查的最新提交号执行python -m open_code_review.cli --platform local --repo-path ./demo-repo --commit HEAD执行完之后CLI 会输出一份文本形式的审查报告。这份报告大致长这样审查目标: demo-repo (commit 3f2a1c0) 审查时间: 2025-01-15 14:32:08 审查文件数: 6 发现问题总数: 5 ├─ 严重问题: 1 ├─ 警告: 2 └─ 提示: 2 [PY001] 检测到调试断点残留 src/utils/logger.py:42 建议: 删除断点语句改用 logging.debug() 输出关键变量 [JS002] 使用了宽松相等运算符 web/component.js:88 建议: 使用 替代 避免隐式类型转换看到这个输出格式就说明核心分析链路已经通了。本地模式下输出的报告和 GitHub 模式下输出的报告内容上完全一致只是分发途径不同。这个设计是为了保证插件层的可替换性审查逻辑从分发逻辑中彻底解耦。3.3 接入 GitHub 和 GitLab 的完整流程本地跑通之后下一步就是接入真实代码托管平台。我做了一套统一的 Webhook 服务兼容 GitHub 和 GitLab 的事件推送。GitHub 接入过程需要注意几个细节。在 GitHub 设置里添加 Webhook 时Payload URL 指向你的服务地址Content type 必须选择application/json触发事件要勾选Pull requests和Issue comments。GitHub 会发送X-GitHub-Event请求头标识事件类型服务端需要校验这个头只处理pull_request事件且 action 为opened、synchronize、reopened的情况。GitLab 的接入大同小异事件头变成了X-Gitlab-Event事件类型是Merge Request Hook。部署在内网的 GitLab 需要注意一个问题Webhook 回调的目标地址必须能被 GitLab 服务器访问到如果 GitLab 和审查服务不在同一内网网段需要在防火墙放行。接入步骤我整理成了如下清单在你的服务机器上启动 webhook 服务python -m open_code_review.webhook_server --host 0.0.0.0 --port 8123在代码托管平台上创建 WebhookURL 填写http://你的服务器IP:8123/webhook/github或http://你的服务器IP:8123/webhook/gitlab如果平台在公网审查服务在内网需要做内网穿透或者反向代理建议直接用 Nginx 搭配proxy_pass处理仓库中创建.open-code-review.yml配置文件按需求启用或禁用规则设置各严重级别是否阻断合并Webhook 服务内部的处理逻辑并不复杂收到事件请求后提取仓库地址、分支、MR/PR 编号信息 → 调用对应的平台 API 获取合并请求详情和改动的文件列表 → 依次拉取每个文件的 diff 内容 → 交给核心分析管道。关键点在于和代码托管平台的 API 认证GitHub 用 Personal Access Token 或者 GitHub App 安装令牌GitLab 用 Personal Access Token 或者 Project Access Token。我这里建议用专用机器账号的 Token不要直接用个人的否则账号变动会导致整个审查服务失效。3.4 配置体系的组织方式这个项目的配置体系经历了多次重构最终稳定为“全局配置 仓库配置 命令行覆盖”的三层结构。全局配置文件default_config.yaml设置了所有项目的默认行为包括模型 API 地址和密钥部署在服务器环境变量里、审查开关、通知渠道等。仓库配置.open-code-review.yml放在被审查的代码仓库根目录允许项目之间有不同的规则启停、严重级别阈值、豁免路径等。命令行覆盖则是最高优先级适合临时调试。仓库配置文件的示例review_rules: enabled: true rule_level: PY001: error PY002: warning JS001: off llm_review: enabled: true model: gpt-4o-mini max_files: 20 max_file_size: 2048 report: output_format: markdown send_to: [github_comment, feishu] ignore_paths: - build/ - dist/ - node_modules/配置体系里最容易被忽略的是ignore_paths配置。没有它依赖锁定文件比如package-lock.json、自动生成的代码文件会大量涌入审查管道把规则引擎和 LLM 的调用次数顶上去浪费算力还污染报告。我在实战中把yarn.lock、poetry.lock、vendor/等路径全部加进了默认忽略列表审查信号质量立刻明显提升。3.5 插件系统的扩展方式插件系统让这个工具从“自己用”变成了“团队用”。最初版本的报告只在终端里打印后来自用的时候加了 Markdown 报告再后来接 GitHub 评论、GitLab 讨论、飞书通知。每加一个渠道改动一次主程序这样搞了几次之后我意识到必须把分发逻辑抽象出来。插件接口的定义很简单任何插件只需要实现一个函数接收一份统一的报告对象返回一个布尔值表示是否发送成功class ReviewPlugin(Protocol): def send_report(self, report: ReviewReport) - bool: ... def plugin_name(self) - str: ...内置的三个官方插件分别覆盖了三种典型场景console_output终端输出适合本地调试github_comment审查完成后在 GitHub 对应 PR 下自动发表评论按“严重问题/警告/提示”三个区块组织内容feishu_sender异步推送到飞书群机器人适合团队成员不常看 GitHub 通知的场景有个细节值得说审查结果的评论里我特意在每条问题后面加了一个很简短的“为什么”说明比如此写法会导致隐式类型转换可能隐藏字符串与数字比较的bug。这个设计并非出于教育目的而是实践验证出来的——当审查意见带有解释时开发者修改代码的意愿明显更强而且可以减少评论区的质疑和来回讨论。4. 常见问题与排查技巧实录4.1 Diff 解析错位问题这是开发过程中遇到最多的一类问题。现象是规则引擎报的行号和实际代码位置对不上有明显的系统性偏移。排查过程比较典型。一开始我怀疑是行号映射算法写错了反复检查 a,b c,d 的解析逻辑还专门加了单元测试覆盖各种 diff 边界情况但问题依旧。后来打日志发现某些文件的二进制内容被 Git 误判为文本在 diff 里出现大量不可见字符行数统计全乱。另外文件末尾没有换行符也会导致解析结果偏行。解决办法是双管齐下一是在 diff 解析前增加git diff --numstat预判如果文件改动行数异常大或包含 NUL 字符直接判定为二进制文件跳过二是文件末尾缺失换行符的边界情况在解析器里做了标记不参与行号偏移计算。这两个修复上线后再没有出现过系统性错位。4.2 LLM 返回内容不稳定问题LLM 通道刚上线那段时间模型经常返回格式不完整的内容。有时候 JSON 解析到一半断了有时候多了一个外层对象有时候返回了一堆解释性文字而不是纯 JSON。网上很多解决方案是“换一个大模型的提示词试试”但我的结论是靠提示词约束响应格式稳定性永远不如工程手段。我最终的做法是三层防护第一层提示词尾部强制要求“只输出 JSON不要包含任何解释性文字”第二层调用后尝试json.loads()解析失败后进入兜底逻辑从响应文本里提取最外层大括号包含的内容重新解析第三层针对/think:open_code_review是那种很典型的“看名字简单做起来全是细节”的项目。我最初决定自己写一套代码审查工具不是因为业内没有现成方案而是因为团队的实际痛点一直没有被解决合并请求越来越多、代码评审越来越赶、低级问题反复漏到线上。与其反复抱怨流程不顺不如直接做一个能嵌入现有流程的审查管道。这个项目至今保持着规则引擎本地快速筛 LLM 语义深度分析的双通道架构。简单说规则引擎用毫秒级的速度扫出确定的、可枚举的问题比如调试断点残留、相等操作符误用、未处理异常等LLM 通道则专门负责需要“看懂”代码才能发现的逻辑问题比如边界条件缺失、并发隐患、设计层面的不合理之处。两条通道的结果经过合并去重后统一输出为一份 Markdown 审查报告。这篇内容适合两类人读。一类是团队里正在为代码评审效率发愁想自建或者选型开源审查工具的技术负责人另一类是本身就在做 AI 辅助编程工具想了解代码审查管道如何设计、规则引擎和 LLM 怎么协同的开发者。我会把这个项目从架构选型到落地接入的完整过程包括踩过的坑和最后的解决办法一次说清楚。1. 从“要不要做”到“做成什么样”项目的定位逻辑1.1 为什么代码审查工具不能只靠人肉代码评审这件事流程上各家都差不多提交合并请求、指定评审人、评审人翻代码、提出改动意见、修改后合入。听起来没毛病但规模一上来就出问题。我见过的一个实际场景是一个后端仓库平均每天合并 15 到 20 个 MR每个 MR 改动 300 到 800 行。也就是说光靠人工评审团队每天需要处理接近一万行代码的阅读量。这还不算通知、回复评论、反复 review 的循环耗时。人肉 review 还有一个更难解决的问题注意力衰减。评审人看前 200 行的时候是新鲜的、认真的看到第 800 行的时候精力基本到底了。一个藏在改动文件后部的breakpoint()残留或者一个被误改的并发参数非常容易被漏掉。这些漏网之鱼到了线上就是生产事故或者调试悲剧。所以我的判断是代码审查一定要有自动化工具打底。机器负责把确定性的、一眼能看出来的问题全部扫掉把漏网率降下来人把精力集中在机器给不了结论的地方比如架构设计、业务语义、未来扩展性。这不是替代人工而是给人工腾出时间和注意力。1.2 商用软件 vs 自研开源为什么我选了后者市面上其实有不少代码审查和代码质量工具。商用软件功能齐全开箱即用但我在选型时遇到了几个绕不开的问题一是费用按席位和组织规模计算团队稍微大一点就是一笔不小的长期开销二是规则体系是封闭的内置规则的粒度很难精确匹配团队自己的编码规范。团队规定“所有对外接口的入参必须做空值校验”这种高度定制化的规则在商业工具里配置成本非常高有些甚至根本不支持。开源社区也有现成方案但大多数停留在“静态代码分析”层面对合并请求的 diff 没有天然的集成能力。我要的是一个能挂在 CI/CD 管道里、每个 MR 自动触发审查、把结果直接反馈到代码托管平台评论区的工具这需要和平台 API 深度耦合。与其拿通用工具做大量二次开发不如直接按自己的需求写一套。当然了自研也有一条明确的红线不重复造基础轮子。diff 解析、Git 交互、Webhook 接收这类通用能力能借助成熟库就借助真正投入精力的是审查规则、LLM 集成方式、平台对接插件这些才是这个项目的价值所在。1.3 项目设计的三个核心原则定下自研路线之后我给自己写了三条设计原则后面所有决策都围绕这三条来。原则一审查能力要分层。规则引擎是确定性检查LLM 是智能分析两者并行而不是取代关系。规则引擎能快速、稳定、免费地处理大量低级别问题LLM 再对剩余的可疑片段做深度判断。这样一来算力成本可控响应速度也有保障。原则二接入成本要趋近于零。团队引入新工具最大的阻力是流程变复杂。如果审查工具需要开发者改变日常提交习惯或者要额外登录一个平台看报告推广起来一定会失败。所以 open-code-review 必须被动嵌入MR 被创建时自动触发结果自动发回 MR 评论区。开发者打开 MR评论就在那儿了不需要跳转任何系统。原则三规则和渠道都可插拔。项目最初的版本把规则写死在主程序里接渠道的时候也直接改主函数结果每加一个新规则就要提一次 PR每接一个通知渠道就要梳理一遍全局逻辑。后来重构成了现在的样子规则用装饰器注册通知渠道做成插件接口各管各的互不干扰。2. 核心模块拆解Diff 解析、规则引擎与报告合并2.1 Diff 解析模块整个项目最容易翻车的地方如果以为 diff 解析就是把文件内容按行读进来那就太天真了。真正处理起来才会发现diff 是高度格式敏感的。GitHub 返回的补丁格式里面有index行、--- a/路径和 b/路径的行前缀每次 hunk 还有行号范围的头部说明。GitLab 某些接口返回的格式有细微差别要是用同一套字符串解析逻辑去硬套早晚要翻车。我的 diff 解析模块核心做了这几件事。换行符统一。Windows 项目常见的 CRLF 行尾如果不转成\n解析出来的行内容会带一个\r规则匹配时会出现莫名其妙的不命中。按 hunk 切分。每个 -start,count start,count 头部下面是一组上下文和变更行。我需要做的是把新旧行号的映射关系建好否则后面规则引擎报的行号全是乱的。新增行和删除行分别处理。这对代码审查来说很重要——删除行不是我们要找的问题除非是某段不该删的代码被删了新增行才是引入问题的主要来源。上下文行的作用则是给 LLM 提供“看懂代码”的线索。行号映射是最容易踩坑的部分。GitHub 的评论接口要求传具体行号GitLab 的讨论接口传法又不一样。我在解析阶段就统一维护一份“新文件行号 ↔ 评论锚定行号”的映射表插件层直接用这份映射避免每个平台各写一套行号换算。2.2 规则引擎让规则真正“好写、好测、好维护”规则引擎不是一堆 if-else 堆起来的。它的核心是一个注册中心和一批规则函数。注册中心维护了一个规则目录记录每条规则的 ID、名称、适用文件类型、严重级别、是否启用。规则函数则通过装饰器注册到中心。rule_register.register( rule_idPY001, namedebug_breakpoint_check, file_types[*.py], severitywarning, ) def check_breakpoint(ctx): issues [] for line_no, line in ctx.new_lines: stripped line.strip() if stripped.startswith(breakpoint()) or stripped.startswith(pdb.set_trace()): issues.append(Issue( rule_idPY001, lineline_no, message检测到调试断点残留请在提交前移除。, suggestion删除调试语句或改用日志方式输出调试信息。 )) return issues上下文对象ctx封装了当前文件路径、语言类型、hunk 内容、新旧行号映射等。每条规则的逻辑都独立在一个函数里只依赖上下文对象不访问任何全局状态。这样做的好处一是测试方便二是规则之间不会互相污染。写规则这件事逻辑本身往往不难难的是确定规则边界。一条规则的命中率太高会产生大量噪音开发者看多了就麻木了命中率太低说明规则没抓住问题本质。业界常说的“规则要有明确意图”做起来比听起来难。我的经验是能枚举出 3 个以上真实故障案例的问题才值得写一条规则如果是“理论上可能有问题但实际没踩过坑”的情况先写成 warning 观察一段时间再升级。规则文件按语言分包管理目前内置覆盖 Python、JavaScript、Java、Go 四门语言的 40 多条规则。随着项目迭代新语言规则通过复制现有规则文件结构来扩展即可测试用例也可以对照已有模板来写。2.3 LLM 审查通道怎么把大模型“塞”进审查管道LLM 通道是这个项目里工程挑战最大的一部分。最开始我天真地把 MR 的整个 diff 全部塞给大模型结果很快碰壁。大型 MR 的 diff 动辄上万行token 数直接超模型上下文限制即便不超上下文太长也会严重分散模型的注意力给出的意见开始变得空泛停留在一堆“建议增强代码可读性”的正确废话上。后来我设计了一套分层裁剪策略。文件级裁剪。改动文件数量超过阈值时按“新增行数降序”排序只取前 N 个文件进入 LLM 审查。这个依据是新增行数越多问题的出现概率越高。文件内裁剪。对每个文件只保留新增行和与新增行相邻的少量上下文行纯删除的代码基本不影响语义判断。消息模板精简。给模型的系统提示词只保留必要的身份定义和输出格式约束不写一大堆无关的“你应该是一个资深专家”这种冗余话术。经过精简的提示词反而更稳定因为模型聚焦在具体任务上而非角色演绎上。模型输出格式的统一走了不少弯路。最初提示词里要求“以 JSON 格式输出”但模型有时候会给出 Markdown 代码块包裹的 JSON有时候里面夹杂解释文字解析直接失败。后来我在提示词末尾强制加上“只输出 JSON 对象不要包含任何其他文字”同时在代码侧做了解析兜底提取响应文本里最外层大括号之间的内容进行json.loads()如果仍然失败则只记录错误日志不阻塞整个审查管道。模型选型上并没有绑定任何一家。open-code-review 在配置里要求用户提供 OpenAI 兼容的 API 地址和密钥。这样一套配置既能接商用模型也能接本地部署的开源模型。团队如果对安全要求高数据不能出内网那就在内网起一个模型服务把地址指向内网即可。2.4 报告合并去重规则引擎和 LLM 的结果如何不打架双通道跑完之后最重要的一步是合并。如果不做处理两条通道可能会针对同一个问题各报一条报告里出现两条内容相似但行号略有差异的条目开发者看起来会觉得这个工具连自己都没协调好。去重逻辑我做了两层。第一层是锚点去重。以“文件路径 行号区间”为锚点如果规则引擎和 LLM 命中了同一个锚点保留规则引擎的结果。原因很直接规则引擎的行号是绝对精确的而且附带规则文档链接对开发者更有参考价值LLM 的行号存在一定的估计误差作为补充参考就够了。第二层是语义去重。当 LLM 报告的行号和规则引擎命中的行号相差在三行以内时对两条问题的描述文本做相似度计算。这里用的是简单的 Jaccard 相似度在去掉停用词之后计算两个问题描述之间的单词交集比例超过阈值判定为同一条问题。语义去重不追求完美目的是在快准之间找一个平衡点把明显重复的合并掉。合并完成后报告对象里每条问题都带上了规则 ID、严重级别、行号、描述和修改建议给到插件系统做分发。3. 接入实操从本地仓库跑通到接上 GitHub、GitLab3.1 本地模式最快验证核心链路的方法如果你想快速验证这个项目我强烈建议先跑本地模式。它不依赖任何代码托管平台不涉及 Webhook不需要公网地址只用一个本地 Git 仓库就能完成一次完整的代码审查。准备好一个测试仓库在里面故意制造一些典型问题Python 文件里放一个breakpoint()JavaScript 文件里用而不是Go 文件里忽略错误返回值。这些都会命中内置规则。然后在项目根目录执行python3 -m venv venv source venv/bin/activate pip install -r requirements.txt python -m open_code_review.cli \ --platform local \ --repo-path ./demo-repo \ --commit HEAD \ --config config/default_config.yaml命令行里--platform local的意思是走本地 Git 分析通道不走 Webhook 事件触发。--commit HEAD指定审查当前最新提交的改动。执行完的终端输出大致长这样审查目标: demo-repo (commit 3f2a1c0) 审查时间: 2025-01-15 14:32:08 审查文件数: 6 发现问题总数: 5 ├─ 严重问题: 1 ├─ 警告: 2 └─ 提示: 2 [PY001] 检测到调试断点残留 src/utils/logger.py:42 建议: 删除断点语句改用 logging.debug() 输出关键变量 [JS002] 使用了宽松相等运算符 web/component.js:88 建议: 使用 替代 避免隐式类型转换看到这个输出就说明核心链路已经通了剩下的事情都是锦上添花。3.2 接入 GitHubWebhook 配置与事件解析GitHub 接入流程的核心是 Webhook。GitHub 仓库的 Settings → Webhooks → Add webhook在 Payload URL 填上你的审查服务地址Content type 选 JSON触发事件勾选 Pull requests 和 Issue comments。服务端收到请求后需要根据X-GitHub-Event请求头来判断事件类型只处理pull_request事件里 action 为opened、synchronize、reopened的情况。synchronize对应的是开发者推了新代码到 PR 分支这个也必须触发重新审查否则审查结果永远停在第一次分析的状态。GitHub 的 API 认证用 Personal Access Token 最简单权限只需要repo范围的读取和评论写入。如果要做得更正规建议用 GitHub App 的形式安装到组织层面权限按组织统一管理但这种做法的开发成本高一些。我的项目里两种认证方式都支持配置文件里切换即可。审查完成后插件系统里的github_comment插件会自动在 PR 下面发一条评论按照严重级别分区块展示。评论内容里每条问题都带了文件路径和行号GitHub 会把这些行号渲染成可点击的锚点开发者点过去就能直接看到对应代码。3.3 接入 GitLab兼容性处理和鉴权差异GitLab 的接入方式整体和 GitHub 类似但有些细节差异需要处理。GitLab 用X-Gitlab-Event请求头标识事件类型Merle Request Hook 的事件类型是Merge Request Hook。在 GitLab 系统设置或项目设置里创建 Webhook 时同样填写 URL 和 Secret Token。如果团队用的是自建 GitLabWebhook 回调地址必须是 GitLab 服务器可以访问到的内网地址或公网地址。这个很容易被忽略——审查服务部署在 A 服务器GitLab 在 B 服务器A 和 B 之间的网络如果不通GitLab 推事件直接失败。GitLab API 的鉴权用 Personal Access Token 即可权限范围勾选read_api和write_repository。用机器账号而不是个人账号创建 Token避免某位同事离职后审查服务突然失效。3.4 后启动审查服务一条命令的事无论接入 GitHub 还是 GitLab审查服务本体就是一个 Python Web 服务监听在指定的端口上。python -m open_code_review.webhook_server \ --host 0.0.0.0 \ --port 8123 \ --config config/default_config.yaml启动之后最好先做几件事来验证接入是否成功。看看服务日志里有没有收到平台推送的事件记录。如果连事件都收不到问题大概率在 Webhook 配置或网络连通性审查逻辑都还没执行。用curl -X POST http://localhost:8123/webhook/github -H Content-Type: application/json -d test_event.json这种方式模拟发送一个测试事件检查服务端能否正确解析并触发完整流程。强制触发一次审查命令确认核心分析管道在服务模式下依然正常工作。3.5 配置体系全局配置、仓库配置和命令行覆盖项目里我设计了一套三层配置结构用来应对“不同仓库有不同审查需求”这个实际场景。全局配置default_config.yaml位于项目部署目录保存了模型 API 地址、Token、默认开关、默认严重级别、默认通知渠道等。这些是兜底配置任何仓库没有特殊指定时使用全局配置。仓库配置.open-code-review.yml放在被审查代码仓库的根目录。这个文件的优势是它可以跟着仓库走不同团队可以按自己的规范调整规则启停和级别不需要改审查服务端的任何配置。review_rules: enabled: true rule_level: PY001: error PY002: warning JS001: off llm_review: enabled: true model: gpt-4o-mini max_files: 20 max_file_size: 2048 report: output_format: markdown send_to: [github_comment, feishu] ignore_paths: - build/ - dist/ - node_modules/ignore_paths值得单独说几句。没有它的时候锁文件、构建产物、自动生成的代码会像洪水一样涌入审查管道规则的命中率被稀释LLM 的 token 消耗蹭蹭上涨报告里充满噪音。加了这层忽略配置审查信号干净多了。在这个配置里还做了一件重要的事把规则级别的维度拆分。同一个规则 ID 在不同仓库可以定义为 error 或 warning甚至可以直接关闭。比如PY001调试断点残留在公共工具库是 error在其他业务仓库可能只是 warning具体的取舍由仓库维护者自己决定。4. 项目管理与协作中的踩坑经验4.1 审查速度慢到被开发者吐槽的 4 个原因LLM 通道刚上线时工具响应慢的问题一度被团队吐槽得很厉害。一个 MR 创建之后开发者等了五六分钟还看不到审查评论体验非常糟糕。复盘下来慢的原因主要有四类。第一类是串行调用 LLM。早期实现是“文件 A 分析完再分析文件 B”每个文件平均耗时 15 秒五个文件就是 75 秒加上排队网络延迟能不慢吗后来改成异步并发调用用一个线程池控制并发数5 个文件并发跑总耗时压缩到 20 秒左右。第二类是 diff 里的文件数量太多。一个 MR 改了 200 个文件每个文件都去做完整分析哪怕并发也顶不住。后来给 LLM 通道加了max_files限制超过数量只取增删行数最多的前 20 个文件其余走规则引擎兜底。第三类是模型网络请求超时时间设置太长。默认 60 秒的超时时间模型一旦响应慢整条链路就卡在等待上。现在把超时压到 30 秒并且加了重试机制首次超时后换一个浅层模型再跑一次。第四类是报告生成和插件分发串行执行。发送 GitHub 评论和发送飞书通知是独立的没有理由串行。这里同样改为并发执行分发环节延迟明显下降。4.2 怎样让开发者真正愿意看审查意见技术问题解决完之后还有一个更难的问题开发者不爱看审查意见。一开始报告直接扔到 MR 评论区长列表的格式很劝退。后来我调整了报告的排版和分发方式效果提升很明显。报告里每条意见增加了“为什么”字段。同一句话只写“检测到调试断点残留”和补充一句“删除调试语句避免测试代码跑到生产环境”带来的开发者行动率是两码事。严重级别的信息前置。评论的顶部先用一句话总结问题分布3 个严重问题5 个警告。然后才按文件分类详情。开发者扫一眼就知道有没有需要必须处理的。工具消息和人工评论分开。审查机器人以特殊账号身份发言不带表情符号不用感叹号不表现得像一个试图证明自己有用的人。语气越接近客观描述开发者越愿意看。4.3 报告语气和规则表述的自我约束在写规则的过程中我很注意避免一种倾向把代码审查工具变成一个“judge”工具。规则描述里不能出现“这段代码写得很差”“这个写法很低级”这类带有主观评价的措辞。每条表述都围绕事实展开——这个写法会触发什么问题不修的话可能带来什么后果建议用什么方式修改。伤害最小化原则可能听起来很软但实际效果非常硬。工具给出的意见如果让开发者感到被指责他们就会开始质疑工具的每一个结论即使工具的结论是对的。保持客观、事实化、建议式的表述配合后面的协作代码建议功能开发者对工具结论的接受度会稳定得多。工具本身越谦逊团队越愿意用它。5. 项目复现的核心要点与后续规划5.1 复现项目时先抓住这几个关键环节如果你想把这个项目重新实现一遍或者说拿去内部部署我的建议是不要一上来就铺所有功能。按依赖关系排序先做这几个核心环节。第一步diff 解析。把 Git 补丁格式处理对能稳定输出“文件路径 新增行号 行内容”的结构化数据。这一步没过关后面全是空中楼阁。第二步规则引擎扫描。不用急着做配置体系先写死 5 条规则跑通整条链路。能看到输出报告这个项目的骨架就有了。第三步报告输出插件。先实现最简单的终端输出把报告内容格式调好后面接什么平台都是改发送方式而已。第四步LLM 通道。把裁剪策略做对这一步最花时间所以放到基础链路通了之后再投入精力优化。第五步Webhook 接入。等平台事件触发的逻辑完善了再根据需要使用 GitHub 或 GitLab 的 API 将结果回传。这些步骤里每一步都可以独立使用。比如只做前两步就是一个本地 diff 快速扫描工具做到第三步就能输出好看的报告做到第四步就成了一个带 AI 分析的审查工具。5.2 这个项目后续可以扩展的方向open-code-review 目前已经覆盖了基础的代码审查场景但我觉得它还有几个值得探索的方向。规则的自学习。通过反馈数据来调整规则的阈值和优先级。某些规则在特定仓库命中率很低说明规则和业务场景不匹配这些规则会根据历史数据自动降级。如果用户点击“误报”按钮反馈会直接进入规则统计库用于后续调优。代码建议一键应用。目前工具只给建议不给实现。后续可以支持对简单的代码问题直接在 MR 分支上生成修复补丁生成基于规则功能的修改代码减少“从建议到实现”的切换成本。这一步的难点在于修改代码的安全性——必须保证自动生成的补丁不改变原有逻辑。多模型协同。不同模型在不同语言上的表现有差异后续可以针对不同文件类型路由到不同的模型实现上。这个想法可以先从实验开始等效果稳定后再集成到正式构建版本。与本地开发工具联动。现在审查是提交之后的事。后续可以在开发者的本地 IDE 里差量提前运行同一套规则让问题在提交前就被拦截。同样的规则既能在本地跑又能跑在 CI 上是完善审查体验的方向之一。5.3 我的几点实操体会如果回到项目最开始让我做一次选择我还是会选择走自研这条路。不是因为现成工具不好而是这个过程教会了我太多代码仓库之外的东西怎么设计一套可插拔的架构怎么在规则粒度上平衡精准度和覆盖率怎么通过工程手段让一个智能模型变得稳定可用。这些经验在用它解决代码审查问题之前其实是很难获得的。一个项目从“能用”到“好用”差距往往不在于核心算法多高级而在于细节是否被反复打磨。审查消息的语气、报告排版的可读性、规则描述的准确性、失败时的重试策略所有这些加在一起才构成了一个团队愿意每天使用的工具。我不主张上来就把架构做得特别复杂从一个真实痛点出发做成最小可用的闭环再逐步迭代功能和性能这个路径是我觉得最稳妥的。最后说一个我在实际运营这个项目时的小经验不要试图让自动化工具解决所有问题。代码审查本质上还是人的协作过程工具可以放大人的效率但不能取代人的判断。好的代码审查工具应该像一个可靠的后台助手安静地准备好所有材料让做决策的人看得更清楚、更从容。