AI 实践 / NOTES

AI Code Review:宁可少报,不要乱报

以函数为单位审查、多轮选举与过滤、few-shot 实验,以及怎么判断建议被采纳了。

过去半年,我主要在做智能代码 CR 系统,从变更级审查做到仓库级审查。这篇按系统的几个组成部分,整理当时的设计、实验和结论。

开始设计时,我先给自己列了七个问题:

  1. 这个系统应该包含哪些部分?每一部分的细节是什么?
  2. 哪些部分是重难点,需要重点解决?
  3. 有哪些指标能说明系统的效果?怎么收集?
  4. 怎么保证系统可靠稳定?哪些部分会成为瓶颈?
  5. 业内有哪些成熟的系统可以借鉴?
  6. 想要的终态是什么?分几步达成?每一步的里程碑是什么?
  7. 现在的架构有什么优缺点?

系统架构

整个系统分三块:

  1. 组装 prompt:AST 解析、RAG 召回、仓库维度的全局代码搜索,以及链式思考、结构化输入输出等 prompt 技巧。
  2. 大模型审查:多模型调度加服务降级;正确性推理加二次确认;多规则、多参数、多模型并行审查,审查后做 issue 选举。
  3. 生成 issue:按本次变更的代码行过滤、函数内去重、规则过滤(编码规则过滤、历史 issue 召回过滤)。

以函数为单位审查

前提是:把代码解析成函数,以函数为维度进行审查。

  • 只解析新增或变更的函数,删除或减少的函数不审查;
  • 函数里审查出的问题要具体到行,只交付变更行或新增行上的问题,原有代码行不处理。

AST 底层用 tree-sitter:先解析节点,再按具体语言的语法组装出函数名、返回值、签名等信息。

审查策略

按规则并行审查。 代码问题可以分成几大类,按类别分别让大模型审查,比如只问“这段代码有没有线程安全问题”。优点是问题小而明确,准确率更高;缺点是问题类型不收敛,既无法保证全面,类型多了性能也扛不住。比较可行的用法是针对高频问题重点审查,或者作为特定场景的兜底。

按参数变体并行审查。 同一个 Agent 运行多次,选出最优解。比如要求严格,就运行 3 次,只保留 3 次都识别出的 issue;也可以每次微调一些参数,比如 temperature、few-shot case、底层模型。

多次结果做 issue 选举。 并发审查多次以后,对重复的 issue 做选举:只保留每次都出现的,或者出现次数超过 2 的,策略可配置。

二次确认。 对审查出的问题,再让大模型做一次二元判断,进一步提升准确性。

prompt 里最重要的两条

审查的 prompt 要求模型只找高价值问题,其中最关键的是两条:

  1. 不要报告无关紧要的问题,比如拼写错误、命名风格。
  2. 不要报告因为缺少上下文而无法确定是 bug 的问题。

第二条我在 prompt 里专门给了一个反例:模型说“这里调用的函数在片段中没有定义,可能导致 NameError”。这不成立,因为片段只是完整代码库的一部分,这个函数极有可能定义在别处,有经验的开发者一般也不会犯这种错。

二次确认阶段,我让模型给每个 issue 打标,不确定的标成 uncertain 并写明原因,比如“不确定父类是否实现了这个方法”“不确定导入的库里是否已经有这个宏定义”“可能因为缺少上下文导致计数错误”。标成不确定的,默认不展示。

后置过滤

模型输出之后,还要经过几道过滤:

  1. 按行数过滤:让模型一并返回问题所在的代码行,只展示本次提交代码块里的 issue,不是本次变更的过滤掉。
  2. 函数级去重:同一个函数里重复类型的问题只展示一个。比如函数比较大,不同行出现同样的问题,只报一次。
  3. 历史 issue 过滤:用大模型对比多次审查的 issue,根据用户的拒绝记录过滤;也用 RAG 召回(向量、文本搜索、关键 key)匹配相似度过滤。
  4. 编码规则过滤:某些问题可以用编程规范做正向过滤。如果数据噪音比较大,就只展示特定规则下的 issue。

另外,审查前会先召回这段代码上一次的审查结果:代码没变,就跳过审查,直接复用;有变化,再重新审查。

few-shot 实验:一个结论撑起了效果

仓库级审查的第一版效果很差:有效问题不到两成。问题记录下来主要是两条:

  1. 核心函数抓取不准,比如把初始化函数、校验函数当成了核心函数,而文件里还有很多其他业务逻辑函数;
  2. 同一段代码审查出完全一样的问题,最多的一处重复了 9 次。

按类型看,unsafe 类问题一个有效的都没有,其次是各种 potential null pointer dereference,这些都是套话。

于是我开始收集 few-shot case,分类型放进 prompt,做了多轮验证。结论是:

  1. case 超过 6 个、到 10 个以后,开始出现注意力丢失;
  2. 在注意力不丢失的前提下,10 个 case 比 5 个 case 的有效率高,尽量多加 case 是提升有效率的重要途径;
  3. 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 里给了四类示例:

  1. 两段代码一致,未应用修复;
  2. 两段代码不一致,应用了修复;
  3. 两段代码不一致,但完全没有关联,未应用修复;
  4. 两段代码不一致,部分应用了修复,但没有完全应用。

业界参考

字节的 BitsAI-CR。 它的论文上个月刚公开,核心看点有三个:

  1. 两阶段评论生成:RuleChecker 基于内部的多维审查规则生成评论;ReviewFilter 用另一个微调模型对输出做二次验证,处理幻觉和低价值评论。
  2. Outdated Rate 指标:只看准确率有两个根本限制:反映不了开发者是否真正采纳,人工评估也需要大量人力。Outdated Rate 追踪被标记的代码行在后续提交中是否被修改,量化审查建议的实际价值。这和我做的采纳自动标注是同一个思路。
  3. 数据飞轮:用户直接反馈、每日抽样人工标注、每周 Outdated Rate 监控,三者结合持续优化规则。

Ellipsis。 国外已经商用的智能代码审查工具。它的做法包括:用代码切片和 AST 解析实现仓库级搜索,多个小型 Agent 分工审查,多层过滤管道减少误报,代码仓库向量化并增量更新,用户数据飞轮,以及用 eval 机制及时感知整体效果。

稳定性和后续

稳定性上主要考虑三件事:

  1. patch 太多导致审查时间过长、单次 issue 过多,需要监控 patch 行数和块数;
  2. 大模型本身不稳定,要有重试和降级;
  3. 整体流程失败的重试机制。

后续有两个方向:

  1. 对每个 issue 生成的修复代码构造测试用例并真正运行。优点是能保证改动可以实际运行;缺点是耗时耗资源,而且大部分提交本身都能运行,只有边界场景会出问题。
  2. 把 RAG 后置。现在是召回代码和 issue 放进 prompt,链路长,经过大模型识别后有效性会进一步下降;如果让 RAG 直接对生成的 issue 做过滤,效果可能会更立竿见影。

这半年最大的体会:AI 代码审查的难点不在发现更多问题,而在只留下值得处理的那几条。 宁可少报,不要乱报。