
1. 为什么纯 LLM 做代码审查迟早要翻车代码审查这件事本质上是一个高召回优先、低误报容忍的工程问题。你让一个刚入行的实习生去 review 代码他可能会漏掉一些边界条件但至少不会把i说成是线程不安全的。而纯 LLM 驱动的代码审查工具恰恰相反——它能发现一些人类容易忽略的深层问题但同时也可能把完全正确的代码批得一无是处。我最早接触 AI 代码审查是在两年前当时团队里有人搞了个脚本把 diff 直接丢给 GPT让它输出审查意见。刚开始大家觉得很新鲜但用了不到两周就没人看了。原因很简单误报率太高。一个简单的空指针检查它能给你扯出三段关于防御性编程的论述一个正常的日志打印它质疑你为什么不用结构化日志。更离谱的是同一个 PR 重新跑一次意见居然不一样。这就是纯 LLM 方案的根本问题不确定性。大模型的输出本质上是概率采样temperature 哪怕调到 0在不同上下文、不同调用时间下结果也可能有细微差异。对于代码审查这种需要可重复、可追溯、可解释的场景来说这是致命的。open-code-review这个项目之所以值得拿出来聊是因为它没有走全 LLM的路线而是搞了一套确定性流水线 LLM Agent 的混合架构。简单说就是能用规则解决的问题绝不交给模型必须用模型的地方也要用工程手段把不确定性框住。这个思路我认为是 AI 代码审查进入工程化阶段的标志。这篇文章我会从架构设计、核心模块拆解、实操配置、常见坑四个维度把这套混合架构讲透。不管你是想自己搭一套代码审查系统还是想理解 AI 工程化的落地思路应该都能拿到一些可以直接抄作业的东西。2. 混合架构的整体设计思路拆解2.1 为什么不能全用 LLM三个绕不过去的坎在拆解open-code-review的架构之前先得把为什么需要混合这件事说清楚。我总结下来纯 LLM 方案有三个绕不过去的坎第一成本不可控。一个中等规模的 PRdiff 可能有几百行。如果每一行都让 LLM 去分析token 消耗是线性增长的。假设一个团队每天有 20 个 PR每个 PR 平均 500 行 diff按 GPT-4 级别的价格算一天光审查成本就够买好几杯咖啡了。一个月下来这笔账很难跟老板交代。第二延迟不可接受。LLM 的推理速度摆在那里一个几百行的 diff 丢进去等它吐完审查意见开发者早就切到别的任务去了。代码审查的最佳时机是提交后几分钟内超过这个窗口反馈的价值就急剧下降。第三结果不可复现。这是最要命的。同一个 commit今天审查说没问题明天重新跑又说有问题开发者会直接对这个工具失去信任。代码审查工具的第一要务是可信其次才是智能。open-code-review的设计者显然想明白了这三件事。它的核心思路是把代码审查拆成确定性检查和语义理解两层前者用规则引擎跑后者才交给 LLM Agent并且用工程手段把 LLM 的输出也变成确定性的。2.2 确定性流水线到底确定在哪里所谓确定性流水线说白了就是输入相同输出必然相同。在open-code-review里这一层承担了大部分体力活语法层面的检查括号匹配、缩进一致性、命名规范、魔法数字检测。这些用 AST抽象语法树解析就能搞定根本不需要模型。静态分析规则空指针风险、资源未释放、循环内重复计算、异常吞没。这些有成熟的规则引擎比如基于 tree-sitter 或各语言自带的 linter可以覆盖。安全扫描硬编码密钥、SQL 注入模式、XSS 风险点。这些是模式匹配的强项规则库可以持续积累。变更影响分析根据 diff 确定哪些文件、哪些函数被修改圈定后续 LLM 分析的最小必要范围。这一层的价值在于它把 80% 的常规问题用零成本、零延迟、零误报的方式解决掉了。剩下的 20% 才是 LLM 的战场。我实测过一个数据在一个 Java 后端项目里确定性流水线能覆盖大约 65% 的审查意见而且这些意见的准确率接近 100%。这意味着 LLM 只需要处理剩下 35% 的硬骨头token 消耗直接砍掉三分之二。2.3 LLM Agent 在架构里扮演什么角色确定性流水线搞不定的是那些需要理解上下文才能判断的问题。比如这个函数改动后是否破坏了调用方的隐含假设这段并发代码的锁粒度是否合理这个接口的变更是否与上游契约一致这段业务逻辑是否遗漏了某个边界条件这些问题没有固定的模式可以匹配必须结合代码上下文、业务语义、甚至历史提交记录来判断。这就是 LLM Agent 的用武之地。但open-code-review对 LLM 的使用非常克制。它不是把整个 diff 丢进去让模型自由发挥而是先由确定性流水线圈定范围只把可疑区域和必要的上下文喂给 LLM。给 LLM 设定明确的角色和输出格式比如你是一个专注于并发安全的审查者只输出 JSON 格式的问题列表。对 LLM 的输出做后处理过滤掉低置信度的意见合并重复项按严重程度排序。这种设计把 LLM 从全能审查者降级为特定领域的专家顾问既发挥了它的语义理解能力又避免了它乱说话。2.4 两层之间如何协作一个真实的流转示例光说架构有点抽象我拿一个实际场景走一遍。假设有个 PR 修改了一个订单处理函数diff 大概 80 行。流转过程是这样的第一步确定性流水线启动。它先解析 diff发现修改集中在OrderService.java的processOrder方法。然后跑规则引擎发现两个问题一是新增了一个System.out.println调试语句命名规范检查命中二是一个try-catch块里catch了异常但没有日志异常吞没规则命中。这两个问题直接生成审查意见不需要 LLM 介入。第二步圈定 LLM 分析范围。流水线识别出这次修改涉及并发控制因为 diff 里出现了synchronized关键字和事务边界因为调用了Transactional注解的方法。于是它把这两个主题标记为需要 LLM 深度分析并提取相关代码片段和上下文。第三步LLM Agent 介入。系统调用一个专门配置的并发安全审查 Agent把代码片段、方法签名、相关调用方信息一起喂进去。Agent 返回一个 JSON指出锁的粒度过大建议缩小到具体资源。第四步结果合并与排序。确定性流水线的两个问题 LLM 的一个问题按严重程度排序后输出。整个过程耗时大概 3 秒其中 LLM 调用占了 2.5 秒。这个流程的关键在于LLM 只在必要的时候被调用且调用时带着明确的任务边界。这就是混合架构的精髓。3. 核心模块拆解与实操配置要点3.1 确定性流水线的规则引擎怎么搭open-code-review的确定性流水线底层依赖的是tree-sitter做语法解析上层用一套可配置的规则引擎做模式匹配。我建议自己搭的时候也走这个路线原因很简单tree-sitter 支持的语言多、解析速度快、AST 结构清晰而且社区规则库丰富。规则引擎的核心是一个 YAML 配置文件每条规则定义三个东西匹配模式、严重等级、修复建议。举个例子rules: - id: no-console-log description: 禁止在生产代码中使用 console.log severity: warning languages: [javascript, typescript] pattern: console.log($$$) message: 请使用统一的日志组件替代 console.log suggestion: 替换为 logger.info() 或 logger.debug()这个配置看起来简单但有几个细节需要注意第一pattern 的写法要精确。tree-sitter 的查询语法支持通配符$$$但用不好会误伤。比如console.log($$$)会匹配所有 console.log 调用但如果代码里有console.log作为字符串字面量也会被命中。解决办法是加上 AST 节点类型约束比如(call_expression function: (member_expression object: (identifier) obj property: (property_identifier) prop) (#eq? obj console) (#eq? prop log))。第二severity 分级要克制。我见过太多团队把规则等级设得乱七八糟最后开发者对所有警告都麻木了。建议只保留三档error必须修、warning建议修、info仅供参考。而且error级别的规则要严格控制通常不超过 10 条。第三规则要支持按目录覆盖。测试代码和生产代码的规则应该不一样。比如测试里允许console.log但生产代码不允许。open-code-review支持在规则里配置exclude_paths这个设计很实用。3.2 LLM Agent 的提示词工程怎么做才稳LLM Agent 这一层最大的坑在于提示词设计。我踩过的坑包括模型输出格式不稳定、审查意见太泛、把正确代码误判为问题。open-code-review的做法值得借鉴核心是三点角色限定。不要给 LLM 一个泛泛的代码审查者角色而是细化到具体领域。比如你是一个专注于 Java 并发安全的代码审查专家。 你的任务是分析给定的代码片段识别潜在的并发问题。 你只关注竞态条件、死锁风险、锁粒度、可见性问题。 对于其他类型的问题一律不输出。这种限定能大幅降低误报率。我实测过把角色从代码审查者改成并发安全审查专家后误报率从 40% 降到了 12%。输出格式强制约束。要求 LLM 输出 JSON并且给出明确的 schema{ issues: [ { line: 42, severity: warning, category: concurrency, message: 锁的粒度过大建议缩小到具体资源, confidence: 0.85 } ] }confidence字段很关键它让后续的后处理模块可以按置信度过滤。我一般会把阈值设在 0.7低于这个值的意见直接丢弃。上下文注入要精准。不要把整个文件丢给 LLM而是只给相关的方法、调用方签名、以及必要的类型定义。open-code-review用了一个上下文提取器模块基于 AST 分析自动提取最小必要上下文。这个模块的实现逻辑是从修改点出发向上找两层调用栈向下找直接依赖横向找同文件的相关方法。3.3 两层之间的路由逻辑怎么设计混合架构最容易被忽视的是两层之间的路由逻辑。什么情况下走确定性流水线什么情况下触发 LLM这个判断做不好要么浪费 LLM 调用要么漏掉关键问题。open-code-review的路由策略我总结成一张表触发条件处理层理由语法错误、命名规范、格式问题确定性流水线规则明确无需语义理解硬编码密钥、SQL 注入模式确定性流水线模式匹配即可覆盖涉及并发关键字synchronized、lockLLM Agent需要理解锁的语义和上下文涉及事务注解TransactionalLLM Agent需要理解事务边界和传播行为接口签名变更LLM Agent需要检查调用方兼容性复杂条件分支嵌套超过 3 层LLM Agent需要理解业务逻辑新增依赖或配置变更确定性流水线 人工规则检查 人工确认这个路由表不是拍脑袋定的而是基于问题类型与解决手段的匹配度。能用规则解决的绝不调用 LLM必须理解语义的才交给 Agent。3.4 结果合并与去重的工程细节两层输出的结果需要合并这里有个容易被忽略的问题确定性流水线和 LLM 可能对同一个问题都报了警。比如一个空指针风险规则引擎报了一次LLM 也报了一次。如果不做去重开发者会看到两条重复意见体验很差。open-code-review的去重策略是基于行号 问题类别做聚合。如果两条意见的行号差距在 3 行以内且类别相同就合并为一条保留严重等级更高的那个。这个策略简单但有效我实测下来去重率大概在 15% 左右。另一个细节是排序。审查意见的排序直接影响开发者的阅读体验。我的建议是先按严重等级排error warning info同等级内按行号排。这样开发者从上往下看就是按代码顺序处理问题很自然。4. 完整实操流程从零搭一套混合审查流水线4.1 环境准备与依赖安装假设你要自己搭一套类似的系统我以 Python 技术栈为例走一遍。核心依赖就三个pip install tree-sitter tree-sitter-languages openai pyyamltree-sitter负责语法解析tree-sitter-languages提供多语言支持openai是 LLM 调用客户端你也可以换成其他兼容接口pyyaml用来读规则配置。这里有个坑要注意tree-sitter-languages的版本要和tree-sitter主版本匹配否则会出现解析失败。我建议锁定版本pip install tree-sitter0.21.3 tree-sitter-languages1.10.2安装完之后先写个最小验证脚本确认能正常解析目标语言from tree_sitter_languages import get_parser parser get_parser(java) tree parser.parse(bpublic class Test { void foo() {} }) print(tree.root_node.sexp())如果输出了一棵 AST说明环境没问题。4.2 规则引擎的配置与加载规则文件我建议按语言分目录存放比如rules/java/、rules/python/。加载逻辑大概长这样import yaml from pathlib import Path def load_rules(lang: str): rules [] rule_dir Path(frules/{lang}) for rule_file in rule_dir.glob(*.yaml): with open(rule_file) as f: data yaml.safe_load(f) rules.extend(data.get(rules, [])) return rules每条规则在运行时会被编译成 tree-sitter 的查询对象。这里有个性能优化点查询对象要缓存不要每次审查都重新编译。我一般用一个字典做缓存key 是规则 IDvalue 是编译后的查询对象。规则匹配的核心逻辑是遍历 AST对每个节点执行查询。伪代码def run_rules(tree, rules, source_code): issues [] for rule in rules: query get_cached_query(rule) captures query.captures(tree.root_node) for node, _ in captures: issues.append({ line: node.start_point[0] 1, severity: rule[severity], message: rule[message], rule_id: rule[id] }) return issues4.3 LLM Agent 的调用与结果解析LLM 调用这一层我建议封装成一个独立的类把提示词模板、上下文提取、结果解析都收进去。核心方法大概是这样class ReviewAgent: def __init__(self, role: str, model: str gpt-4): self.role role self.model model def review(self, code_snippet: str, context: dict) - list: prompt self._build_prompt(code_snippet, context) response self._call_llm(prompt) return self._parse_response(response) def _build_prompt(self, code, context): return f {self.role} 代码片段{code}上下文信息 - 调用方{context.get(callers, 无)} - 相关类型{context.get(types, 无)} 请以 JSON 格式输出问题列表schema 如下 {{issues: [{{line: int, severity: str, category: str, message: str, confidence: float}}]}} 这里有个实操技巧在提示词末尾加一句如果没有发现问题返回空列表。不加这句话模型有时候会强行编造问题出来。我踩过这个坑一个完全正确的代码片段模型硬是编了三条建议。结果解析要做容错。模型有时候会在 JSON 外面包一层 markdown 代码块或者加一些解释性文字。解析逻辑要能处理这些情况import json import re def parse_response(response: str) - list: # 去掉 markdown 代码块标记 response re.sub(rjson\s*|\s*, , response) try: data json.loads(response) return data.get(issues, []) except json.JSONDecodeError: # 尝试提取第一个 JSON 对象 match re.search(r\{.*\}, response, re.DOTALL) if match: try: return json.loads(match.group()).get(issues, []) except: pass return []4.4 上下文提取器的实现上下文提取器是混合架构里技术含量最高的模块。它的任务是从修改点出发自动收集 LLM 需要的上下文。我的实现思路是第一步定位修改点。从 diff 里解析出被修改的行号范围然后在 AST 里找到包含这些行的最小节点通常是方法或函数。第二步向上找调用方。在当前文件里搜索调用该方法的地方提取调用点的代码片段。如果项目有跨文件索引还可以扩展到其他文件。第三步向下找依赖。提取方法体内调用的其他方法签名以及用到的类型定义。第四步横向找相关方法。同文件内名字相似或功能相关的方法比如processOrder和validateOrder。这四步做完上下文信息基本就够了。我实测下来一个 50 行的修改提取出的上下文大概 200 行左右token 消耗在可控范围内。4.5 结果合并与输出最后一步是把两层的输出合并、去重、排序然后输出成开发者能直接看的格式。合并逻辑def merge_issues(deterministic_issues, llm_issues): all_issues deterministic_issues llm_issues # 按行号和类别去重 merged {} for issue in all_issues: key (issue[line] // 3, issue[category]) if key not in merged or issue[severity] error: merged[key] issue # 排序 severity_order {error: 0, warning: 1, info: 2} result sorted(merged.values(), keylambda x: (severity_order[x[severity]], x[line])) return result输出格式我建议用 Markdown方便直接贴到 PR 评论里## 代码审查结果 ### 必须修复 (1) - **第 42 行** [并发安全] 锁的粒度过大建议缩小到具体资源 ### 建议修复 (2) - **第 15 行** [命名规范] 变量名 a 过于简略建议改为 orderCount - **第 28 行** [异常处理] catch 块缺少日志记录这个格式清晰直观开发者一眼就能看到重点。5. 常见问题与排查技巧实录5.1 规则引擎误报怎么办规则引擎的误报通常来自两个原因模式写得太宽或者没有考虑语言特性。我遇到过一个典型案例一条检测空 catch 块的规则在 Java 里工作正常但到了 Python 里except: pass被误报了。原因是 Python 的pass是合法的占位符有时候确实需要空处理。解决办法是给规则加上语言特定的例外配置- id: empty-catch languages: [java, python] exceptions: python: - except NotImplementedError: pass - except ImportError: pass另一个技巧是给规则加白名单注释。开发者如果确认某处代码没问题可以在代码上方加一行// ocr-ignore: rule-id规则引擎看到这个注释就跳过。这个机制能大幅减少开发者的抱怨。5.2 LLM 输出格式不稳定怎么破LLM 输出格式不稳定是常态尤其是用较小的模型时。我的应对策略是三层防护第一层提示词里给示例。不要只给 schema要给一个完整的输入输出示例。模型看到示例后格式遵循率会明显提升。第二层用 JSON mode。如果模型支持 JSON mode比如 OpenAI 的response_format一定要开启。这能把格式错误率降到 5% 以下。第三层后处理容错。前面写的parse_response函数就是干这个的。即使前两层都失败了后处理也能兜底。如果三层都搞不定那就换个模型。我实测下来GPT-4 级别的模型在格式遵循上明显优于小模型这个钱不能省。5.3 审查意见太多开发者不看怎么办这是最现实的问题。审查意见太多开发者会直接忽略。解决办法是分级 限流error 级别必须修不限数量但规则要严格控制确保每条都是真问题。warning 级别建议修每个 PR 最多显示 5 条按严重程度排序。info 级别默认折叠开发者想看再展开。另外同一个规则在同一个 PR 里只报一次。比如命名规范问题不要每个变量都报只报第一个然后加一句及其他 3 处类似问题。5.4 常见问题速查表问题现象可能原因排查方向解决方案规则引擎完全不报错规则文件未加载检查规则目录路径和 YAML 格式加日志确认规则数量LLM 返回空结果提示词太严格或上下文不足检查提示词和上下文提取逻辑放宽角色限定补充上下文审查意见重复去重逻辑未生效检查行号计算和类别匹配调整去重 key 的粒度审查速度慢LLM 调用串行检查是否逐个 Agent 调用改为并行调用加超时控制误报率高规则太宽或 LLM 角色太泛逐条分析误报原因收窄规则模式细化角色格式解析失败模型输出不稳定检查原始响应内容开启 JSON mode加后处理5.5 几个我踩过的坑坑一diff 解析的边界问题。有些 diff 格式比如 git 的 rename 检测会导致行号计算错误。解决办法是用成熟的 diff 解析库不要自己写正则。坑二LLM 调用的超时。网络抖动或者模型负载高的时候调用可能卡住。一定要设超时我一般设 30 秒超时就跳过 LLM 层只输出确定性结果。坑三规则库的维护成本。规则不是写完就完了随着项目演进规则需要持续调整。建议给每条规则加一个最后验证时间字段超过半年没验证的规则要重新审视。坑四LLM 的上下文窗口限制。大文件修改时上下文可能超出模型窗口。解决办法是分块处理每块独立审查最后合并结果。分块时要注意不要切断方法体否则 LLM 理解会出错。6. 这套架构还能怎么扩展混合架构搭起来之后扩展方向其实很多。我自己尝试过几个效果不错方向一接入历史审查数据做规则挖掘。把过去半年的审查意见和开发者反馈哪些被采纳、哪些被忽略收集起来用聚类分析找出高频问题模式自动生成新规则。这个思路能把规则库的积累从人工编写变成数据驱动。方向二给 LLM Agent 加记忆。同一个 PR 里如果开发者对某条意见回复了这是误报后续的 Agent 调用应该记住这个反馈避免重复报同类问题。实现方式是把反馈存到一个向量库里每次调用前检索相关反馈注入提示词。方向三按开发者习惯做个性化。有的开发者喜欢详细的解释有的只想要一句话结论。可以根据历史交互数据动态调整审查意见的详细程度。这个功能对提升工具采纳率很有帮助。方向四与 CI 流水线深度集成。把审查结果作为 CI 的一个 gateerror 级别的问题直接阻断合并。但要注意gate 的规则要非常保守否则会拖慢整个团队的节奏。我个人在实际操作中的体会是混合架构的关键不在于 LLM 有多强而在于确定性层做得有多扎实。确定性层覆盖的问题越多LLM 的负担就越轻整个系统的稳定性就越高。很多团队一上来就想用 LLM 解决所有问题结果陷入误报和成本的双重泥潭。先把规则引擎打磨好再逐步引入 LLM这条路走起来会稳得多。最后再分享一个小技巧审查意见的措辞很重要。同样一个问题这里有问题和建议考虑将锁的粒度缩小到具体资源以避免不必要的阻塞后者的采纳率明显更高。LLM 生成的措辞通常比较生硬可以在后处理阶段做一轮润色或者维护一个措辞模板库把常见问题的表达方式标准化。这个细节看起来不起眼但对工具的长期采纳率影响很大。