ARTICLE · 1104853
OCR反漂移:我读源码钉出的3层硬契约
OCR 反漂移:我读源码钉出的 3 层硬契约
用 Cursor / Claude Code 审 PR,我踩过同一类翻车,至少 3 种:
1. 范围漂:改动一大,文件被挑着看,漏了还像审完了;
2. 落点漂:评论写得很像那么回事,一点开发现行号对不上;
3. 效果漂:提示词稍改,质量就抖,调试像玄学。
原本以为是「模型还不够强」。没想到翻阿里开源的 Open Code Review(OCR)源码后,结论反过来了:漂,往往不是模型笨,是职责边界错了。
OCR 主张「确定性工程 × Agent 混合驱动」。压缩成工程口号就是——反漂移。 但「稳」很容易被误读成「两次运行评论一字不差」。那不是它在追求的,LLM 系统也不该这么承诺。我读下来钉的是更窄、也更硬的一种稳:
契约稳:审谁、挂哪、怎么记账——不随对话心情漂移; 生成可变:深不深、怎么写、漏不漏——仍由模型决定,允许变。
下面按源码机制走 3 层硬契约,再说规模与接入上的诚实边界。(对照公开仓库,非通稿。)
一、漂移从哪来:把「必须一样」的事交给了概率
代码审查同时要两样能力:完备可定位(工程),以及语义判断与取证(模型)。
通用 Agent + Skill 常把两者塞进同一次自然语言对话。Skill 擅长分发工作流,却钉不住这些「必须可复现」的问题:
日常我只问一句:
这一步再跑一遍,必须完全一样吗? 必须 → 确定性代码说了算;可以变 → 才打开 Agent 的槽位。
「硬的归 Go」稍滑——Go 里也会发起分组、主审、过滤等模型调用。更准确是:
程序拥有状态机与契约;模型只在契约允许的槽位里推理,结果还要通过确定性闸门才能生效。
混合不是五五开和稀泥,而是 所有权清晰的分层。
二、第一层:选集不漂——「审谁」变成可预览的纯函数
选集逻辑被写成 纯函数:不访问网络、不调模型。按固定门控决定去留——二进制、密钥路径、排除/包含规则、扩展名白名单、默认测试路径、单文件 diff 过大等。更早还有对 node_modules、vendor 一类噪声目录的剔除。
关键约束:命令行预览(--preview)与真实审查 必须得出同一套答案。历史上若两边各算一遍,就会漂——那是工程自欺,不是模型幻觉。
通用 Agent 的「我会审这些文件」是对话里的承诺,跑前难核对、跑后难审计。OCR 把「审谁」提成 可预览的函数值:你预览里看到的待审列表,就是后面覆盖率分母的候选。
范围若再漂,多半是 git 输入或配置变了——可追查;而不是「今天模型心情不好」。
三、第二层:落点不漂——先救证据,再请模型
通用 Agent 习惯「报一个行号」。行号是生成的,天然漂。
OCR 要求评论带上代码片段(尽量贴近新增行),再用算法去 diff 里匹配落点,顺序刻意是:
1. 先在本文件的变更块上滑动匹配;
2. 不行再做 跨文件确定性搜索:片段若唯一落在别的变更文件,就改挂路径和行号(不用模型);
3. 仍失败才让模型重生片段,再匹配;
4. 再失败 → 宁可未锚定,不假装挂准。
过滤误报时也一样:模型只负责指出「哪几条可能是错的」,真正删除由程序执行。
为什么跨文件重挂必须抢在模型前面?
这里有个很典型的失败模式,源码注释写得很锋利——也是我这次读库最「拍桌子」的一段:
Agent 常会读到 别的文件 的代码,却把评论挂在「当前正在审的文件」上——比如声明在头文件、问题在实现里。若此时直接让模型「重新定位」:喂给它的往往是 错误文件的 diff,提示又逼它交回一段代码,它就会在错误 diff 里找「看起来最像」的片段,覆盖掉原来那份真正指向问题的证据。结果是:看起来定位成功了,实际挂在无关行上。
假锚定,比未锚定更危险。 原本以为「再让模型修一次位置」更稳,其实翻车。
所以程序必须先用原始片段在内存里的变更上做唯一匹配;匹配不到、或匹配到多个文件,都 拒绝猜测——猜错只是换一种错。
宁可不挂,也不用模型在错误上下文里「圆」出一个看起来很准的行号。
行号问题从「生成」变成「检索」:可测、可回归。模型仍要写出好的代码片段;写砸了,系统暴露未锚定,而不是用假行号粉饰。
四、第三层:账本不漂——覆盖是状态机,不是客套话
进入待审集合的文件,会在分发前被 密封:分母冻结,之后不能悄悄变少。结束时,每个文件都必须落到可记录的终态(完成、复用、失败、弃权等)。若总 token 预算触发停派,没轮到的记为预算失败,部分结果仍输出——受控截断,不当成功。
这里最容易说满,必须钉准:
通用 Agent 的漏审,经常是文件 从未进入任何可观测集合。OCR 先消灭这种账外循环:能失败,不能蒸发。
可审计的不完备,优于不可审计的「好像很全」。
公开基准里更偏准确率、召回可低一些,和这是同一条线:宁可少报噪声、宁可曝光失败,也不用对话里的「我审过了」换虚假安全感。稳定,首先是工程诚实,其次才是分数好看。
五、大了怎么办:切开,是为了不让「少看」成为续命方式
单次对话塞不下时,通用 Agent 往往靠 选择性遗忘 撑下去——这正是范围漂的温床。
OCR 不在同一轮里硬塞全仓,而是切开再审:
• 先减负:明确不该审的进不了待审集合;
• 再分组:文件多时捆成小组(每组有数量与体积上限;失败就退回「一文件一组」)。分组主要看路径和增删行数,不靠把全文 diff 再喂一遍;
• 分组后:每个小组单独审查,吃该组的 真实 git 统一 diff,默认有限并发;
• 仍超预算:可设总 token 上限,后续小组停派并记失败,而不是装成全审完。
切开和账本是一对:切开降低「被迫少看」的压力;账本保证没派到的不能装成功。
先定义可调度的切片与可审计的终态,再让模型在切片里推理。
(分组阈值等细节随版本调整,以项目文档为准。)
六、降方差:规则表与工具面,而不是更长的说明书
规则: 按文件路径匹配到一套审查原则,再和 diff 一起放进提示。所谓「模板引擎」,实质是规则表加占位符——匹配路径短、可测。它降低的是「选哪套原则」的漂移,不是消灭判断方差。
顺带:多文件多规则时会先按路径排序再拼进提示——避免文件顺序抖动导致提示前缀一直变。管道形状尽量稳,模型输出可以变。
工具: 内置工具刻意收得很窄;计划阶段与主审阶段可见工具不同;收尾轮次甚至只允许写评论和宣布结束。少给铲子,乱挖的方差就小一截。
再次强调:OCR 的「稳」不是「评论可字节级复现」。用错度量,就会失望。
七、若用委托模式:契约可拆走,硬闸默认不搬家
还有一种用法:OCR 自己不调模型,只输出「审哪些文件、用哪套规则」;真正推理交给 Cursor / Claude 等宿主(复用已有订阅额度)。
• 仍在的:选集与规则解析;
• 默认不会完整带走的:密封账本、行号匹配与跨文件重挂、误报过滤那条内嵌硬链。
若只理解成「省一把 API Key」,却以为完整继承了上面 3 层反漂移,会高估稳定性。契约碎片 ≠ 整条闸门。
八、一张总图:每一跳归谁管?
git 变更
→ 选集(纯函数,预览 ≡ 真跑)
→ 密封分母(不能蒸发,只能终态化)
→ 分组(可本地 / 可模型,有硬上限与失败回退)
→ 小组内审查(真实 diff + 规则 + 有限工具)
→ 落点闸门(匹配 → 跨文件重挂 → 可选模型 → 程序删误报)
→ 输出 + 可审计覆盖率
与其问「哪里用了 AI」,不如问:
每一跳的所有权在谁手里?出错时,系统是暴露,还是圆谎?
结语
OCR 值得写的,不是又一个审代码命令行,而是它把「反漂移」拆成可辩论的层:
1. 选集不漂——可预览的纯函数;
2. 落点不漂——先救原始证据,宁可不挂不做假锚定;
3. 账本不漂——分母密封,截断也记账;
4. 规模切开——不让「少看」成为唯一续命策略;
5. 管道收束——规则与工具面降低能省则省的方差。
设计自己的 Agent 系统时,我现在会带走两个问题:
这一步再跑一遍,必须完全一样吗? 这一步失败时,我们是暴露,还是圆谎?
必须一样的,钉子钉在代码里;允许变化的,才留给模型。 失败时优先暴露——假完成,才是漂移最体面的伪装。
少把「稳」寄托在更长的 Skill 上。那往往是漂移最肥沃的土壤。
适用 / 不适用
结论就一句: 反漂移靠的是选集 / 落点 / 账本三层硬契约,不是更长的 Skill,也不是更强的模型。
互动
你用通用 Agent 审 PR,最常踩的是 范围漂 / 落点漂 / 效果漂 哪一种?评论区丢一个场景,我可以按你的场景再拆。
觉得有用就 转发 给同组也在用 Cursor / Claude 审代码的同事。 欢迎关注「码畜生活」,后续还会写大 diff 怎么切开、以及委托模式到底稳了什么。
本文基于对开源项目 alibaba/open-code-review 公开源码与文档的阅读整理,非官方通稿;具体阈值与旗标以当前版本为准。