多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

AI Code Review架构实战:从Git Diff到大模型的分层编排与上下文工程

AI Code Review架构实战:从Git Diff到大模型的分层编排与上下文工程 1. 为什么Git Diff 丢给大模型这条路走不通1.1 一个被低估的起点diff 本身就不是给机器读的很多人做 AI Code Review 的第一反应特别朴素拿到git diff的输出拼一段 prompt丢给大模型让它吐评论。我最早也是这么干的跑通 demo 只花了半小时然后上线第一天就被现实教育了。问题出在git diff这个格式本身。它是为人类阅读设计的不是为机器理解设计的。你随便看一段真实的 diff -142,7 142,9 def process_order(order_id): - result db.query(fSELECT * FROM orders WHERE id {order_id}) result db.query(SELECT * FROM orders WHERE id %s, (order_id,)) if not result: raise OrderNotFound(order_id) return result这段 diff 里大模型能看到什么它看到了一行被删、两行被加。但它看不到这个函数在文件里的上下文——process_order被谁调用、调用方有没有处理OrderNotFound、这个改动会不会破坏某个依赖返回None的分支。diff 的上下文行默认只有 3 行git diff -U10能多给一点但依然只是附近不是相关。这就是第一个核心矛盾代码审查需要的是语义上下文而 diff 提供的是文本上下文。两者根本不是一回事。一个改动是否安全取决于它在整个调用链、整个模块、整个仓库里的位置而不是它在文件里上下相邻的那几行。1.2 大模型在纯 diff 上的三种典型翻车我把早期踩过的坑归了三类基本覆盖了diff LLM方案的死法。第一类上下文缺失导致的误报。模型看到新增了一行raise OrderNotFound(order_id)立刻评论建议补充异常处理。但实际上调用方早就用try/except OrderNotFound包住了。模型不知道因为它只看到了被改的这个函数。这类误报在真实项目里占比极高我统计过一轮纯 diff 方案里大约 40% 的评论属于上下文不足导致的假阳性。第二类跨文件改动被割裂。一个功能改动往往涉及接口定义、实现、调用方、测试四个文件。diff 会把这四个文件的改动分开列模型逐段看很难自己拼出这是一次接口签名变更所有调用方都同步改了这个整体判断。结果就是它对着接口文件说这个参数没人用对着调用方说这个参数哪来的。第三类噪声淹没信号。真实的 diff 里混着格式化改动、import 重排、锁文件更新、自动生成的代码。这些内容对审查毫无价值但会大量消耗模型的注意力预算。一个 2000 行的 diff 里可能只有 50 行是真正需要审的逻辑剩下 1950 行是噪声。模型在噪声里泡久了要么漏掉真问题要么在噪声上编出问题。1.3 一个反直觉的结论diff 是变更记录不是审查输入想清楚这件事之后我的认知发生了一个转变diff 的职责是记录改了什么而 Code Review 需要回答的是这个改动对不对。这是两个完全不同的问题中间隔着一整套语义重建的工作。打个比方diff 就像监控录像里某人进了房间这一帧而 Code Review 要判断的是他进房间这个行为在这个场景下是否合理——你得知道房间是金库还是茶水间、他有没有钥匙、他平时进不进这个房间。只看那一帧你什么也判断不了。所以 OpenCodeReview 这类架构存在的根本理由不是把 diff 喂给更强的模型而是在 diff 和 LLM 之间插入一层语义重建与上下文编排。这层东西才是架构的核心也是Git Diff LLM和真正的 AI Code Review之间的分水岭。2. OpenCodeReview 的分层架构把审查拆成可编排的流水线2.1 整体分层从原始 diff 到结构化评论我理解的 OpenCodeReview 架构大致可以拆成五层每一层解决一个独立问题层与层之间通过明确的数据结构衔接。这种分层不是为了好看而是因为每一层的失败模式完全不同混在一起就没法定位问题。层级职责输入输出典型失败模式变更解析层解析 diff、识别文件类型与改动语义原始 diff结构化变更对象二进制/大文件解析失败上下文构建层拉取相关代码、调用链、历史变更对象上下文包上下文超预算、召回不准分析编排层决定谁来审、审什么、按什么顺序上下文包审查任务任务拆分粒度失衡模型推理层调用 LLM 执行具体审查审查任务原始评论幻觉、格式漂移结果聚合层去重、排序、过滤、定位原始评论最终评论误报、定位错行这张表是我自己在做类似系统时反复打磨出来的。你会发现只有第四层和 LLM 直接相关其余四层全是工程问题。这也解释了为什么diff LLM方案注定单薄——它把 80% 的工程复杂度压缩进了那 20% 的模型调用里指望模型自己搞定一切。2.2 变更解析层先搞清楚改的是什么这一层最容易被跳过但它决定了后面所有环节的质量。核心任务是把 diff 从文本变成结构化的变更对象。具体要做几件事。第一是文件分类区分源码、测试、配置、文档、锁文件、生成代码。不同类别走不同审查策略锁文件和生成代码直接跳过测试文件用更宽松的规则。第二是改动语义识别是新增函数、修改签名、删除分支、还是纯格式化这决定了后续要不要拉调用链。第三是hunk 重组把同一个逻辑改动的多个 hunk 合并成一个语义单元而不是按文件机械切分。这里有个实操细节值得说git diff --check这个命令很多人不知道它的价值。它专门检测空白字符错误、冲突标记残留、行尾空格这类问题。在变更解析层前置跑一遍git diff --check能零成本过滤掉一批低级问题根本不用浪费模型 token。我在流水线里把它放在最前面作为廉价预检效果很好。2.3 上下文构建层审查质量的天花板在这里如果只能保留一层我会保留上下文构建层。因为模型的能力是上限上下文的质量是下限而下限决定实际体验。这一层要解决的核心问题是给定一个改动怎么找到审查它所需要的全部信息同时不超出模型的上下文预算。这本质上是一个检索 排序 裁剪的问题。我常用的召回策略有这么几路调用链召回改动的函数被谁调用、调用了谁向上向下各追 1-2 层。符号召回改动涉及的类、接口、类型定义把它们的完整定义拉进来。历史召回这个文件/函数最近的相关提交尤其是被回滚过的改动往往藏着坑。测试召回覆盖这个改动的测试用例能帮模型理解预期行为。相似代码召回仓库里已有的类似实现作为项目惯例的参照。召回之后是排序和裁剪。这里有个经验不要按相关性分数简单截断要按审查任务需要什么来裁剪。比如审查一个 SQL 改动最需要的是表结构和调用方而不是这个文件的历史提交。裁剪策略要跟着任务走不能一刀切。2.4 分析编排层Agent 真正发挥作用的地方到了这一层就绕不开 Agent 这个概念了。热词里agent 和 llm 有什么区别agent 架构agent 框架与编排被反复搜说明很多人对这块是模糊的。我用一句话说清楚LLM 是会说话的大脑Agent 是带着大脑去干活的执行者。LLM 负责推理和生成Agent 负责决定下一步做什么、调用什么工具、拿到结果后怎么继续。在 Code Review 场景里Agent 的价值体现在动态编排上。一个固定的流水线只能处理预设好的情况但真实审查是发散的看到 SQL 改动要查表结构看到并发代码要查锁的使用看到配置改动要查环境差异。这些查什么的决策靠硬编码的 if-else 会爆炸靠 Agent 动态决策才合理。我设计编排层时遵循一个原则把确定性工作和探索性工作分开。确定性的部分解析、召回、格式化用固定代码稳定可控探索性的部分要不要再拉一层调用链、要不要查历史交给 Agent 决策。这样既保证了主干稳定又保留了灵活性。2.5 模型推理层与结果聚合层别让幻觉流到用户面前模型推理层反而是最标准的一层但有两个坑必须提。一是输出格式约束一定要用结构化输出JSON schema 或 function calling不要让模型自由发挥。热词里修复 llm 返回 json 的 java 库被搜说明大家在这上面吃过亏。我的做法是schema 定义得尽量简单字段少而明确复杂结构拆成多次调用比让模型一次吐一个大 JSON 稳得多。二是temperature 的设置。热词里有人问temperature 是如何在 llm 的输出中发挥作用的。在 Code Review 场景我建议把 temperature 压到很低0 到 0.2。审查需要的是稳定、可复现的判断不是创意。同一个 diff 跑两次给出完全不同的评论用户会疯掉。结果聚合层是最后一道防线。要做去重同一个问题被多个任务重复报、排序按严重程度、过滤低于置信度阈值的丢掉、定位把评论精确映射到 diff 的行号。这一层做不好前面所有努力都会以满屏废话的形式呈现给用户。3. 上下文工程决定审查质量的那 80%3.1 上下文预算的分配逻辑模型的上下文窗口是有限的怎么分配是个真问题。我的经验分配比例大致是这样改动本身占 20%直接上下文占 40%项目惯例占 20%任务指令占 20%。为什么改动本身只占 20%因为改动是问题上下文是解题条件。条件不够问题再清晰也解不出来。很多人反着来把 diff 塞满上下文挤没了结果就是模型只能基于 diff 瞎猜。具体到 token 数假设模型窗口是 128k我会给单次审查任务留 8k-16k 的预算而不是把 128k 塞满。原因有两个一是塞太满模型注意力会稀释二是留出余量给多轮交互。实测下来8k 高质量上下文的效果远好于 32k 注水上下文。3.2 召回不准比召回不全更致命这是个反直觉的点。新手总觉得多召回点总没错但实际是召回噪声的伤害大于召回缺失。拉进来一堆不相关的代码模型会被带偏在无关的地方编出问题。我踩过这个坑早期为了保险把改动文件的整个文件都拉进上下文。结果模型对着文件里没被改动的老代码一顿评论用户看到的是你审了我没改的地方。后来改成只拉改动相关的片段误报率直接降了一半。所以召回策略要精准优先。宁可少召回也不要乱召回。判断标准很简单这段上下文能不能帮模型回答这个改动对不对不能就直接砍掉。3.3 用项目惯例给模型定调每个项目都有自己的风格错误处理用异常还是返回码、日志怎么打、命名什么规范。模型不知道这些它会用通用最佳实践来审结果就是一堆建议改成 XXX的评论而 XXX 恰恰不符合项目惯例。解决办法是在上下文里注入项目惯例样本。具体做法是从仓库里挑几个被公认写得好的同类文件作为 few-shot 示例放进 prompt。模型看到这些样本就会模仿项目的风格来提建议而不是套用教科书。这个技巧我在多个项目里验证过效果立竿见影。尤其是那些有强约定的项目比如特定的分层架构、特定的 DTO 转换模式注入惯例样本后评论的贴合度提升非常明显。3.4 上下文里的安全红线热词里使用 llm 时如何防止密钥等鉴权信息泄露是个必须严肃对待的问题。Code Review 场景天然会接触大量代码而代码里可能藏着密钥、token、内部地址。我的处理原则是在上下文构建层就做脱敏而不是指望模型别看。具体做法对召回的内容做正则扫描命中密钥模式各种 key、token、password 的赋值的片段直接替换成占位符或者整段剔除。同时审查任务本身不应该需要这些敏感值——审查的是密钥有没有被硬编码这个模式而不是密钥的具体内容。另外prompt 里要明确告诉模型不要复述上下文中的任何疑似凭据。双保险比单靠一层过滤稳。4. Agent 编排的实战设计让审查会思考4.1 为什么固定流水线不够用我一开始用的是固定流水线解析 → 召回 → 审查 → 聚合四步走完。跑了一段时间发现不同改动的审查需求差异极大。一个改错别字的 diff 和一个改并发逻辑的 diff需要的上下文、审查深度、检查项完全不同。固定流水线要么对简单改动过度审查浪费要么对复杂改动审查不足漏报。这就是需要 Agent 的地方。Agent 的核心能力是根据当前情况动态决定下一步。看到并发代码它知道要去查锁的使用看到数据库改动它知道要去查索引和事务边界看到 API 改动它知道要去查版本兼容性。这种因材施教的能力是固定流水线给不了的。4.2 审查 Agent 的工具箱设计Agent 要干活得有工具。我给审查 Agent 设计的工具箱大致有这么几类代码检索工具按符号、按调用关系、按相似度检索代码。历史查询工具查文件的提交历史、查某个改动的回滚记录。静态分析工具接入 linter、类型检查器把确定性问题的判断交给工具而不是模型。测试查询工具查覆盖这个改动的测试用例。规范查询工具查项目的编码规范文档。这里有个关键设计能用工具确定的事绝不交给模型猜。比如这个变量有没有被使用用静态分析工具一秒出结果让模型去数纯属浪费还可能出错。Agent 的职责是调度工具 综合判断而不是自己硬算。4.3 任务拆分的粒度控制Agent 编排里最难的是任务拆分粒度。拆太粗一个任务里塞太多东西模型顾此失彼拆太细任务之间丢失关联评论碎片化。我的经验是按审查维度拆而不是按文件或行数拆。一次审查可以拆成安全性审查、正确性审查、性能审查、可维护性审查。每个维度独立跑最后聚合。这样拆的好处是每个任务的目标明确模型注意力集中而且不同维度可以用不同的上下文和 prompt。但要注意维度之间不是完全独立的。比如一个 SQL 注入问题既是安全性也是正确性。所以聚合层要做交叉验证同一个位置被多个维度标记的优先级要提升。4.4 Agent 的刹车机制Agent 自主决策很强大但也很危险。如果不加约束它可能陷入无限循环一直拉上下文、或者跑飞审查范围失控。所以必须有刹车机制。我设的刹车有三道最大工具调用次数比如 10 次、最大上下文预算比如 32k、最大执行时间比如 60 秒。任何一道触发Agent 就停止探索用已有信息给出结论。宁可结论不完美也不能让一次审查卡死整个流水线。热词里agent execution terminated due to error被搜说明很多人被 Agent 跑飞坑过。刹车机制不是可选项是必选项。5. 从架构到落地那些文档不会写的坑5.1 误报治理是个持续工程上线初期误报率能到 50% 以上用户很快就失去信任。误报治理没有银弹只能持续迭代。我的做法是建立误报反馈闭环用户标记这不是问题这个信号回流到系统用于调整召回策略和 prompt。具体来说我会维护一个已知误报模式库。比如建议补充异常处理这个评论如果在一个已经处理了异常的上下文里出现就是误报。把这类模式沉淀下来在结果聚合层做过滤能砍掉一大半重复误报。5.2 评论的可操作性比正确性更重要一个正确但模糊的评论这里可能有并发问题价值很低一个具体可操作的评论这个 map 在并发写建议改用 sync.Map 或加锁价值很高。所以 prompt 里要强制模型给出具体的修改建议而不是泛泛而谈。我甚至会在聚合层做一道过滤没有具体建议的评论降权或丢弃。这条规则逼着模型说人话效果很好。5.3 别忽视审查的审查模型给出的评论本身也需要被审查。我会用一个轻量的二次检查把评论和上下文再喂给模型一次问它这个评论在当前上下文下成立吗。这一步能过滤掉相当一部分幻觉。虽然多花一次调用但换来的是可信度值。5.4 性能与成本的平衡全量审查每个 diff 成本很高。我的策略是分级审查小改动走轻量流程只做基础检查大改动或高风险改动走完整流程。风险判断可以基于改动文件类型、改动行数、涉及模块等。这样既控制了成本又保证了重点改动的审查质量。6. 我对这套架构的几点个人判断做了这么久有几个判断越来越清晰。第一AI Code Review 的竞争不在模型在上下文工程。模型大家都能用但谁能把上下文构建得又准又省谁就能做出真正好用的产品。这是脏活累活也是护城河。第二Agent 不是越多越好。我见过一些方案恨不得每个环节都塞个 Agent结果系统复杂到没法调试。我的原则是只在真正需要动态决策的地方用 Agent其余用确定性代码。稳定压倒一切。第三人机协作的边界要清晰。AI 负责发现可能的问题人负责判断是不是真问题。不要试图让 AI 做最终裁决那既不现实也不安全。把 AI 定位成不知疲倦的初审员而不是最终裁判心态会好很多。最后分享一个我踩过的坑早期我追求评论越多越好觉得覆盖全面是优点。结果用户被淹没直接关掉了功能。后来改成少而精每条评论都确保有上下文支撑、有具体建议用户反而愿意看了。审查工具的价值不在于说了多少而在于说的有多少被采纳。这个认知转变比任何技术优化都重要。
返回列表