本文是「V2R:AI 迁移多智能体实战」系列第 1 篇。系列目录:
- 2026
- 07-05 V2R Studio 架构:一个不写代码的编排大脑(本篇)
核心架构是:编排层由pi Agent实现只做编排、路由、标准化,所有文件读写由 Claude Code 进程执行。确保:留痕天然完整、门禁可以做成确定性代码、事实源只读可以机器强制。下面按这个顺序展开。
分层架构的原则
前三条原则是结构性的,直接决定系统长什么样。
- 「pi 不写业务代码」把系统切成两层:上面是编排大脑,决定每个节点派什么活、把工人的输出标准化落盘;下面是执行层,干活的是 N 个 Claude Code 长驻进程。v2r-agent 里编排层自己也读写文件,之前的方案中留痕要靠编排器自觉记录,漏记一处就是一个阻塞点。这次把文件读写全部交给Claude Code之后,每一次读写都发生在工人的会话里,而会话有 stream-json 协议的全量帧。留痕变为程序的一个必须执行的环节,不再需要模型参与。
- 「门禁不经过 LLM」。coverage、tsc、eslint、e2e 的判定全是确定性代码,LLM 只能看到门禁结论,不能参与判定。分层在这里起的作用很实际:编排层不碰业务代码,意味着门禁代码就是普通的 TypeScript 脚本,输入是文件和命令输出,输出是布尔值和结构化 issue,可以单测、可以复跑、可以逐行核对。判定逻辑一旦交给 LLM,同样的输入可能给出不同的结论,门禁就从「判定」退化成了「参考意见」。之前 v2r-agent 的 auto-resolve 存在 fixture 幻觉本质就是因为没有将这些流程固化到代码中。
- 「一切数据持久化」。分为两层,①外部会话可恢复,每个实际工作的Claude Code的 session_id、节点产出、审批决策全部落进
artifacts/<run-id>/,程序 crash 之后能从断点继续,具备更好的容错性。② 整个工作流是基于自研的 river-flow-core 实现的,所有的节点数据都会落盘,用户可以从任意一个节点恢复,Fork,这对状态的重置和验证有非常好的作用。
后三条原则(知识证据准入、能力滞后于负载、token 限制)更多落在数据和操作纪律上,后面的篇章展开。
四层:人、业务层、通用层、工人池
落成的结构是一个分层 monorepo,四层:
最上面是人。人和系统之间有两条通道:对话 CLI 用来过审批门、追问、随时下指令,工单文件用来批量处置普查出来的大批量 gap。两条通道写进同一个现场目录。
第二层 apps/v2r 是迁移业务层,整个仓库里唯一允许含 Vue/React 领域知识的地方。对话 REPL 和工单收割器在这里,S0 到 S4 的 workflow 定义也在这里,用 flow-core 的 API 写成普通代码。pi 作为编排大脑嵌在这一层,决定每个节点派什么活。
第三层 packages/ 是通用能力层,七个独立可发布的包:flow-core(可恢复工作流引擎,从 river 迁入并修了 resume 失效的 bug)、cc-driver(Claude Code 协议驱动)、dep-analyzer(依赖图核心)、knowledge(知识条目 schema 与证据准入)、coverage-ledger(文件/行级覆盖账本)、model-router(模型路由)、report-model(只读报表聚合)。通用性靠两条机器纪律看守:依赖方向 apps → packages 由 depcruise 强制;genericity lint 扫通用包的源码标识符和依赖清单,出现领域字样直接拦截。v2r-agent 的 analyzer 和 Vue 深度耦合,这次拆开,框架无关的依赖图进 dep-analyzer,SFC 解析和 vue2 方言归一作为语言适配器放在 apps/v2r 的 lang-vue2,经注册接口注入。
最下面是工人层:N 个 Claude Code 长驻进程,由 cc-driver 的进程池管理。再往下是六个仓库和 artifacts/ 现场目录,读写权限后面单讲。
调度链路概括
架构图是静态的,看一次调度怎么跑才能看清各层的分工。
pi 先决定当前节点分配什么工作,共分四种类型:调查、转码、规划、标准化。model-router 把类型映射到模型 profile:调查用 glm-5.3-flash,转码用 glm-5.3,复杂规划用 kimi-3,输出归一由确定性代码优先、flash 兜底。模型名经 argv 的 --model 传给Claude Code,端点和鉴权经 spawn 的环境变量注入,配置里只存环境变量名,token 本体不入库。
驱动方式选了 CLI + stream-json,没用 Agent SDK。这是另一套生产调度系统验证过的路径,协议自控,cc-driver 把整条协议包成了通用包。spawn 出来的工人是长驻子进程:claude -p --output-format stream-json --input-format stream-json,外加按角色配置的 --disallowedTools 和按需注入的 MCP 配置(账本事务写入这类结构化工具经 0600 权限的临时文件传入)。
协议层有几条原则,都在 cc-driver 的代码里。
- init 帧抓到 session_id 立即落盘(session-store 用 tmp 加 rename 原子写),与进程生死解耦。
- result 帧是一轮的终止信号,也是定稿文本的权威来源,帧里的 usage 累计进成本护栏。
- 多轮对话直接往 stdin 写下一帧 user 消息,热进程复用实测比每次冷启动快 1.6 到 2.4 倍;
- stdin 写入走独立异步任务,防止协议帧互相写死。
- 429 限速优先认结构化重试帧,指数退避。
- 看门狗盯单轮无事件超时。
- spawn 参数过一道黑名单,剥掉调用方夹带的协议 flag,模型只以 model-router 为准。
- 进程池以页面单元为 key,同单元串行、跨单元并行,上限默认 CPU 核数;
- 闲置 30 分钟确认 session_id 已落盘后杀进程。启动时还有版本探针做冒烟握手,协议对不上直接 fail-fast,不静默错解析。
崩溃恢复走两条分支。默认 --resume <id> --fork-session 继续,fork 是为了防僵尸进程没死透时同 id 冲突,新 id 同样落盘。resume 被拒(会话找不到、会话不属于当前账号,session-store 只认这两种明确文本,不把任意报错当会话丢失)就降级为新会话加上下文重注入。重注入的权威来源是 plan 加覆盖账本加目标仓的 git status,会话快照只作补充。因为会话内容是Claude Code写了一半的工作内容,账本和 git 是确定性的现况,CC拿着现况续写或重写,比拿着自己的回忆可靠。 同时会对已有的僵尸进程做 kill 处理。
会话恢复只是两层恢复里的下层。上层在 flow-core:workflow 每个节点的产出落 artifacts/<run-id>/nodes/,nodeId 由序号、节点名和参数哈希确定性拼出,恢复时已完成的节点真跳过;run 目录加带 PID 和心跳的 lockfile,防止 resume 进程和在跑实例并发写坏现场。两层各管各的:flow-core 保证节点不重复执行,cc-driver 保证节点内部那次派活的上下文接得上。
事实源只读的三道强制验证
系统的输入面是六个仓库,五个只读、一个可写:源仓 shop-w(Vue 2.7)、老组件库 A(mc-* 组件)、老组件库 B、新设计体系 nova-design、基建仓 foundation-web 是只读事实源;目标仓 shop-web(React 18)是唯一落码处。
「只读」不是靠 prompt 里写一句请勿修改,有三道机器强制。
- 调查、普查类工人 spawn 时用
--disallowedTools禁掉 Write、Edit、NotebookEdit,这一类CC连写工具都拿不到;所有CC统一禁掉 AskUserQuestion,防止工人绕过审批门直接问人。 - 转码工人的 cwd 设在目标仓,源仓内容只能经读取进入 prompt,写操作的默认落点天然在可写仓里。
- 每个 run 结束跑确定性审计:对五个事实源逐个执行
git status --porcelain,输出必须全空,这道审计是 G2 静态门禁的前置项,不干净就不许往下走。审计代码在 apps/v2r 的门禁模块里,只发 status 这一种读命令,失败时带上每个仓的首行明细。
单纯的 prompt 管不住Agent概率的 Edit,而权限表、cwd 隔离和 git 审计限制的住。
接新项目等于加一个 profile
通用性最后落在接入成本上:新仓库接入等于在 projects/<name>/ 下新增一个 profile,不改代码。
# projects/<name>/profile.yaml
source:
repo: <源仓库路径>
pages_dir: src/pages
entry_glob: src/.ob/entries/**/*.js
target:
repo: <目标仓库路径>
page_file: index.tsx
fact_sources:
- <源仓库>
- <老组件库 A>
- <老组件库 B>
- <新设计体系组件库>
- <基建仓>
stack: vue2→react-novaprofile 声明源仓和目标仓的位置、entry 的提取口径、事实源清单和所属技术栈。知识库按栈索引,同栈的新项目直接继承已有的映射条目,接入的边际成本主要剩普查和补洞。迁移本身按需进行:manifest 是页面集合,可以先迁两个页面再迁下一批,每页独立 run、独立门禁、独立交付。项目特定信息只允许出现在 projects/ 数据和知识库里,genericity lint 保证它渗不进通用包的代码。
架构之后的下一个问题
到这里,架构回答了三个问题:谁干活(CC 进程池),谁判定(确定性门禁,LLM 只看结论),证据放哪(artifacts/ 现场加覆盖账本)。编排层不碰业务代码,换来的是这三件事各自都能被机器强制,系统的可信度来自结构,来自权限表和审计脚本。
下一个问题是:一个页面具体怎么走完这套系统?S0 到 S4 五个阶段各干什么、五道门禁分别卡在哪、修复循环怎么收敛。下一篇跟着一个审批流列表页走完全程。
