ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

纯LLM代码审查翻车?混合架构实战:确定性流水线+LLM Agent

纯LLM代码审查翻车?混合架构实战:确定性流水线+LLM Agent 1. 为什么纯 LLM 做代码审查迟早要翻车先把结论撂在这儿用纯 LLM 做代码审查在 demo 阶段看着很惊艳一旦接入真实工程流水线两周之内必然暴露出不可维护的问题。这不是模型能力不够而是架构选型从根上就错了。open-code-review 这个项目之所以值得拿出来拆恰恰是因为它没有走把 diff 丢给大模型然后祈祷这条捷径而是老老实实搭了一条确定性流水线 LLM Agent的混合架构。我先把纯 LLM 审查的翻车现场还原一下你对照自己团队的情况看看中了几条。第一类问题是结果不稳定。同一段代码今天审出来说缺少空值判断明天再审同样的代码它说命名不规范后天可能直接给你一个 LGTM。原因很简单LLM 是概率系统temperature 只要不为 0输出就在漂移。代码审查是要卡在合并门禁上的东西一个会随机变脸的审查器团队根本没法信任它。第二类问题是上下文爆炸。一个中等规模仓库一次 PR 改动几百行、涉及十几个文件是常态。你把整个 diff 塞进上下文token 成本先不说模型对中间部分的注意力衰减是实打实的。改到第 8 个文件的时候它已经忘了第 1 个文件里定义的接口长什么样。第三类问题是无法定位、无法复现。纯 LLM 给出的审查意见经常是这段逻辑可能有问题但具体哪一行、什么条件下触发、怎么改它说不清楚。更麻烦的是你没法写测试去验证一个 LLM 审查器的行为今天过了明天挂CI 直接变成玄学。第四类问题是成本失控。每次 push 都触发全量 LLM 审查一个活跃仓库一天几十次 push账单肉眼可见地涨。而且大部分 diff 其实是无关紧要的格式化改动、注释修改根本不值得动用大模型。open-code-review 的混合架构本质上就是针对这四个问题给出的工程化解法。它的核心思路可以概括成一句话能用确定性规则解决的绝不交给 LLM必须交给 LLM 的先用确定性流水线把问题空间压缩到最小。这个思路听起来朴素但落地的时候有大量细节要抠。下面我按模块拆开讲。1.1 确定性流水线到底确定了什么很多人一听确定性流水线以为是拿几个 lint 规则跑一跑就完事这就把它想简单了。在 open-code-review 这类架构里确定性流水线承担的是问题空间裁剪和证据收集两件事它输出的不是最终审查结论而是一份结构化的审查任务包。具体来说流水线要做这几层过滤变更分类把 diff 按文件类型、变更类型新增/修改/删除/重命名打标签。纯文档改动、纯格式化改动、锁文件变更直接走快速通道不进入 LLM 环节。影响面分析基于静态分析构建调用图找出被改动函数/类的上下游引用。这一步决定了 LLM 审查时需要携带哪些上下文切片。规则预检跑一遍确定性规则集命名规范、圈复杂度、重复代码、明显的空指针风险、硬编码密钥等能定的问题直接定生成结构化告警。风险打分综合变更行数、涉及模块、历史 bug 密度给每个变更块打一个风险分决定它值不值得动用 LLM、用哪个档位的模型。这四步做完进入 LLM 环节的输入就从一坨 diff变成了带上下文、带预检结果、带风险标签的结构化任务。这一步的收益是巨大的token 消耗能降一个数量级审查的稳定性也上来了因为 LLM 面对的是一个被裁剪过的、边界清晰的问题。提示确定性流水线的价值不在于它自己能查出多少问题而在于它让 LLM 环节变得可控。如果你的流水线只是简单过滤文件后缀那基本等于没做。1.2 LLM Agent 在混合架构里扮演什么角色流水线把问题空间压缩完之后LLM Agent 接手的是那些需要语义理解、需要跨文件推理、需要结合业务意图判断的问题。比如这个改动是否真的实现了 PR 描述里声称的功能新增的错误处理分支是否覆盖了所有失败路径这个接口变更会不会破坏下游调用方的契约这段并发代码有没有潜在的竞态条件这些问题确定性规则答不了必须靠语义理解。但注意Agent 不是自由发挥它是在流水线给定的上下文切片和预检结果基础上做推理。open-code-review 里 Agent 的典型工作流是接收任务包 → 按需调用工具读文件、查调用图、跑测试片段→ 生成带行号定位的审查意见 → 输出结构化结果。这里有个关键设计Agent 的输出必须是结构化的而不是一段自然语言。结构化输出意味着每条审查意见都带file、line、severity、category、suggestion这些字段这样才能被下游的 CI 系统消费、被统计、被追踪。自然语言的我觉得这里不太好在工程上是没有价值的。2. 混合架构的分层设计与选型逻辑理解了为什么混合接下来讲怎么分层。open-code-review 的架构大致可以切成四层每一层的职责边界非常清晰这是它能工程化的前提。2.1 四层架构的职责划分层级名称核心职责是否用 LLML1接入层对接 Git 平台、拉取 diff、触发审查否L2确定性流水线变更分类、影响面分析、规则预检、风险打分否L3Agent 编排层任务分发、上下文组装、工具调用、结果聚合是L4输出与反馈层结构化结果、行内评论、门禁决策、数据回流否这个分层最重要的原则是LLM 只出现在 L3且 L3 不直接接触原始 diff。原始 diff 在 L2 就被消化成了任务包。这样做的好处是如果哪天你想换模型、想加一个模型做交叉验证改动范围被死死限制在 L3L1、L2、L4 完全不用动。我见过不少团队把 LLM 调用散落在各个模块里结果想换个模型要改十几个文件想加个缓存发现到处都要改。分层不是为了好看是为了让变更成本可控。2.2 为什么确定性部分要用流水线而不是脚本堆有人会问确定性检查我写几个 shell 脚本不就行了为什么要搞成流水线区别在于可组合性和可观测性。脚本堆的问题是每个脚本独立运行输出格式各异没法共享中间结果。影响面分析算出来的调用图规则预检想用风险打分也想用脚本堆里只能各算各的重复劳动还容易不一致。流水线的做法是把每一步的产物都标准化成中间表示IR下一步按需消费。open-code-review 里这套 IR 大致包含变更块列表、每个块的 AST 摘要、调用关系边、预检告警列表、风险分。这些 IR 是纯数据可以被任意下游步骤消费也可以被持久化下来做审计。注意流水线的每一步都应该是幂等的、可单独重跑的。这样当某一步出问题时你能快速定位并只重跑那一步而不是整个流程推倒重来。2.3 模型选型的取舍不是越贵越好L3 的模型选型是很多人纠结的点。我的经验是按风险分档选模型而不是一刀切用最强的。open-code-review 的常见做法是分三档低风险变更比如单文件、小改动、非核心模块用轻量模型甚至规则模板处理成本极低。中风险变更用中等能力模型配合完整的上下文切片。高风险变更核心模块、并发逻辑、安全相关用最强模型并且可以触发多轮推理或交叉验证。这个分档策略能把整体成本压下来一大截。实测下来一个活跃仓库里真正需要动用最强模型的变更通常不超过总量的 15%。剩下 85% 用轻量模型处理效果差异在可接受范围内但成本能差出好几倍。3. 核心环节的实操拆解前面讲的是设计思路这一节讲具体怎么落地。我会把关键环节的配置、参数、代码骨架都摆出来你可以直接对照着改。3.1 变更分类与快速通道的实现变更分类是流水线的第一道闸门它的准确性直接决定了后面所有环节的效率。核心逻辑是给每个变更文件打上类型标签然后根据标签决定走哪条通道。# 变更分类核心逻辑示意 import fnmatch FAST_TRACK_PATTERNS [ *.md, *.txt, *.rst, # 纯文档 *.lock, package-lock.json, # 锁文件 *.min.js, *.min.css, # 压缩产物 **/generated/**, **/vendor/**, # 生成代码/第三方 ] def classify_change(file_path, diff_stats): # 快速通道文档、锁文件、生成代码 for pattern in FAST_TRACK_PATTERNS: if fnmatch.fnmatch(file_path, pattern): return {channel: fast, reason: non_logic_file} # 纯格式化只有空白/缩进变化 if diff_stats.only_whitespace: return {channel: fast, reason: whitespace_only} # 纯注释变更 if diff_stats.only_comments: return {channel: fast, reason: comment_only} # 其余进入完整流水线 return {channel: full, reason: logic_change}这段逻辑看着简单但有几个坑要避开。第一only_whitespace的判断不能只看字符要考虑不同语言的缩进语义比如 Python 的缩进是有语义的纯缩进变化可能改变逻辑。第二generated目录的识别要结合项目实际有些团队把生成代码放在src/下面硬编码路径会漏。第三快速通道的判定结果要记录下来方便后续审计为什么这个变更没被审查。3.2 影响面分析与上下文切片这是整个流水线里技术含量最高的一步。目标是为每个变更块找出审查它需要知道哪些上下文然后把这些上下文切成 LLM 能消化的片段。实现上分三步走构建调用图用语言对应的静态分析工具Python 用astpycgJava 用javaparserJS/TS 用ts-morph解析整个仓库构建函数级调用关系。定位变更影响对每个改动的函数找出它的调用方谁调我和被调用方我调谁以及它实现的接口、继承的父类。切片组装把变更函数本身、直接调用方、被调用方的签名、相关接口定义组装成一个上下文包。注意只带签名和关键逻辑不要把整个文件塞进去。# 上下文切片组装示意 def build_context_slice(changed_func, call_graph, max_tokens8000): slice_parts [] # 1. 变更函数完整代码 slice_parts.append(changed_func.full_source) # 2. 直接调用方只带调用点附近代码 for caller in call_graph.get_callers(changed_func.id): slice_parts.append(caller.call_site_snippet) # 3. 被调用方签名不带实现 for callee in call_graph.get_callees(changed_func.id): slice_parts.append(callee.signature) # 4. 相关接口/父类定义 for iface in changed_func.implements: slice_parts.append(iface.signature) # 按 token 预算裁剪优先保留变更函数和直接调用方 return truncate_by_priority(slice_parts, max_tokens)这里的 token 预算分配很关键。我的经验是变更函数本身占 40%直接调用方占 30%被调用方签名占 20%接口定义占 10%。如果超预算从优先级最低的部分开始砍。千万别平均分配那样会导致最关键的变更函数被截断。提示调用图的构建要考虑动态调用反射、依赖注入。纯静态分析会漏掉一部分边可以在切片时对这类函数做保守处理——把可能的调用方都带上宁可多带不可漏带。3.3 Agent 编排与工具调用L3 的 Agent 不是简单的prompt 进、结果出它需要能主动调用工具去补充信息。open-code-review 里 Agent 可用的工具集通常包括read_file(path, range)读取指定文件的指定行范围search_symbol(name)全仓库搜索符号定义get_callers(func_id)查询调用方run_linter(file, rules)对指定文件跑特定规则get_history(file, lines)查询这段代码的历史变更记录Agent 的工作循环是接收任务包 → 判断信息是否充分 → 不充分则调用工具补充 → 生成审查意见 → 输出结构化结果。这个循环要设最大轮次限制通常 3-5 轮防止 Agent 陷入无限调用。# Agent 编排骨架示意 def run_review_agent(task_package, tools, max_rounds5): context task_package.initial_context findings [] for round_idx in range(max_rounds): response llm_call( systemREVIEW_SYSTEM_PROMPT, contextcontext, toolstools.available(), ) if response.type tool_call: tool_result tools.execute(response.tool_name, response.args) context context.append(tool_result) continue if response.type findings: findings response.findings break return normalize_findings(findings)REVIEW_SYSTEM_PROMPT的设计要点是明确要求输出结构化 JSON、明确要求每条意见带行号、明确要求区分 severity、明确禁止输出看起来没问题这类无信息量的结论。我见过太多团队的系统提示词写得太宽松导致 Agent 输出一堆废话。3.4 结构化输出与门禁决策L4 层要做的是把 Agent 的输出变成 CI 能消费的东西。核心是两件事行内评论的生成和门禁决策。行内评论就是把每条审查意见映射到具体的 diff 行上。这里有个细节Agent 给的行号是文件绝对行号而 diff 评论需要的是 diff 内的相对位置。要做一次坐标转换转换失败的意见降级为 PR 级别的普通评论。门禁决策则是根据审查结果的 severity 分布决定是否阻断合并。常见策略是Severity处理策略critical直接阻断合并必须修复high阻断合并可申请豁免medium不阻断但记录并通知low仅记录不通知这个策略要可配置不同团队、不同仓库的容忍度不一样。核心模块可以严格实验性模块可以宽松。4. 踩坑实录与排查速查表这一节是我在实际落地过程中踩过的坑以及对应的排查思路。这些内容在官方文档里基本找不到但每一条都真实影响过上线进度。4.1 典型问题与排查方法问题现象可能原因排查方法解决方向审查意见大量重复上下文切片重叠同一函数被多个任务包覆盖打印任务包检查切片边界加去重逻辑按函数 ID 合并任务Agent 频繁超轮次工具调用返回信息不足Agent 反复尝试记录每轮工具调用日志优化工具返回格式补充关键信息行号定位错乱diff 坐标转换未考虑多 hunk 情况对比 Agent 输出行号和实际文件行号修正坐标转换处理 hunk 偏移成本突然飙升高风险变更比例异常升高统计风险分分布检查风险打分逻辑修正阈值审查结果前后矛盾同一变更被多次触发审查模型输出漂移对比多次审查结果加结果缓存同一 commit 只审一次快速通道漏审分类规则误判逻辑变更被当作文档审计快速通道判定记录收紧分类规则增加人工复核4.2 独家避坑经验第一个坑不要相信一次审查就能覆盖所有问题。我一开始的预期是 Agent 能像资深工程师一样一次性把所有问题都指出来实际跑下来发现单次审查的召回率大概在 60%-70%。后来改成分维度多次审查——一次专门看逻辑正确性一次专门看边界条件一次专门看性能——召回率能提到 85% 以上。代价是成本上升所以只对高风险变更做多维度审查。第二个坑上下文切片不是越多越好。我试过把整个调用链都塞进去结果模型反而抓不住重点审查意见变得泛泛而谈。后来把切片控制在变更函数 直接调用方 被调用方签名这个范围效果反而更好。模型和人类一样信息过载的时候会偷懒。第三个坑结构化输出的 schema 要严格校验。早期我没做 schema 校验Agent 偶尔会输出格式不对的 JSON导致下游解析失败。后来加了严格的 schema 校验和重试机制解析失败率从 5% 降到了 0.1% 以下。重试的时候要把校验错误信息反馈给模型让它自己修正。第四个坑门禁策略要留后门。一开始我把 critical 设成硬阻断结果有一次误报导致紧急 hotfix 被卡住差点出事故。后来加了豁免机制特定标签的 PR 可以跳过门禁但会记录豁免原因并通知负责人。工程系统一定要有逃生通道。第五个坑审查结果要回流做评估。我见过很多团队上线了 AI 审查就完事从不评估效果。正确的做法是记录每条审查意见的后续处理——是被采纳修复了还是被标记为误报。这些数据是优化 prompt、调整阈值、评估模型的核心依据。没有回流系统就没法迭代。4.3 性能与成本优化技巧成本优化这块我总结了几个实测有效的做法结果缓存同一 commit 的审查结果缓存起来重复触发直接返回。这一条能省掉 30% 以上的重复调用。增量审查只审查本次 push 新增的变更而不是整个 PR 的全量 diff。对于迭代频繁的 PR这一条收益很大。模型降级低风险变更用轻量模型实测在简单问题上轻量模型和强模型的差异很小。批量合并把多个小变更合并成一个任务包减少调用次数。注意合并的前提是这些变更在语义上相关。异步审查非阻断性的审查异步执行不占用 CI 的关键路径时间。提示成本优化不要牺牲召回率。我的原则是宁可多花点钱也不能漏掉 critical 问题。优化应该从减少无效调用入手而不是降低审查深度。5. 这套架构还能怎么扩展混合架构的好处是扩展点清晰。基于 open-code-review 这套设计我实际尝试过几个扩展方向效果都不错。扩展一接入历史 bug 数据做风险预测。把仓库的历史 bug 修复记录和代码变更关联起来训练一个简单的风险预测模型替代手工设定的风险打分规则。实测下来基于历史数据的风险分比手工规则准不少尤其是对这个模块历史上 bug 多这类隐性风险的识别。扩展二多模型交叉验证。对 critical 级别的变更用两个不同的模型分别审查取交集作为高置信度结论取差集作为待人工复核项。这个做法能把误报率降下来代价是成本翻倍所以只对 critical 变更启用。扩展三审查意见的自动修复建议。现在 Agent 输出的是哪里有问题可以进一步让它输出怎么改甚至直接生成 patch。这个方向要谨慎自动生成的 patch 必须经过人工确认才能应用不能直接提交。扩展四团队审查风格学习。每个团队对代码的偏好不一样有的严格有的宽松。可以把团队历史的人工审查意见作为 few-shot 示例喂给 Agent让它学习团队的审查风格。这个做法能让审查意见更贴合团队习惯减少这个意见我们不关心的噪音。我个人在实际操作中的体会是混合架构的落地难点从来不在 LLM 那一层而在确定性流水线的工程质量。流水线做得扎实LLM 环节就是水到渠成流水线做得潦草再强的模型也救不回来。所以如果你正准备上手类似的项目我的建议是先把变更分类、影响面分析、上下文切片这三块打磨好别急着调 prompt。这三块稳了整个系统的下限就有保障了。
返回列表