过去半年,我主要在做智能代码 CR 系统,从变更级审查做到仓库级审查。这篇按系统的几个组成部分,整理当时的设计、实验和结论。
开始设计时,我先给自己列了七个问题:
- 这个系统应该包含哪些部分?每一部分的细节是什么?
- 哪些部分是重难点,需要重点解决?
- 有哪些指标能说明系统的效果?怎么收集?
- 怎么保证系统可靠稳定?哪些部分会成为瓶颈?
- 业内有哪些成熟的系统可以借鉴?
- 想要的终态是什么?分几步达成?每一步的里程碑是什么?
- 现在的架构有什么优缺点?
系统架构
整个系统分三块:
- 组装 prompt:AST 解析、RAG 召回、仓库维度的全局代码搜索,以及链式思考、结构化输入输出等 prompt 技巧。
- 大模型审查:多模型调度加服务降级;正确性推理加二次确认;多规则、多参数、多模型并行审查,审查后做 issue 选举。
- 生成 issue:按本次变更的代码行过滤、函数内去重、规则过滤(编码规则过滤、历史 issue 召回过滤)。
以函数为单位审查
前提是:把代码解析成函数,以函数为维度进行审查。
- 只解析新增或变更的函数,删除或减少的函数不审查;
- 函数里审查出的问题要具体到行,只交付变更行或新增行上的问题,原有代码行不处理。
AST 底层用 tree-sitter:先解析节点,再按具体语言的语法组装出函数名、返回值、签名等信息。
审查策略
按规则并行审查。 代码问题可以分成几大类,按类别分别让大模型审查,比如只问“这段代码有没有线程安全问题”。优点是问题小而明确,准确率更高;缺点是问题类型不收敛,既无法保证全面,类型多了性能也扛不住。比较可行的用法是针对高频问题重点审查,或者作为特定场景的兜底。
按参数变体并行审查。 同一个 Agent 运行多次,选出最优解。比如要求严格,就运行 3 次,只保留 3 次都识别出的 issue;也可以每次微调一些参数,比如 temperature、few-shot case、底层模型。
多次结果做 issue 选举。 并发审查多次以后,对重复的 issue 做选举:只保留每次都出现的,或者出现次数超过 2 的,策略可配置。
二次确认。 对审查出的问题,再让大模型做一次二元判断,进一步提升准确性。
prompt 里最重要的两条
审查的 prompt 要求模型只找高价值问题,其中最关键的是两条:
- 不要报告无关紧要的问题,比如拼写错误、命名风格。
- 不要报告因为缺少上下文而无法确定是 bug 的问题。
第二条我在 prompt 里专门给了一个反例:模型说“这里调用的函数在片段中没有定义,可能导致 NameError”。这不成立,因为片段只是完整代码库的一部分,这个函数极有可能定义在别处,有经验的开发者一般也不会犯这种错。
二次确认阶段,我让模型给每个 issue 打标,不确定的标成 uncertain 并写明原因,比如“不确定父类是否实现了这个方法”“不确定导入的库里是否已经有这个宏定义”“可能因为缺少上下文导致计数错误”。标成不确定的,默认不展示。
后置过滤
模型输出之后,还要经过几道过滤:
- 按行数过滤:让模型一并返回问题所在的代码行,只展示本次提交代码块里的 issue,不是本次变更的过滤掉。
- 函数级去重:同一个函数里重复类型的问题只展示一个。比如函数比较大,不同行出现同样的问题,只报一次。
- 历史 issue 过滤:用大模型对比多次审查的 issue,根据用户的拒绝记录过滤;也用 RAG 召回(向量、文本搜索、关键 key)匹配相似度过滤。
- 编码规则过滤:某些问题可以用编程规范做正向过滤。如果数据噪音比较大,就只展示特定规则下的 issue。
另外,审查前会先召回这段代码上一次的审查结果:代码没变,就跳过审查,直接复用;有变化,再重新审查。
few-shot 实验:一个结论撑起了效果
仓库级审查的第一版效果很差:有效问题不到两成。问题记录下来主要是两条:
- 核心函数抓取不准,比如把初始化函数、校验函数当成了核心函数,而文件里还有很多其他业务逻辑函数;
- 同一段代码审查出完全一样的问题,最多的一处重复了 9 次。
按类型看,unsafe 类问题一个有效的都没有,其次是各种 potential null pointer dereference,这些都是套话。
于是我开始收集 few-shot case,分类型放进 prompt,做了多轮验证。结论是:
- case 超过 6 个、到 10 个以后,开始出现注意力丢失;
- 在注意力不丢失的前提下,10 个 case 比 5 个 case 的有效率高,尽量多加 case 是提升有效率的重要途径;
- 10 个 case 时,审查出的问题稳定在 20 个左右,不排除有真正的问题被过滤,后续还需要收集 high level 的 case:
- low level case 用于提升问题有效率;
- high level case 用于提升问题发现的质量。
prompt 里放入这 10 个 case 后,连续 5 次审查,有效问题都在七成以上;不加这些 case,一次审查会报出 200 多个问题,有效率只有一成多。
怎么知道建议被采纳了
有一部分同学接受了修复意见、改了代码,但没有在界面上标注。只靠点赞点踩,数据太少。所以我设计了一个 issue 采纳自动标注系统:让大模型判断同一用户、同一个变更的两次代码提交,自动标注 issue 是否被采纳。
核心是找到“同一个函数”:同一个用户、同一个仓库、同一个文件、同一个函数。
- v1:上一次审查时的函数代码,以及给出的修复代码;
- v2:这一次提交后的函数代码。
让大模型判断:v2 是不是 v1 应用了修复代码之后的结果。
判断前先用工程手段做前置过滤:文件 hash 相同、函数内容相同,就直接判定未修改,最后才用模型兜底。prompt 里给了四类示例:
- 两段代码一致,未应用修复;
- 两段代码不一致,应用了修复;
- 两段代码不一致,但完全没有关联,未应用修复;
- 两段代码不一致,部分应用了修复,但没有完全应用。
业界参考
字节的 BitsAI-CR。 它的论文上个月刚公开,核心看点有三个:
- 两阶段评论生成:RuleChecker 基于内部的多维审查规则生成评论;ReviewFilter 用另一个微调模型对输出做二次验证,处理幻觉和低价值评论。
- Outdated Rate 指标:只看准确率有两个根本限制:反映不了开发者是否真正采纳,人工评估也需要大量人力。Outdated Rate 追踪被标记的代码行在后续提交中是否被修改,量化审查建议的实际价值。这和我做的采纳自动标注是同一个思路。
- 数据飞轮:用户直接反馈、每日抽样人工标注、每周 Outdated Rate 监控,三者结合持续优化规则。
Ellipsis。 国外已经商用的智能代码审查工具。它的做法包括:用代码切片和 AST 解析实现仓库级搜索,多个小型 Agent 分工审查,多层过滤管道减少误报,代码仓库向量化并增量更新,用户数据飞轮,以及用 eval 机制及时感知整体效果。
稳定性和后续
稳定性上主要考虑三件事:
- patch 太多导致审查时间过长、单次 issue 过多,需要监控 patch 行数和块数;
- 大模型本身不稳定,要有重试和降级;
- 整体流程失败的重试机制。
后续有两个方向:
- 对每个 issue 生成的修复代码构造测试用例并真正运行。优点是能保证改动可以实际运行;缺点是耗时耗资源,而且大部分提交本身都能运行,只有边界场景会出问题。
- 把 RAG 后置。现在是召回代码和 issue 放进 prompt,链路长,经过大模型识别后有效性会进一步下降;如果让 RAG 直接对生成的 issue 做过滤,效果可能会更立竿见影。
这半年最大的体会:AI 代码审查的难点不在发现更多问题,而在只留下值得处理的那几条。 宁可少报,不要乱报。