先说个我最近的感受:我在多个仓库里试过纯靠大模型直接读PR评论代码,结论很一致——AI代码审查的工具很多,但能放进CI里稳定跑的没几个。丢给LLM一个diff让它"看看有没有问题",输出往往飘忽不定,有时候能揪出真bug,有时候对着格式问题长篇大论,还有时候干脆幻觉出一个根本不存在的漏洞。真正让我觉得"这玩意能用了"的转折,是我接触到open-code-review这个项目之后才发生的。它的核心思路不是让LLM自由发挥,而是把整个审查过程拆成两层:确定性流水线负责收集、过滤、分类、去重,LLM Agent只在一个被严格约束的范围内做语义判断。这篇文章我就基于open-code-review的实际拆解,把这种"确定性流水线 + LLM Agent"的混合架构讲清楚,包括每一步为什么这么设计、参数怎么定、哪些坑我替你先踩了。
这个架构解决的核心问题有三个:漏报(该查的没查)、误报(无关问题刷屏)、成本失控(每个PR烧掉大量token)。如果你正在搭建团队的AI代码审查能力,或者单纯想理解Agent系统怎么和传统工具链配合,这篇内容应该能给你一套可以直接落地的参考框架。
1. 为什么纯LLM审查走不远:三个绕不开的坎
先说结论:纯LLM做代码审查不是"效果差",而是"不可控"。工程化最忌讳的就是不可控。
1.1 上下文窗口的物理限制
一个中型PR的diff通常在几百行到上千行,但如果牵扯到跨文件改动,你需要的上下文可能包括:改动文件本身、依赖这些文件的调用方、相关类型定义、历史变更记录、项目规范文档。把这些全部塞进上下文窗口,token消耗会迅速膨胀到几十万级别。成本还只是其一,更麻烦的是LLM在长上下文里的注意力会衰减——实测下来,超过一定长度后,模型对前面文件内容的记忆明显变弱,导致审查质量断崖式下跌。
这不只是open-code-review遇到的问题,是所有LLM Agen类工具的共同瓶颈。解决的思路不是无限加大上下文,而是把"需要大模型看的"和"不需要大模型看的"分开。
1.2 幻觉与误报的代价比漏报更高
很多人在意LLM会不会漏掉bug,但真正用过以后你会发现:幻觉(hallucination)比漏报更折磨人。一个根本不存在的空指针风险,LLM能给你编出一整套调用链来佐证。reviewer在PR里看到这种评论,第一反应是"这工具又在胡说八道",第二次就不再看了——信任崩塌之后,工具价值归零。
从工程化角度看,误报的成本是信任损耗,信任损耗的累积速度远快于漏报的修复速度。所以架构设计的首要目标不是"查得多",而是"查得准"。
1.3 规则一致性问题
代码审查里有很多"确定性"规则:禁止直接使用console.log提交、新代码必须包含测试、禁止在循环里创建对象、import顺序必须符合规范。这类规则用LLM去执行,每次结果都可能不同——同一个PR,上午审和下午审结论不一样,这在工程流程里没法接受。规则类的检查应该由确定性工具执行,只有需要语义理解的部分才交给LLM。这是open-code-review架构的第一原则。
2. 混合架构的整体设计:把确定性留在流水线,把智能留给Agent
open-code-review的架构可以概括成一句话:流水线负责"有没有问题",Agent负责"是不是问题"。这句话听起来简单,但拆分清楚了,整个系统的稳定性、成本、可维护性都会有本质改善。
2.1 架构分层
整个系统分为四层:
| 层级 | 职责 | 技术构成 | 确定性 |
|---|---|---|---|
| 采集层 | 获取MR信息、diff、元数据 | Git API、文件系统 | 100% |
| 加工层 | 提取函数/类/import/调用关系 | AST解析、静态分析 | 100% |
| 过滤层 | 规则引擎、格式检查、死代码识别 | Regex、AST规则、lint规则 | 100% |
| 决策层 | 语义理解、逻辑推理、优先级判断 | LLM Agent | 部分 |
前两层是纯粹的确定性流水线,第三层是"确定性优先、LLM兜底"的混合过滤,第四层才是Agent发挥的空间。
这个分层的核心逻辑是:越底层越要确定,越顶层越要智能。如果你的静态分析器漏了一个格式问题,你可以在规则里加上去,行为是可预期的;但如果你的LLM漏了一个格式问题,你没法通过简单的规则修复——它每次的行为都是概率性的。
2.2 流水线为什么不能只靠LLM实现
有一个试验过的方案:不做任何前置处理,直接把整个PR的diff喂给LLM,让它输出JSON格式的审查意见。结果很快暴露了两个问题:
一是JSON输出不稳定。模型偶尔会在JSON前后加markdown标记,偶尔会在JSON里写注释,偶尔直接输出纯文本。你不得不花大量精力做解析容错。二是审查颗粒度不可控。模型有时候一条意见写800字,有时候列十个问题每个就一句话,reviewer没法统一处理。
把diff预处理成结构化数据后,LLM的输入就变成了"文件A的函数X调用了文件B的函数Y,函数Y的第三个参数可能为空,请判断是否存在空指针风险"这些精确的问题。模型只需要做判断,不需要做发现,任务的复杂度大幅下降,输出质量稳定很多。
2.3 Agent在这里不是"自主行动体",而是"受限判断器"
现在很多Agent框架强调自主规划、自主执行,在代码审查场景里这其实是个误区。你不需要Agent自己去翻代码、自己决定看哪里——该看哪里,流水线已经知道了。Agent需要做的只有两件事:判断某个潜在问题是否真实存在,判断这个问题的严重性级别。
这两个判断恰恰是确定性工具做不了的。比如"这个变量命名不符合语义",AST工具无法判断什么叫"符合语义";比如"这个函数在并发场景下可能出问题",静态分析能告诉你这里有共享变量,但无法判断实际风险。这些就是Agent存在的价值。Agent的价值不在于"自主",而在于"在正确的地方做判断",这是混合架构与纯Agent方案的本质差异。
3. 确定性流水线的核心拆解:代码地图的构建
open-code-review的流水线第一阶段,是构建"代码地图"——把diff变成一份结构化的、可以被后续处理的数据。
3.1 diff的精确提取与解析
第一步是拿到PR的完整diff。注意不是直接用GitHub API返回的原始diff字符串,而是要解析成结构化信息:
- 新增了哪些文件
- 删除了哪些文件
- 每个文件新增/删除的具体行号
- 每个改动所属的函数或类
- 关联的测试文件(如果修改了
src/foo.py,对应测试在tests/test_foo.py)
这个阶段我用下来觉得最麻烦的是获取准确的改动行号。GitHub的diff格式里有@@块头部,包含起始行号和块长度,但不同平台的diff格式略有差异,GitLab和GitHub返回的数据结构不一样。open-code-review在适配层做了统一处理,如果你自己实现,建议直接封装一个DiffParser类,屏蔽平台差异。
# 伪代码示意,实际实现需处理更多边界情况 class DiffParser: def parse(self, unified_diff: str) -> list[FileChange]: files = [] current_file = None for line in unified_diff.splitlines(): if line.startswith("+++ "): current_file = FileChange(path=line[4:]) files.append(current_file) elif line.startswith("@@ "): current_file.hunks.append(self.parse_hunk(line)) return files3.2 AST解析:找到函数与调用关系
拿到diff之后,需要对改动文件做AST解析,目的是回答几个问题:这个改动在哪个函数里?这个函数被谁调用?这个改动涉及哪些变量?
这里有个性能优化技巧:不要对整个仓库做全量AST解析,只解析diff涉及的文件,然后通过import关系拉入被依赖的文件。大多数PR只涉及几个文件,全量解析在大型monorepo里会慢到不可接受。
AST解析结果包括:
- 改动函数列表(名称、参数、返回类型)
- 函数间的调用关系图
- 新增/删除的全局变量、类属性
- import变更情况
这些信息会在最后组装给Agent的时候作为"基础证据"。我自己的经验是,让Agent自己读代码分析调用关系,不如直接给它调用关系图——一是省token,二是避免了Agent幻觉出不存在的调用链。
3.3 静态规则的确定性检查
流水线里要跑一组静态检查规则,这些规则的特点是可以被精确判定,不需要任何语义理解。例如:
- 新增代码是否包含
TODO、FIXME、console.log - 是否引入了
subprocess但缺少白名单校验 - 新文件是否缺少对应的测试文件
- 是否有未使用的import
- 是否有硬编码的密钥(正则匹配API key模式)
- 方法长度/复杂度是否超过阈值
这些规则的执行结果有三个去向:直接拦截(返回给MR评论/CI失败)、作为低优先级提示(进入信息收集区)、作为后续Agent的输入候选。比如"新增代码里出现了console.log"就不要让Agent来判断了,直接report,确定性规则能做到100%准确。
注意:规则引擎的配置建议做成可声明式的(YAML或JSON),不要写死在代码里。团队之间的代码规范差异很大,开放规则给团队自己调整,工具的可接受度会高很多。
4. LLM Agent的智能决策层设计:让模型只做判断题
确定性流水线把代码地图构建好之后,就轮到Agent出场了。但我强调过,你不是把整个diff丢给LLM让它自由发挥,而是把证据组织成精确的问题,交给LLM去做判断题。
4.1 两阶段Agent设计:三明治结构
open-code-review的Agent分层设计很有参考价值,我称之为"三明治结构":
第一层:Precheck Agent(功能前置判断)
在每个文件/每个函数级别,先让Agent做一轮粗筛,判断"这个改动潜在风险高不高"。输出结果只有三个选项:高风险、中风险、低风险。高风险的进入深度检查队列,中风险的进入标准检查队列,低风险的只做记录。
这一步的作用是大幅减少深度检查的次数,降低token消耗。实测下来,一个PR里约60%的改动是低风险的格式调整、变量重命名、注释修改,这些完全没有必要做深度语义分析。
第二层:Deep Review Agent(核心审查)
针对高风险和中风险的改动,做深度审查。输入信息包括:
- 改动的完整代码段
- 相关函数调用链
- 涉及的数据流路径
- 相关的测试用例定义
- 最近的类似变更记录(如果存过历史)
要求Agent输出的维度有:逻辑正确性、潜在异常分支、并发与状态风险、安全风险、性能隐患、可维护性。每个维度输出问题描述、相关代码位置、严重级别、修复建议。输出格式用JSON,且用JSON Schema做一次结构校验,不通过的重新生成。
第三层:Suggestion Synthesizer(建议聚合)
把多个Agent的结果汇总,做冲突检测和去重。比如两个Agent都发现同一个函数有空指针风险,就合并成一条,避免重复评论。
这个分层设计的好处是:每一个层都不需要做太多事,但合起来覆盖了审查的完整链路。单一Agent塞入所有职责,Prompt会变得臃肿且行为不可控。
4.2 Prompt设计的正确姿势
有一件重要的事:Prompt里不要写"你是一个资深代码审查专家",直接描述任务结构反而效果更好。我试过很多种方式,效果最好的Prompt模板结构是:
- 明确输入格式说明(你会收到JSON,里面包含哪些字段)
- 明确输出格式要求(JSON Schema)
- 明确判断标准(列出关键检查点)
- 给出1-2个正例和反例
- 强调"不要编造不存在的调用关系,只基于提供的信息判断"
有一个细节影响很大:给Agent输入时,要明确区分"事实"和"推断"。调用关系图是事实,测试覆盖情况是事实,但"这个改动可能导致性能下降"是推断。Agent只有在事实基础上做推断,才不会产生无根据的结论。具体做法是在输入数据结构里加一个source_type字段,标注fact或inferred。
4.3 温度与采样参数的设置
聊到参数设置,很多同学直接默认temperature=0.7。但代码审查场景里,temperature要尽量低,我建议0.1-0.2,top_p设置在0.9左右。过高的随机性会让输出在"同样的事实下给出不同结论",这在工程流程里是致命的。低温让模型"保守",但代码审查本来就是保守的任务——拿不准的宁可说有风险,也不能为了讨好而沉默。
不过有个反直觉的点:温度太低也会导致漏报。如果你发现Agent对某些类型的问题总是沉默(比如对性能问题完全不做评论),可以针对该类型单独提高温度到0.3左右,最大程度覆盖不同检查维度。这个小技巧我在实践里试过,有用。
5. 流水线与Agent的协同机制:数据流与状态管理
架构拆完之后,下一步要解决协同问题:流水线产生的数据怎么交给Agent?Agent的结论怎么回流到流水线?
5.1 统一数据结构:Issue Report
open-code-review定义了一个统一的数据结构——IssueReport,它贯穿整个链路:
{ "issue_id": "f3a9c2e1", "file": "src/auth/login.py", "line_start": 42, "line_end": 58, "category": "security", "severity": "high", "title": "Timing attack risk in password comparison", "description": "Usage of built-in string compare may lead to timing-based enumeration", "confidence": 0.92, "source_type": "llm_agent", "suggestion": "Use hmac.compare_digest or similar constant-time comparison" }这个结构与确定性检查工具的输出保持同构,这样不管是规则引擎产生的还是Agent产生的,最终都能统一进入同一个报告管道。我认为这是决定混合架构是否优雅的关键细节——如果你用两套完全不同的数据结构去承接两种来源的审查结果,合并和去重会非常痛苦。
5.2 协同流程图的核心节点
虽然不能用mermaid画图,我直接用文字描述协同流程:
- Diff获取阶段:通过Git API拉取PR信息,生成统一diff
- 代码地图构建:AST解析生成函数图、调用图
- 静态规则检查:确定性规则跑完,结果直接进报告
- Agent任务分发:根据代码地图+静态规则结果,筛选出需要Agent深度检查的文件/函数
- Agent执行:对每个文件/函数执行Precheck,再决定是否进行Deep Review
- 结果回流:Agent输出转换统一格式,进入Issues列表
- 去重与优先级排序:合并重复问题,按严重度和文件热度排序,生成最终审查建议
- 评论/报告生成:发布到MR/PR评论区,或发送到Webhook
5.3 增量审查的状态缓存
这个细节很容易被忽视,但工程上影响很大:同一个PR反复推送新commit时,你的审查系统不能每次都全量重跑。open-code-review的解决方式是维护一个审查状态缓存:
- 每个文件/函数有一个hash,基于文件内容和所属commit计算
- 如果某个文件的hash没变,直接沿用上一次的审查结论
- 只有hash变化的文件,才会重新执行Agent检查
这套缓存机制我实测下来,能把第二轮之后的审查成本降到原来的20%~30%。很多MR会有4、5轮迭代,如果每一轮都全量跑Agent,成本会失控。
6. 工程化落地:性能、成本与CI集成的坑
架构设计得再漂亮,最终都要在CI里跑起来才算数。落地阶段暴露出来的实际问题,比架构阶段多得多。
6.1 延迟预算与并发策略
一个PR的审查如果超过5~8分钟,开发者的体验已经很不舒服了。全量Agent审查在代码量大时极容易超时。我的经验是:给Agent层设置严格的延迟预算,超时直接降级为静态检查结果。
具体策略:
- 流水线层(diff获取、AST解析、静态规则)必须在30秒内完成
- Agent Precheck层控制在45秒内
- Deep Review层控制在3分钟内
- 整个审查全流程控制在5分钟内
为了满足这个预算,并发是必须的。open-code-review对多个文件的Deep Review是并行执行的(每个文件一个独立任务),并发数可以通过配置调整。我建议初始并发设置在3~5,太高会撞上模型API的rate limit。
另外要设置API调用的超时时间。市面上大多数模型API不设超时的情况下,可能挂起几分钟无响应。设到60秒比较安全,超时后标记该文件为"审查失败"并通知重试,而不是卡住整个流水线。
6.2 成本控制:token是怎么烧掉的
一个容易被低估的问题是token消耗。用CLAUDE级别的模型跑一次全量审查,一个中型PR可能要烧掉相当于几万token的输入+输出。一个月跑几百个PR,成本确实很可观。
省token的实操方法:
- Diff压缩策略:不要把所有变更代码块原样喂给Agent,先用AST提取函数签名、关键变量、控制流骨架,用这些结构化信息代替完整代码。减少代码上下文冗余。
- 只查Changed Code:只把diff涉及的函数代码作为输入,不把整个文件传给模型。
- 设置输入上限:单个任务的输入超过一定长度(比如2万字符)时,优先截断或分块,分块之间采用独立判断不互相引用。
- 使用便宜模型做第一层筛选:Precheck层用轻量模型(比如更小的参数量或低价的推理模型),只有深度检查才调用强模型。第一层负责"发现问题难度低但数量大"的任务,第二层负责"精确判断"的任务。
- 结果缓存复用:同仓库同函数的审查结果按版本缓存,只对变更内容重新审查。
这些策略叠加在一起,实际单MR审查成本可以控制在很小的范围内,比不用缓存直接全量审查省60%以上。
6.3 CI集成:不要成为MR的"阻塞者"
AI代码审查在CI里的角色定位要想清楚:它应该是"辅助"而非"门禁"。一开始我们尝试把Agent的高危问题设置为CI失败条件,结果开发体验非常差——Agent偶尔的误判直接阻塞了合入,团队怨声载道。后来改成"把结果作为机器评论写入MR,仅警告不强制",大家反而更愿意看。
从定位上来说:
- 确定性规则的P0/P1问题可以设为CI失败条件(比如密钥泄露、危险函数调用)
- Agent的审查结论只作为评论/建议存在,不阻塞合入
- 提供"忽略此问题"的按钮,开发者可以标注误报,这些标注可以反馈到系统里做后续的规则校准
一旦开发者产生"这东西不懂装懂还卡我不能合代码"的抵抗情绪,工具的寿命就到头了。宁可让它安静地给建议,不要让它大声地Say No。
6.4 部署形态:独立服务还是挂载在CI里
open-code-review的部署可以做成独立的HTTP服务,由CI脚本调用API触发;也可以作为GitHub Action/GitLab CI插件直接运行。两种方式各有利弊:
独立服务的好处是状态缓存、历史记录、配置管理都可以集中维护,多个仓库共用一套基础设施;缺点是运维成本高一些,需要自己托管和监控。
直接挂载在CI里的好处是部署简单,跟CI生命周期绑定,不需要额外服务;缺点是每次运行都是"无状态"的(除非配置外部存储),历史数据不好沉淀。
我的建议是:早期直接用CI挂载方式跑起来,跑出效果后再抽象成独立服务。先验证业务价值,再优化架构形态,这样风险最低。
7. 常见问题与排查技巧实录
最后记录一些实际踩过的坑,有些问题我在配置open-code-review的过程中花了不少时间才定位到原因,直接整理出来供参考。
7.1 Agent输出不稳定:JSON解析失败
这是高频问题。有些模型,即使Prompt里明确写了"只输出JSON",也会在前后加说明文字。我的解法是:用正则先把{到最后一个}之间的内容提取出来再解析,如果解析失败,整条结果标记为"格式错误"并请求重试一次,重试仍失败则降级为只输出静态规则结果。不要为了强行解析而写复杂的容错逻辑,超过两次失败直接放弃比花一堆时间去解析更划算。
同样的道理,我在并行审查里设了最大重试次数限制,避免单个任务的坏请求阻塞整个队列。
7.2 误报率失控:Agent在"编造调用链"
之前遇到过比较头痛的问题:Agent会虚构出实际上根本不存在的函数依赖关系,然后用这个虚构的依赖来佐证自己的判断。定位后发现根因在于Prompt里给了Agent过宽的权限——它自己读了代码,自己提取了调用关系,然后基于自己有误的提取结果做判断。修正方案就是回到我们的架构:调用关系由流水线的AST解析器生成,Agent只基于list中的事实推断,绝不能自己额外读代码。
设置好之后,这类幻觉明显减少。如果你发现Agent总是在分析一个在diff里根本不存在的函数,先检查你给它的输入数据里是不是包含了无关字段,或者Prompt里是不是模棱两可。
7.3 严重级别判断标准不统一
最初我们把"严重级别"完全交给Agent定,结果一个PR里所有问题都是Medium或High,级别体系失去意义。解决方案是:定义明确的分级标准并写进Prompt。
- Critical:可能直接导致生产事故、数据丢失、安全问题
- High:明确的功能缺陷、异常处理缺失
- Medium:潜在的边界条件问题、代码结构不佳、缺少错误处理
- Low:风格类、命名类、注释类
同时流水线会根据改动文件的线上流量权重做修正:核心服务的Medium可以升级为High,边缘模块的High也可以降级为Medium。这套修正逻辑放在确定性流水线里做,不依赖Agent,保证一致性。
7.4 一个PR里Review Comment太多
曾经有位同事的新手PR被AI审查刷了80多条评论,大部分是风格问题和低优先级建议。开发者看到就崩溃了,一条都没看完。后来我们采用这样几个策略降低噪音:
- 同类问题只报一次:比如10个文件都有命名问题,只在第一个出现的位置报一次,然后说明"其他位置类似"。
- 增量式反馈:新commit只对新产生的diff做评论,历史已评论过的问题不再重复。
- 按reviewer的偏好过滤:手动设置"这个仓库只关注安全和正确性问题",风格类问题直接不进评论。
反馈质量比反馈数量重要得多。也是经验之谈。
7.5 缓存击穿:同一份内容重复审查
当两个分析任务并发处理同一个文件时,可能会出现缓存击穿——两个任务同时发现缓存未命中,同时执行了Agent调用。解决方法是给状态缓存加一个简单的锁机制:
# 简化示例 cache_lock = {} def get_or_review(file_hash: str, review_func: callable) -> ReviewResult: if file_hash in cache: return cache[file_hash] with cache_lock.setdefault(file_hash, threading.Lock()): result = review_func() cache[file_hash] = result return result多进程部署的环境建议用Redis做分布式锁,单进程环境一个dict就够了。这个细节不算复杂,但忽略它会导致同一文件同一commit被重复审查多次,token浪费很浪费,排查起来又很隐晦。
8. 已经是尾声的时候,说几句大实话
聊了这么多架构和机制,最后说点主观的体会。我在多个团队里推过AI代码审查,最大的感受是:工具解决的是判断题,流程解决的是信任题。open-code-review这种混合架构,本质上是把"哪些问题应该被提出"这个发现环节交给确定性流水线,把"这个问题是不是真的值得改"这个判断环节交给LLM Agent。这种分工,意味着每一条审查意见都同时具备"确定性的来源"和"智能的解释力"——带着调用关系图来论据说服你,比一句"我觉得这里有问题"可信得多。
落地这套东西的过程中,我最想提醒别人的一点是:不要追求审查意见的数量,不要想着替换掉人的review。在很长一段时间内,AI都只是前置漏报过滤器和重复劳动消化器,真正的架构决策、业务语义合理性判断,还是得靠人。把自己系统中所有重复性、确定性、规则性的审查内容交给流水线和Agent,让人力专注于真正的架构评审——这样的组合效率最高,团队的接受度也最强。
如果你也在搭类似的东西,建议从小仓库、小规则集起步,先把流水线层做扎实,再逐步放开Agent的权限。跑通一轮之后,再看哪些环节值得优化——大概率会发现,Agent的价值比预想的晚显现,但确定性流水线的价值比预想的早见效。