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改成了while,diff 清清楚楚。但为什么改?是因为性能问题?是因为要支持中途 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 审查
一个改动了两百个文件、上万行代码的 PR,AI 审查的效果会急剧下降。原因很简单:上下文太多,模型注意力被稀释,而且审查时间会很长。
我的处理策略是分级:
- 小 PR(< 200 行):全量 AI 审查,效果最好。
- 中 PR(200-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,很容易做出一个"看起来很智能但实际不好用"的系统。
第四,规则和模型是互补的,不是替代的。确定性高的约束用规则,需要判断的用模型。我见过一些团队试图"全部用模型解决",结果在那些本该用规则搞定的简单约束上反复翻车。规则负责"守住底线",模型负责"发现意外",这个分工最稳。
最后分享一个我在实际使用中的小体会:审查意见的质量,最终是由"审查者是否愿意看"来定义的。一条再正确的意见,如果审查者不看,价值就是零。所以做这个系统时,我花在"如何让意见被看到、被信任"上的精力,不比花在"如何让意见更准确"上的少。这两件事同等重要,甚至前者更基础。