本文是「V2R:AI 迁移多智能体实战」系列第 3 篇。系列目录:
- 2026
- 07-05 V2R Studio 架构:一个不写代码的编排大脑
- 07-12 一个页面的迁移之旅:S0-S4 流水线
- 07-19 门禁不经过 LLM:确定性信任链(本篇)
上一篇跟着审批流列表页走完 S0 到 S4,本篇讲怎么记账、怎么判定、无法判定的地方怎么处理,迁移只是载体,这个问题属于所有用 AI 生成代码的系统:代码可以由模型写,判定不能由模型做。这条把产出和判定分开的方式,我把它叫确定性信任链。
AI 说「改完了」为什么不作数
LLM 交付一段代码时说的是「改完了,应该没问题」。这是一份声称,不是证据。这个没问题不能独立复核,换另一个 LLM 来评审,情况没有本质变化。判定要满足三个条件才算判定:
- 同样的输入给出同样的结论,可以复跑;
- 依据是第三方可核对的输入;
- 判定者和被判定者不同构,不会一起犯同一个错。 LLM 三条都不满足:采样有随机性,依据是自己的生成过程,评审模型和转码模型又是同一类系统,盲区高度重合。
之前有实际的教训。技术宅治理机制里有一个 auto-resolve,演示跑通用的是合成 fixture;真实流量 83 个 unknown 全部停在 pending,0 次 disposition。试点 0/12 verified 的情况下,里程碑出口四条标准逐条评了「达成」或「部分达成」。裁判和运动员都是模型的时候判定者会倾向于产出能让它通过的数据,这是自证机制的共同结局,与模型好坏无关。
所以铁律二是:门禁不经过 LLM。LLM 在系统里仍然干最重的活,调查、转码、修复全是它,但它只能看到门禁的结论,不能参与判定。判定环里流动的每一份输入都必须是可以独立获取的事实:文件、命令输出、浏览器采集结果。
覆盖率:每一行源代码都要有着落
判定要落地,先得有可判的指标。「代码写得好不好」不可判,「每一行源代码有处理」就是一个核心指标。金标准按这个思路定成两层:100% 源代码行有 disposition,这是记账完备性,确定性可判;高危行 100% 有结构对账或工单,这是正确性保障。前者保证没有行被悄悄忘掉,后者保证标了「已转」的高危行真的被核过。
账本也是两级的。文件级账本给闭包内每个文件记一个终态:ported(已落目标仓并过门禁)、lib-mapped(依赖库有对等映射)、adapted(有映射但需调整)、skipped(显式不迁)、blocked(无法自动处理,已上报)。每个终态必须带证据:ported 带目标文件清单和门禁快照引用,lib-mapped 和 adapted 带知识库条目 id,adapted 还要记下实际执行的转换,skipped 带理由加决策记录引用,blocked 带工单 id。没有证据的终态不存在,schema 层面就不允许。
行级账本是每个被转码的 Vue 文件一份 line-map:行区间按块记 disposition,ported、risk-high、risk-medium、dropped、skipped 五种,risk-high 的块必须挂 ticket_id。完备性的核对是机械的:所有行区间的并集必须等于文件全集。
这里有一个容易放过去的分工细节。行区间的骨架由 dep-analyzer 按 AST 锚点产出,CC 只往骨架里填 disposition 标签,填完由 dep-analyzer 核对完备性。如果让 LLM 自己画行区间,它就获得了把难办的行画出账本外的能力;自报骨架等于把账本边界交给被记账的一方。骨架、填标签、核对三个动作里,LLM 只占中间一格。
这套方案的边界也要说清。完备性是能直接体现的,正确性则不行:一行 $refs 调用被标成 ported、实际在目标侧丢了,靠覆盖率看不出来,它只知道这行有标签。标签的正确性由 G3 结构对账和高危行人工评审保证。这就是金标准定成两层的原因:只要求覆盖率,每行都会被贴上标签,无论标签是否为真。
账本怎么落盘也做了专门安排。CC 不直接写账本文件,更新走 cc-driver 注入的 MCP 工具,按文件事务性落盘:先写临时文件,fsync,再 rename。这套纪律和 session_id 落盘是同一条,目的是与节点生死解耦:进程被 kill、节点重跑,账本要么完成整个文件的更新、要么没有,不会出现写了一半的假账。崩溃最多丢当前文件的进度,不会出现「文件迁了一半、账上显示完成」的假绿。
高危行三选一:转换、降级、上报
普查阶段归过类的 1300 多处高危操作,深度样式穿透、$refs 调子组件方法、事件总线、直操 DOM、局部 mixin,到转码阶段逐个落进行级账本的 risk-high 块。每一块有三种合法结局:转换,有映射规则就执行规则,证据记进账本;降级,改成行为等价但形态不同的实现,理由留痕;上报,生成工单由人决策。不允许第四种结局:静默丢弃。
为什么把「不允许静默丢弃」写成铁律:静默丢弃是 AI 写代码最便宜的一种变绿方式。难办的功能删掉,门禁照样绿,页面看起来也大致正常。对策分两层。行区间并集核对让「这行不见了」可观测,每行必须有标签。dropped 是合法标签,但默认需人工批准;自动放行只限白名单,注释、空行、纯模板语法壳,而且白名单的判定要求区间内每一行都命中模式,防止一行注释放行整块代码。白名单随人工批准累积,等于人每批准一次,系统就多一条「这种行丢得起」的模式。
高危处置在修复循环里怎么运转,上一篇讲过,不再展开。只提一句和本篇相关的:同一个错误连续两轮逐字节相同立即升级人工,判重本身是机械字符串比较,和门禁遵守同一条铁律。
对账和 differ 必须是代码完成
覆盖率管「每行有标签」,G3 结构对账管「标签是真的」。做法是拿两份独立提取的事实比对:i18n key 全量对账、props 和 events 映射核验、ref 调用点对账、文案零拼接的全量正则判定。两个性质值得单独说。
第一是全量,不是抽查。抽查是概率判定,而迁移风险恰恰集中在长尾:只出现一次的 ref 调用、某个不常用组件的 event。抽查给出的是「大概没问题」,对账要给的是「逐条核过」。
第二是比对双方都不含 LLM 的结论:源侧调用点清单是 dep-analyzer 从 AST 提取的,目标侧是静态扫描的结果,对账代码只做集合运算。转码的 LLM 声称「所有 ref 调用都保留了」,这句话在整个判定里一次都没有被采信,被采信的是两份机器提取的清单。
G5 的 differ 同理。双端 e2e 里,基线是 CSI 驱动真实 Chrome 在 Vue 页面录的,回放是同一份 mock 走 React 页面,对比是确定性代码。判据细节上一篇讲过,这里只强调判定材料的来源:浏览器里真实发生的行为采集,不是任何模型的自述。
这几道门禁有个共同模式:判定的输入必须可以独立获取。文件、命令输出、浏览器采集,都是不看 LLM 脸色也能拿到的东西。LLM 在环里只有一个位置:接收判定结论和结构化 issue,作为下一轮修复的输入。
信任阶梯:五层各管一段,缺口如实显示
把各道判定按可信度和成本排起来,是一条五层阶梯:G2 静态、G3 结构对账、G4 覆盖、G5 双端 e2e、人工验收。
前三级是机器判定,可无人值守;G5 是半自动,录制需要我本机的登录态和真实 Chrome,上一篇说过这道闸永远不可能无人值守;人工验收是纯人工。层级越高,单页成本越高,覆盖的问题越接近「用户实际看到什么」。这条阶梯报告的不是「是否可信」的布尔值,是「信任到哪一层为止」。
缺口怎么处理,决定这条阶梯诚不诚实。人工验收这一层没有数据源,诚实的系统只有一个选择:如实显示「未覆盖」。仪表盘的设计里就定了一条:人工验收层显示为灰色缺口,不隐藏。缺口本身是信息,它说明这条链的信任到 G5 为止,往上靠人用眼睛补;补没补、补了几页,系统不知道,也不假装知道。
诚实的缺口比虚假的绿灯值钱。绿灯终止追问,缺口让人知道该去哪查。v2r-agent 的里程碑出口就是那种绿灯:四条标准逐条评了「达成」或「部分达成」,0/12 的事实沉在证据文件里没人看。
阶梯跑起来之后
这条信任链每跑一轮,就在 artifacts/ 里留下一批数据:文件级清单、行级 line-map、门禁快照、工单、token 用量、人工干预记录,十几种 JSON。判定是确定性的,数据就逐条可核对,这是最大的好处;问题是它们散在盘上,人要抓住重点只能逐个 cat。
后续有两件事。一是这些数据怎么沉淀成知识:每次人工裁决、每条高危处置,都应该回流成下次能自动命中的规则,而不是留在工单里睡大觉。二是怎么聚合成一张人能盯的盘:页面矩阵、风险、工单、成本,外加那道诚实的灰色缺口。这是下一篇的内容。
