ARTICLE DETAIL

资讯详情

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

AI代码审查架构实战:从Git Diff到Agent多轮工具调用

AI代码审查架构实战:从Git Diff到Agent多轮工具调用 1. 从一次失败的代码审查说起为什么 Git Diff 喂给 LLM 远远不够去年年底我接手了一个内部工具项目目标很朴素把每次 Pull Request 的 diff 抓出来拼一段 prompt 丢给大模型让它自动生成审查意见。第一版跑通只花了一个下午效果看起来也还行——模型能指出变量命名不规范、能发现空指针风险、偶尔还能揪出边界条件遗漏。团队里几个人试用之后反馈不错我一度觉得这事成了。但真正把它接入 CI、每天面对几十个真实 PR 之后问题开始集中爆发。最典型的一次一个 PR 只改了三行代码把某个配置项的默认值从false改成了true。diff 干净得不能再干净模型给出的意见是改动合理未发现问题。可实际上这个配置项控制的是某个下游服务的降级开关改成true之后会导致所有请求走降级逻辑线上直接出事故。模型看不到这个配置项在哪里被消费、被谁依赖、改动会影响哪些调用方——它只看到了那三行文本。这件事让我彻底想明白一个道理Git Diff 是文本层面的变更快照而 Code Review 需要的是语义层面的影响推理。这两者之间隔着一整条鸿沟。你给模型再强的推理能力它拿到的输入本身就是残缺的输出的质量天花板就被锁死了。OpenCodeReview 这个架构之所以值得拆解正是因为它试图回答一个核心问题当 LLM 已经足够聪明时AI Code Review 的瓶颈到底在哪里答案不在模型而在喂给模型什么以及模型如何与代码库交互。这篇文章我会从架构层面把这个问题拆开讲清楚包括为什么单纯的 diff LLM 会失效、一个合格的 AI Code Review 系统需要哪些组件、以及在实际落地时那些文档里不会写的坑。如果你正在做类似的事情——不管是自研代码审查工具还是在团队里推 AI 辅助 Review——这篇内容应该能帮你少走至少半年的弯路。我会尽量把每个设计决策背后的为什么讲透而不是只给结论。2. Git Diff LLM 的三种失效模式不是模型不行是输入不对很多人第一次做 AI Code Review 时直觉都是diff 就是变更变更就是需要审查的内容。这个直觉在简单场景下成立但在真实工程里会以三种方式失效。理解这三种失效模式是设计任何 AI Code Review 架构的起点。2.1 上下文缺失被审查的代码只是冰山一角一个函数被修改了diff 里只有这个函数的新旧版本。但这个函数可能被十几个地方调用它的返回值可能被某个中间件依赖它抛出的异常可能被上层捕获后做了特殊处理。这些信息全都不在 diff 里。我见过最离谱的案例是一个 PR 修改了某个工具函数的参数顺序。diff 看起来只是两个参数换了个位置模型说参数顺序调整注意调用方同步修改。但模型不知道的是这个函数在代码库里有两百多个调用点其中三十多个用的是位置传参。PR 作者只改了其中五个剩下的全炸了。如果审查系统能拿到这个函数的所有调用点这个上下文模型完全有能力指出还有二十多个调用点未同步修改。这里的关键认知是代码审查的本质是影响分析而不是文本比对。影响分析需要的是调用图、依赖关系、数据流这些都不是 diff 能提供的。2.2 意图不可见diff 告诉你改了什么但没告诉你为什么改一个 PR 把某个循环从for改成了whilediff 清清楚楚。但为什么改是因为性能问题是因为要支持中途 break还是因为原来的循环在某个边界条件下会死循环这些意图信息藏在 PR 描述、关联的 issue、甚至作者的脑子里diff 里一个字都没有。模型在没有意图信息的情况下只能做语法层面的审查——命名、格式、明显的逻辑错误。它没法判断这个改动是否真的解决了它声称要解决的问题。而后者恰恰是 Code Review 最有价值的部分。我在实际项目里的做法是把 PR 描述、关联 issue、commit message 全部作为上下文注入。哪怕这些文本质量参差不齐也比没有强。实测下来光是加上 PR 描述这一项模型给出的意见相关性就能提升一大截。2.3 全局一致性无法验证单文件视角看不到跨文件约束有些约束是跨文件的。比如项目里约定所有数据库查询必须走 ORM 层不允许裸写 SQL这个约束不会写在任何一个文件的 diff 里但审查时必须检查。再比如新增的 API 端点必须在网关配置里注册这是两个不同文件的联动。diff LLM 的模式下模型每次只看一个文件的变更它没有项目级约束这个概念。你可能会说那我把约束写进 prompt 不就行了——可以但约束会越来越多prompt 会越来越长而且模型在长 prompt 下的注意力分配是个玄学问题。更根本的是很多约束是隐式的、从代码库里长出来的你很难穷举。下面这张表总结了三种失效模式的核心差异失效模式根因典型表现架构层面的解法上下文缺失输入只有变更文本调用方未同步修改、依赖断裂代码检索 调用图构建意图不可见缺少变更动机信息意见泛泛、抓不住重点多源上下文注入PR/issue/commit全局一致性单文件视角跨文件约束被忽略项目级规则库 多文件联合分析理解了这三层就能明白为什么 OpenCodeReview 这类架构要把大量精力花在上下文工程上而不是单纯调模型。模型能力是必要条件但上下文才是决定上限的变量。3. OpenCodeReview 的分层架构把审查拆成可组合的能力单元OpenCodeReview 的架构思路我理解下来核心是把一个模糊的审查任务拆解成若干个职责清晰、可独立演进的能力层。每一层解决一类问题层与层之间通过明确定义的数据结构通信。这样做的好处是当某一层效果不好时你能定位到具体是哪一层的问题而不是笼统地说AI 审查不准。3.1 变更感知层不只是解析 diff而是构建变更语义图最底层是变更感知层。它的输入是原始 diff输出不是变更的文本而是变更的语义结构。这两者差别很大。原始 diff 是一堆和-行加上一些 hunk header。变更感知层要做的是识别出这次变更涉及哪些函数、哪些类、哪些模块每个变更属于什么类型新增、删除、修改、重命名变更之间的关联关系是什么比如 A 函数的签名改了B 函数是它的调用方。我自己的实现里这一层会输出一个结构化的变更描述大概长这样{ changed_symbols: [ { name: processOrder, type: function, file: src/order/processor.ts, change_type: signature_modified, old_signature: processOrder(orderId: string), new_signature: processOrder(orderId: string, options: ProcessOptions) } ], related_symbols: [ { name: handleCheckout, relation: caller, file: src/checkout/handler.ts, call_sites: 3 } ] }有了这个结构后续的检索层就知道该去代码库里找什么——找processOrder的所有调用点检查它们是否都传了新的options参数。这就是从文本比对升级到语义推理的关键一步。实操心得变更感知层最容易踩的坑是过度解析。有些 diff 涉及大量格式化变更比如整个文件重新缩进如果逐行分析会浪费大量算力。我的做法是先做一次语义等价性判断把纯格式变更过滤掉只保留有语义变化的 hunk。这一步能砍掉 30% 到 50% 的无效分析。3.2 上下文检索层按需拉取而不是全量塞入第二层是上下文检索层。它的职责是根据变更感知层输出的语义结构去代码库、文档库、历史 PR 库里检索相关的上下文。这里有个关键设计决策是全量塞入还是按需检索早期我试过全量塞入——把整个代码库的相关文件都拼进 prompt。结果 prompt 动辄几十万 token成本高不说模型在超长上下文里的表现反而不稳定经常忘记前面的内容。按需检索的思路是变更感知层告诉我processOrder的签名改了有 3 个调用点检索层就只去拉这 3 个调用点的代码加上processOrder本身的完整实现再加上相关的类型定义。这样拼出来的上下文是精准的、可控的。检索层通常需要几个能力符号级检索给定符号名找到它的定义、引用、调用点。这需要代码索引支持简单的文本搜索不够得用 AST 级别的索引。语义检索给定一段自然语言描述比如 PR 描述找到语义相关的代码片段。这需要向量检索。历史检索找到这个文件/函数过去的变更历史看看有没有反复修改的模式。一个函数如果半年内被改了十几次那它大概率是个热点值得重点审查。我实测下来符号级检索的性价比最高语义检索作为补充历史检索在特定场景比如识别回退式修改下很有用。3.3 推理编排层让 LLM 做它擅长的事别让它做检索第三层是推理编排层。这一层是很多人容易搞混的地方——他们把所有事情都丢给 LLM包括去代码库里找调用点这种检索任务。但 LLM 不擅长检索它擅长的是给定充分上下文后的推理和判断。OpenCodeReview 这类架构的做法是把检索和推理分离。检索由专门的工具完成代码索引、向量库推理由 LLM 完成。LLM 在推理过程中如果需要更多上下文可以通过工具调用的方式主动请求而不是一次性把所有东西塞给它。这就是 Agent 思路在 Code Review 场景的体现。一个典型的推理流程可能是LLM 看到变更processOrder签名改了LLM 判断需要检查所有调用点LLM 调用工具find_callers(processOrder)工具返回3 个调用点其中 2 个已更新1 个未更新LLM 基于这个结果生成审查意见handleCheckout中的调用点未同步更新会导致类型错误这个流程比一次性塞入所有调用点然后让模型自己找要可靠得多因为检索的准确性由工具保证模型只需要做它擅长的判断。3.4 规则与知识层把团队经验沉淀成可执行的约束最上层是规则与知识层。这一层承载的是团队特有的审查标准——那些不在通用最佳实践里、但对你团队很重要的约束。比如所有对外 API 必须有超时设置、数据库迁移脚本必须可回滚、新增依赖必须经过安全扫描。这些规则如果每次都写进 prompt既冗长又容易遗漏。更好的做法是把它们做成结构化的规则在推理编排层按需注入。规则的形式可以很多样模式匹配规则检测到 diff 里出现fetch(且没有timeout参数触发提醒自然语言规则用一段话描述约束让 LLM 判断是否违反示例规则给出正例和反例让 LLM 做 few-shot 判断我自己的经验是模式匹配规则适合那些确定性高、容易形式化的约束自然语言规则适合需要判断的约束。两者结合覆盖率最高。4. Agent 化改造从一次性问答到多轮工具调用的实战差异把 Code Review 从diff LLM 一次性问答改造成Agent 多轮工具调用是我在这个项目里做的最有价值的架构调整。但这个过程不是简单的加个工具调用就完事中间有几个关键的设计决策直接决定了最终效果。4.1 为什么一次性问答模式会撞到天花板一次性问答模式的流程是拼 prompt → 调模型 → 拿结果。它的隐含假设是所有需要的信息都能在调模型之前准备好。但 Code Review 场景下这个假设经常不成立。原因在于审查过程中需要什么上下文往往取决于审查过程中发现了什么。比如模型看到一处数据库查询它需要判断这个查询有没有走索引。要判断这一点它需要知道表结构、索引定义、查询条件。但需要看索引定义这个需求是在模型看到查询语句之后才产生的。一次性问答模式下你要么提前把所有可能相关的信息都塞进去浪费且容易超长要么就接受信息不全。Agent 模式解决的就是这个问题模型可以在推理过程中发现自己缺什么然后主动去取。这个能力在复杂审查场景下是决定性的。4.2 工具集设计给 Agent 配什么工具比给它多强的模型更重要Agent 的能力边界很大程度上由工具集决定。工具设计得好普通模型也能做出靠谱的审查工具设计得差再强的模型也白搭。我在项目里最终沉淀下来的工具集大概有这么几类代码检索类工具find_symbol_definition(symbol_name)找符号定义find_callers(symbol_name)找调用点find_references(symbol_name)找所有引用get_file_content(file_path, range)读文件内容历史与元数据类工具get_file_history(file_path, limit)获取文件变更历史get_pr_description(pr_id)获取 PR 描述get_related_issues(pr_id)获取关联 issue分析类工具check_type_compatibility(symbol, change)类型兼容性检查run_linter(file_path)跑静态检查check_rule_violation(rule_id, diff)检查特定规则这里有个设计原则工具的输出要结构化不要返回大段原始文本。比如find_callers返回的应该是调用点的列表文件、行号、上下文片段而不是整个文件的内容。结构化输出能让模型更容易消费也更容易控制 token 消耗。踩坑记录我一开始设计的工具返回的是相关文件的完整内容结果模型经常被无关代码干扰给出的意见跑偏。改成返回精准的代码片段 位置信息之后意见的准确率明显提升。工具的输出粒度直接决定了模型的注意力质量。4.3 多轮调用的成本控制不是每轮都要调最强模型Agent 多轮调用带来的直接问题是成本上升。一次审查可能触发五到十轮工具调用每轮都要调模型token 消耗是单次问答的好几倍。我的优化策略是分层用模型规划轮用强模型负责判断这次审查需要检查哪些方面、需要调用哪些工具。这一轮的质量决定了后续所有轮次的方向值得用好模型。执行轮用中等模型负责根据工具返回的结果做具体判断。这一轮的任务相对确定中等模型够用。汇总轮用强模型负责把所有发现整合成最终的审查意见。这一轮需要综合判断用好模型。实测下来这个分层策略能在保证质量的前提下把成本压到原来的 40% 左右。另一个技巧是缓存工具调用结果——同一个 PR 里find_callers可能被调用多次结果是一样的缓存起来避免重复检索。4.4 终止条件设计Agent 什么时候该停下来Agent 模式最容易失控的地方是停不下来。模型可能陷入循环调用工具 → 觉得信息不够 → 再调用 → 还是觉得不够。没有明确的终止条件审查任务可能跑很久。我用的终止条件组合是工具调用次数上限硬性限制比如最多 15 次工具调用信息充分性判断让模型在每轮结束时判断当前信息是否足以给出审查意见如果够了就停止无新增信息检测如果连续两轮工具调用返回的信息高度重叠说明已经收敛强制停止时间预算整个审查任务的时间上限超时就用当前已有信息生成意见这几个条件里信息充分性判断是最关键的。我在 prompt 里明确告诉模型你的目标是给出高质量的审查意见不是穷尽所有信息。当你能够对变更做出有依据的判断时就应该停止检索开始生成意见。这句话显著减少了无效的工具调用。5. 上下文工程的核心细节检索什么、怎么排序、如何压缩上下文工程是 AI Code Review 里最脏也最见功力的部分。模型能力是公开的但喂什么给它是每个团队自己的事。这一节我把自己踩过的坑和总结的方法讲透。5.1 检索优先级不是所有上下文都同等重要面对一个变更可能相关的上下文有很多变更文件本身、调用方、被调用方、类型定义、测试文件、相关文档、历史 PR。全塞进去不现实必须有优先级。我的优先级排序是这样的变更符号的完整定义最高优先级。模型必须看到变更的完整上下文而不只是 diff 里的几行。直接调用方次高优先级。签名变更、行为变更的影响首先体现在调用方。类型定义与接口如果变更涉及类型类型定义必须给。测试文件测试能反映这个代码被期望怎么用对判断变更合理性很有帮助。历史变更帮助识别这个改动是不是在重复过去的错误。相关文档优先级最低因为文档往往过时而且噪声大。这个排序不是拍脑袋来的是根据信息对审查结论的影响程度排的。实测下来前两项覆盖了 80% 的有效审查意见所需的信息。5.2 上下文压缩token 预算下的取舍艺术即使做了优先级排序拼出来的上下文还是可能超预算。这时候需要压缩。压缩不是简单截断而是有策略地保留关键信息。我常用的压缩手段函数体摘要对于不需要逐行审查的函数用 LLM 生成一段摘要代替完整代码。摘要保留签名、关键逻辑、副作用去掉实现细节。调用点聚合如果某个函数有 50 个调用点不需要全部列出。按是否已更新分组只列出未更新的调用点已更新的给个数量即可。diff 精简对于大 diff按 hunk 的重要性排序优先保留涉及逻辑变更的 hunk格式变更的 hunk 可以折叠。历史折叠文件历史不需要每次都列全只列最近 N 次涉及同一函数的变更。一个反直觉的经验压缩上下文时宁可少给不要给错。我试过为了信息完整把一些不确定是否相关的内容也塞进去结果模型被误导给出了错误的审查意见。后来改成只给高置信度相关的上下文虽然偶尔会漏掉一些信息但意见的准确率反而更高。5.3 上下文顺序模型对开头和结尾更敏感这是一个容易被忽略的细节上下文在 prompt 里的排列顺序会影响模型的注意力分配。模型对 prompt 开头和结尾的内容记得更牢中间部分容易被遗忘。基于这个特性我的排列策略是开头放任务说明和审查标准。让模型一开始就明确我要做什么、按什么标准做。中间放检索到的上下文。这部分内容多放中间即使被部分遗忘影响也相对小。结尾放变更本身diff和请开始审查的指令。让模型在最后看到最核心的审查对象。这个顺序和很多人的直觉相反——很多人习惯把 diff 放最前面。但实测下来diff 放结尾的效果更好因为模型在生成意见时diff 还在短期记忆里。5.4 负面示例的价值告诉模型什么不要报审查系统的一个常见问题是误报太多。模型倾向于把任何它觉得可能有问题的地方都报出来导致审查意见里充斥着建议检查 XXX这种低价值内容。解决这个问题的有效手段是注入负面示例。在 prompt 里明确告诉模型以下类型的问题不要报纯格式问题、命名风格问题除非违反项目明确规范、没有具体依据的猜测性意见。我还会给一些具体的反例不要报这类意见 - 建议添加注释除非逻辑确实复杂且无注释 - 变量名可以更清晰除非命名严重误导 - 考虑性能优化除非有明确的性能问题证据 要报这类意见 - 明确的逻辑错误 - 调用方未同步修改 - 违反项目明确规则 - 有具体依据的边界条件问题加了负面示例之后审查意见的信噪比提升非常明显。团队反馈从意见太多看不过来变成了每条意见都值得看。6. 落地时那些文档不会写的坑从 Demo 到生产环境的距离把 AI Code Review 从 Demo 做到生产可用中间的距离比想象中大。这一节我列几个自己踩过的、在架构设计阶段不容易想到的坑。6.1 大 PR 的处理不是所有 PR 都适合 AI 审查一个改动了两百个文件、上万行代码的 PRAI 审查的效果会急剧下降。原因很简单上下文太多模型注意力被稀释而且审查时间会很长。我的处理策略是分级小 PR 200 行全量 AI 审查效果最好。中 PR200-1000 行按文件分组分批审查最后汇总。大 PR 1000 行只审查核心文件根据变更影响面排序其余文件走传统审查或抽样审查。这个分级策略需要在架构里预留分批处理的能力。我一开始没考虑这点遇到大 PR 直接超时后来补上分批逻辑才解决。6.2 误报的代价为什么宁可漏报不可误报在审查系统里误报把没问题的地方报成有问题的代价远高于漏报。原因是误报会消耗审查者的信任。如果审查者连续看到几条无意义的意见他就会开始忽略所有 AI 意见包括那些真正有价值的。所以我在调优时的原则是宁可漏报不可误报。具体做法提高意见的置信度阈值只有高置信度的问题才报对每条意见要求模型给出具体依据没有依据的不报定期收集审查者的反馈这条意见有用/没用用来调整阈值这个原则听起来简单但执行起来需要克制——看到模型能发现那么多问题很容易想全部报出来。但生产环境里克制才是对的。6.3 与现有工作流的集成AI 意见放哪里AI 审查意见放在哪里直接影响它被采纳的概率。我试过几种方案独立评论每条意见单独发一条评论。问题是评论太多刷屏。汇总评论所有意见汇总成一条评论。问题是长评论容易被跳过。行内评论意见直接挂在对应的代码行上。这是效果最好的因为审查者看到代码时正好看到意见。最终我采用的是行内评论为主 汇总摘要为辅的方案具体问题挂在行内整体评价比如这个 PR 整体质量不错有 3 处需要关注放在汇总评论里。6.4 持续迭代审查规则不是一次写完的AI Code Review 系统上线不是终点而是起点。团队会不断发现这类问题应该报但没报、这类问题不该报但报了这些反馈需要能快速转化成规则调整。我在架构里预留了一个规则热更新的能力规则存在配置里修改后不需要重新部署就能生效。同时每次审查的结果都会记录方便后续分析哪些规则误报率高、哪些规则漏报多。这个迭代机制是系统能长期存活的关键。没有它系统上线三个月后就会因为意见越来越不准而被弃用。7. 我对这套架构的几点个人判断拆解完 OpenCodeReview 这类架构之后有几个判断我想单独说一下因为它们影响的是要不要投入做这件事的决策。第一AI Code Review 的价值不在替代人而在覆盖人覆盖不到的地方。人审查代码时注意力是有限的容易漏掉跨文件的联动、容易忽略历史模式。AI 的优势恰恰在这些需要全局视野的地方。把 AI 定位成人的补充而不是人的替代架构设计会清晰很多。第二上下文工程的重要性被严重低估。大部分讨论都在聊用哪个模型但实际效果差异主要来自喂什么上下文。同样的模型上下文工程做得好和做得差审查质量能差出一个数量级。如果只能在一个地方投入我会投上下文工程。第三Agent 化是方向但不是银弹。多轮工具调用确实能解决一次性问答的信息不足问题但它也带来了成本、延迟、可控性的挑战。我的建议是先从增强上下文的一次性问答做起把上下文工程做扎实再逐步引入 Agent 能力。跳过第一步直接上 Agent很容易做出一个看起来很智能但实际不好用的系统。第四规则和模型是互补的不是替代的。确定性高的约束用规则需要判断的用模型。我见过一些团队试图全部用模型解决结果在那些本该用规则搞定的简单约束上反复翻车。规则负责守住底线模型负责发现意外这个分工最稳。最后分享一个我在实际使用中的小体会审查意见的质量最终是由审查者是否愿意看来定义的。一条再正确的意见如果审查者不看价值就是零。所以做这个系统时我花在如何让意见被看到、被信任上的精力不比花在如何让意见更准确上的少。这两件事同等重要甚至前者更基础。
返回列表