
1. 为什么我想把代码审查“开放”出来先说说这个项目的由来。我在团队里做了快三年的代码审查GitHub 上开过的 PR 评论没有一千也有八百但有个问题一直让我很别扭审查这件事本质上是被“关”在仓库里的。每条评论、每次讨论、每个“这里为什么要这样写”的结论最终都沉没在 PR 的历史记录里。三个月的项目做下来新人要理解一个模块的设计动机唯一的办法是去翻几十个已关闭的 PR效率极低。另一个触发点是跨团队协作。我们公司内部有好几个业务线各自维护不同的仓库。A 团队踩过的并发坑B 团队三个月后又原封不动踩了一遍。不是大家不愿意分享而是当时的审查讨论没有一种好的方式能被“提取”出来、被“检索”到、被“复用”到其他仓库。代码是开放的但代码背后的审查智慧却是封闭的。所以我起了个念头能不能做一个开源的、独立于 Git 托管平台的代码审查增强层它不替代 GitHub/GitLab 自带的 Review 功能而是在上面叠加一层能力——自动抽取审查事件、沉淀审查结论、形成跨仓库可检索的审查知识库甚至帮你统计团队的审查健康度。这就是 open-code-review 项目的起点。这个工具适合谁不是那种需要一个完整 PaaS 级别的代码审查平台的大厂而是从几个仓库起步、团队规模在十到一百人左右、希望把审查经验变成团队资产的中小型研发团队。它不大但能解决一个很具体的问题让代码审查不只是“把关关口”更是“知识沉淀渠道”。2. 核心问题拆解审查记录不该困在 PR 的时间线里动手写代码之前我先把需求拆成了三个层面。这个拆解过程很重要因为如果一开始就把目标定成“做一个功能完整的 Code Review 平台”这个项目大概率会烂尾。我需要的是最小可用、但价值密度高的切片。2.1 审查数据的三层价值第一层是流程价值审查是否及时、评论是否被回复、合入前是否至少有一人确认。这一层解决的是“审查有没有被执行”。第二层是语义价值这次审查中提出了哪些类型的问题——是并发安全、可读性、性能隐患还是架构设计分歧。这一层解决的是“审查发现了什么”。第三层是沉淀价值一个反复出现的问题类型在不同仓库中的处理方式有什么异同哪些讨论值得被提炼成团队规范。这一层解决的是“审查经验如何复用”。现有平台GitHub/GitLab 的 PR 界面其实把第一层做得很好第二层部分支持通过标签但第三层几乎没有。我决定重点做第二层和第三层。2.2 技术路线的取舍在最开始我用 Python 写了一个脚本定时调用 GitHub API 拉取仓库的 PR 和 Review 评论存到 SQLite 里。跑了两个星期后发现几个问题API 有速率限制仓库一多就会被卡住拉取是全量的但真正有价值的其实是那些“非赞成的评论”和“有回复的讨论串”;纯定时轮询不够实时审查结束后要等一两个小时才能在系统里看到结论。后来我重构了方案用 Webhook 事件驱动替代轮询用事件日志替代全量快照。也就是说只有当 PR 有新的审查评论、新的 review 提交、新的标签变化时系统才会去处理这一段增量数据。这个改动的收益非常明显API 调用量降了大概 70%数据延迟从小时级降到秒级。架构上最终是四层层级职责技术选型事件接入层接收 GitHub/GitLab Webhook做签名校验FastAPI uvicorn预处理层事件去重、关联仓库/PR、过滤无关事件Redis 简单幂等表 内存队列分析存储层抽取审查结论、打语义标签、存储可检索记录PostgreSQL JSONB SQLite轻量模式查询展示层提供检索接口和简单报表内置只读 API 一个单页 HTML 面板这个选型不是纯跟风而是基于团队实际情况。FastAPI 本身异步性能好适合处理 Webhook 高频到达的场景PostgreSQL 的 JSONB 适合存变长的审查元数据而 SQLite 轻量模式则是给那些没有条件部署 PG 的小团队留的后路。3. 关键实现让一条审查评论变成一条可检索的知识记录这一部分我讲讲核心实现思路比代码本身更重要。如果你打算复用这套设计照着逻辑重写一遍也完全可行。3.1 审查事件的语义化抽象一条从 GitHub 过来的 review 事件长什么样它里面其实有一个action字段submitted、edited、dismissed还有review对象里的stateapproved、changes_requested、comment、body、html_url再加上pull_request和repository的信息。但对我来说真正有价值的是“这条评论到底在说什么”。我定义了一个语义层把每个审查事件转换成轻量的“审查结论”对象# events/schemas.py from pydantic import BaseModel from typing import Optional, List from enum import Enum class ReviewVerdict(str, Enum): APPROVED approved CHANGES_REQUESTED changes_requested COMMENT comment DISMISSED dismissed class ReviewCategory(str, Enum): CORRECTNESS correctness # 正确性/崩溃风险 PERFORMANCE performance # 性能隐患 SECURITY security # 安全风险 READABILITY readability # 可读性/维护性 ARCHITECTURE architecture # 架构耦合/设计分歧 TESTING testing # 测试覆盖不足 STYLE style # 风格问题 OTHER other class ReviewRecord(BaseModel): repo: str pr_number: int pr_title: str reviewer: str author: str verdict: ReviewVerdict categories: List[ReviewCategory] [] body_excerpt: str discussion_url: str replied: bool False created_at: str有了这个统一结构不管审查来自 GitHub 还是 GitLab最终都能落到同一套存储里。多平台兼容不是靠写死两套逻辑而是靠这个抽象层把差异消除掉。3.2 审查类型的自动分类先规则后模型接下来一个大问题怎么判断一条评论属于哪个“审查类别”我的做法分两步。第一步是关键词规则覆盖了大约六成场景。比如评论里出现“race condition”“线程安全”“并发”——归为 CONCURRENCY“SQL 查询”“N1”“索引”——归为 PERFORMANCE“XSS”“注入”“密钥”——归为 SECURITY。这部分的实现不复杂就是维护一张带权重的关键词表。第二步是轻量模型兜底。对于一些规则覆盖不到的长评论我用了开源的多语言句子向量模型做文本分类。具体来说先把历史审查评论人工标注出类别训练一个简单的分类头对新的评论向量做预测。可能有人觉得这里应该上大模型做语义分析但我权衡了一下项目里一半以上评论是中文和英文混合的用通用大模型的成本高、响应慢而且本地向量模型的延迟只有几百毫秒足够用。把规则和模型的结果做置信度融合分类准确率大概在 82% 左右。# classifier/categorize.py def categorize_comment(text: str) - list[str]: matched match_keyword_rules(text) if matched: return matched vec embed(text) proba cls_model.predict_proba([vec])[0] top cls_model.classes_[proba.argmax()] return [top] if proba.max() 0.45 else [other]这里有一个细节绝不只依赖模型结果。如果一条评论规则命中了 SECURITY那就直接信任规则不交给模型重新判断。因为规则命中意味着文本里有明确的特征词模型反而可能因为上下文措辞复杂而把它分错。3.3 讨论串串联识别哪些评论“被回复了”Review 知识库的价值很大一部分在“讨论过程”上。一条批评如果被作者解释了、反驳了或者接受了它的信息量比单纯的静态评论大得多。GitHub 的 Review API 会把评论组织成 thread每条评论有一个in_reply_to_id字段表示它是回复哪条评论的。我通过这个字段构建一棵评论树然后判断一条初始评论是否“有争议”——只要它的子孙节点里存在非自动回复、非重复文本的实质回复就把这条初始评论标成replied: true。这一步看着简单但有个非常烦人的坑GitHub 的 bot比如 Dependabot 或 CI 自动评论也会触发 Webhook。如果不加过滤bot 评论会被当成“团队实质性讨论”污染整个统计分析结果。我的过滤规则是维护一个已知 bot 关键词列表比如[bot]后缀、dependabot、github-actions另外再加一个启发式判断评论者是否是仓库的 registered contributors。两个条件任一命中就标记为非人类评论。4. 落地部署与接入Webhook 里全是细节这套系统说白了一个字就是“接”。但真正去对接 GitHub/GitLab Webhook 时你会发现文档里不写清楚的坑特别多。4.1 Webhook 收件端的安全校验Webhook 的 URL 一旦暴露任何人都能伪造请求。GitHub 提供了签名机制用 HMAC-SHA256 对请求体做哈希和X-Hub-Signature-256头里的值比对。我实现了这个校验一个盲点是对原始 body 做签名绝对不能对已经反序列化的 JSON 字符串做签名。# webhook/verify.py import hashlib import hmac def verify_github_signature(secret: bytes, body: bytes, signature_header: str) - bool: if not signature_header: return False expected sha256 hmac.new(secret, body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature_header)中间我踩过一次坑在测试的时候用curl -d {key:value}发送请求本地一直报验签失败。排查半天发现是 uvicorn 拿到的是已经被解析过的 JSON 字符串再重新编码之后 key 顺序变了。最后解决方式是让 FastAPI 的 endpoint 直接接收Request对象把原始body读出来用于验签验通过后再单独解析 JSON顺序绝对不能反过来。GitLab 的做法略有不同它用的是X-Gitlab-Token这个 header直接在应用层配置一个共享 token 做比对不涉及 HMAC 签名。所以如果你的团队同时用 GitHub 和 GitLab这部分的适配逻辑得分别写。4.2 事件去重与幂等Webhook 的设计是“至少一次投递”也就是说同一个事件可能被推送两次。如果你不做幂等相同的一条审查评论就会被插进两次数据库后续的统计数字全乱掉。我的做法是在数据库里建一个事件处理表主键是(event_id, repo, pr_number, review_id)处理前先插入插入冲突就直接跳过。Redis 里做同样一份近期的 event_id 缓存作为一级过滤减少无效的数据库查询。这套双保险跑下来丢事件和重复事件基本都控制住了。CREATE TABLE processed_events ( event_id TEXT PRIMARY KEY, repo TEXT NOT NULL, pr_number INTEGER NOT NULL, review_id TEXT NOT NULL, processed_at TIMESTAMPTZ NOT NULL DEFAULT now() );4.3 与 CI 流程的联动光有一个独立的系统还不够审查数据要嵌入开发流程才有生命力。我做了两件事第一给 open-code-review 写了一个轻量的 CLI 工具可以在 CI 里跑。比如当 PR 创建或更新时CI 会自动跑一遍“审查健康度检查”如果发现 PR 一直没有得到任何 review就在 PR 上打一个review-pending的标签。这个标签可以让团队的管理者一眼看出哪些 PR 在等待审查。第二在项目的 README 里提供了 GitHub Action 的 example用官方 action 调用这个 CLI# .github/workflows/review-health.yml name: review-health on: pull_request: types: [opened, synchronize, ready_for_review] jobs: check-review: runs-on: ubuntu-latest steps: - name: Run open-code-review health check uses: your-org/open-code-reviewv1 with: token: ${{ secrets.GITHUB_TOKEN }} mode: check这个说不上多复杂的集成但是整个流程跑通之后你会发现“审查状态”不再需要人肉去盯而是主动出现在技术讨论的上下文里效果立竿见影。5. 实测三个月的效果与踩坑清单这项目上线到我们团队内部使用已经三个月了有些数据变化是可以量化的也有些问题是花了很多时间才逐一解决的。这个环节我用真实体会来讲讲哪些功能真正触到了痛点又有哪些设计需要重新回炉。5.1 良性变化审查知识开始“流动”我们团队有三个仓库在没有这套系统之前历史审查讨论是相互隔离的。现在有了检索面板之后我发现了一个高频使用行为人们在写代码前会先去检索一下“这个模块之前有没有人踩过坑”。比如后端新同学在处理 Redis 分布式锁时先搜到上上个月在同仓库一个 PR 里有人指出用SETNX但没有设置过期时间是会出问题的他直接就把这个坑绕过去了。这种知识传递的链路在以前几乎不可能发生。另外统计面板增加了几个指标后团队 review 的节奏也有改善。以前 merge 的 PR 里有接近三成没有收到任何反对意见不是真的写得完美而是很多时候流程走个形式。现在如果 PR 只有“功能自查”没有任何 reviewer 的打分式审查标签会自动标记出来这个有形的“空转指标”确实让团队更认真对待审查环节了。5.2 踩坑一GitHub API 的评论内容截断这是我最早遇到的坑。GitHub API 返回的 review comment 有一个body字段正常情况下是完整文本但如果评论内容超过一定长度大约 65535 字符API 会给出body_truncated: true同时把body字段截断。如果不处理这个标记你存进知识库的“讨论摘录”是不完整的搜索的时候会漏掉关键信息。我的处理方式是检测到body_truncated为 true 时改用issue_comment的 API 去拉取完整内容如果还是拿不到理论上极少发生就退而求其次存评论文本的 hash 值和一个可跳转的链接不存储全文。不要让数据的完整性卡住整个流程。5.3 踩坑二Forks 和跨仓库 Merge 的事件冲突很多人容易忽略的一个场景是开发者从 fork 出来的仓库提 PR。这种 PR 的 review comment 在 API 里挂在“目标仓库”下但head.repo是另一个仓库。如果你像我一样用repo.full_name做维度统计就会出问题——同一个 PR 的讨论串一部分要算进目标仓库的知识库里一部分因为 author 在 fork 仓库里的评论会被错误归属到 fork 仓库。最终的解决方案是事件归属始终以baseRepo为准所有 review 记录、讨论串、统计都只在目标仓库维度聚合。fork 仓库可以忽略因为它只是临时来源不是团队的正式资产。5.4 踩坑三时区和“今天”的错觉统计面板上线后有一天我发现“今日审查评论数”在下午六点以后基本是零但团队明明还在活跃评审。查了半天问题出在 GitHub 返回的created_at是 UTC 时间。我直接用这个时间做CURRENT_DATE分组等于把 UTC 的日期当成了团队本地日期。到中国时间的晚上八点UTC 还停留在当天凌晨统计自然是空的。改正很简单存储时统一用 UTC但展示和聚合时必须用团队配置的时区转换。我在系统设置里加了一个timezone_offset字段所有按日期的统计都先转换再分组。SELECT date(created_at INTERVAL 8 hours) AS local_day, count(*) AS comment_count FROM review_records GROUP BY local_day ORDER BY local_day DESC;这个坑很小但特别容易发生在新写的统计逻辑里一定要在一开始就把“存储时区”和“展示时区”的边界划清楚。5.5 踩坑四Review 被 Dismiss 后的数据一致性GitHub 允许维护者 dismiss 一个 review通常是“这个 review 作废了”或“lgtm 但想重新触发 CI”。如果系统不处理dismissed事件之前统计到的changes_requested就会变成永久性污点数据团队的“change request 数”指标会虚高。这个问题的正确处理链路是监听pull_request_review事件里 action 为dismissed的情况把对应的 review 记录状态改成dismissed然后在所有指标计算中排除掉已驳回的 review。注意不要直接删数据因为审查历史本身也是有价值的过程记录保留状态字段优于物理删除。6. 关于“开放审查”的下一步规范比工具更重要工具跑起来只是第一步。我越来越觉得开源一个代码审查工具真正难的其实不是代码而是把“代码审查应该是什么样”的共识注入到这个工具的使用习惯里。我在我们团队内部推行了几件小事情每一件都和 open-code-review 的功能有关但又不只是功能层面的问题。6.1 给审查结论分类而不只是打分GitHub 原生的 Review 只有 approved / changes_requested / comment 三档。这有点粗暴。我建议团队在写评论时尽量用一些表达类型的前缀比如 [correctness]、[performance]、[security]、[design]、[question]。这不是为了给 open-code-review 的分类器提供数据而是为了让作者一眼就知道这条评论的物理性质是必须修改还是可以讨论还是仅仅是一个疑问。半年下来PR 讨论的“有效回复率”明显提高了因为大家更清楚什么时候需要用 arguments 回应什么时候只要简单说“好的已修复”。6.2 审查意见要能够追溯到“目标代码段”工具层面我已经在考虑做一件事情把 review comment 关联到 commit diff 的特定行。GitHub API 其实已经给出了位置信息path和line/position但以前我建模时没有存下来。等到我想回头分析“哪类问题最容易出现在哪类文件上”的时候才发现数据没存全只能靠评论正文里的文件名猜测非常痛苦。如果你现在用 open-code-review 或自建类似系统一定要在数据模型里预留代码位置字段。这条经验值五千块钱。将来做“diff 级别的审查质量分析”或者“按目录聚合高频问题类型”这类进阶报表时没有位置信息几乎做不出来。6.3 知识库必须支持主动订阅而不是被动搜索最后说一个我实验过的方向。单纯做搜索引擎还不够真正让审查知识产生价值的是“在正确的时间把正确的经验推给需要的人”。我在 open-code-review 里做了一版简单的订阅规则比如某个开发者关注了 SECURITY 类问题当新入库的 review 记录被分类为 SECURITY 时系统会往他的 IM 机器人推一条摘要。实验了两周之后我发现这个功能被使用的频率远超我的预期很多人不是主动去搜而是希望系统在关键时刻踢他一脚。后续如果要扩展这个方向可以考虑给每个团队自定义“知识订阅模板”比如“和后端网关相关的 PR 产生了哪些新结论”。这个功能的想象空间其实比统计报表更大也更值得维护下去。代码审查这件事本质上是一次小型的知识交换。open-code-review 真正的野心不是做一个更花哨的 Review 页面而是把交换的知识结构化沉淀下来让它们有机会去指导第二次、第三次类似的决策。这也是我把项目开源出来的原因——单靠一个公司内部推动很难把“开放审查”的观念打磨好我希望更多团队能在这个框架上共建出适合各自组织的那一套沉淀逻辑。如果你也在这个方向上有自己的尝试或者踩到过我还没写出来的坑欢迎找来交流。