lili fc0c164678 docs(architecture): fan-out 重写架构/前端/运营/后端/运维 5 域(20 档 · 全 opus)
Workflow 每目标档一个 opus agent,以产品域 README 为风格样板:架构 L2 主档 + 13模块 + 生成引擎旗舰子树(11 档:README/固定游戏架构/SAA编排/引擎与运行时/验收门-W-G1/prompt治理/设计合理性裁决/OpenGame对照/WG1基准/SAA能力API-dossier/开闸接线)+ 前端 + 运营(README/变现与单位经济/渠道发行/合规闸门)+ 后端瘦主档 + 运维瘦主档。全部:人读散文 + 多图(Mermaid)+ 少黑话(专名首现解释)+ 单档 ≤2000(最大 452)+ 品牌绘境AI + 硬事实保留(门ID/数字/契约/裁决)。源档归档、§10 治理反转、~17 连带引用、L1 总索引、_index/AGENTS 对账、死链门统一留 Phase7。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:43:41 -07:00

23 KiB
Raw Blame History

SAA 能力 / API / 接入 · 逐键源码证据底座

这是什么:绘境AI 生成引擎采用 SAA(Spring AI Alibaba,阿里在 Spring 生态里的 AI 编排框架)作为编排基建——这份文档是那一决策背后的逐键源码证据底座。它不重复"为什么这样分层、有哪些不能破的不变量"那套架构论证(那在SAA 编排),而是回答更下沉的一层:照着写代码时,该调哪些 API、本期到底哪些能力能用哪些不能、引进 game-cloud 会撞哪些依赖、按什么范式落节点、最容易踩哪几个坑——每一条都钉在 SAA 某个类的某一行源码上。 给谁看:真正动手把 SAA 图落进 game-module-aigc-server 的后端工程师、做依赖冲突评审的人、复核"这条结论有没有源码支撑"的架构评审。 怎么读:这是一份技术参考册,不必线性通读。先扫第 0 节那五条决策级结论建立锚点,然后按需跳——要照着写代码翻第 1 节 API 速查,要判断"这个能力本期能不能用"翻第 2 节能力矩阵,要落 Maven / 解依赖冲突翻第 3 节,要照范式骨架写节点翻第 4 节,动手前务必先过一遍第 5 节那六个坑

这份文档在生成引擎子树里的定位,是SAA 编排那篇架构文档的证据附录。编排文档讲"这台编排机器长什么样、为什么这么搭";本文档讲"那篇文档里每一条工程结论,在 SAA 源码里能不能落地、落在哪一行"。两者一上一下:看设计读编排文档,照代码写实现、或要复核某条结论的源码出处,读本文档。


调查方法与基线(先讲清证据从哪来)

本文档的全部结论,来自一轮多路独立源码取证:四个 Claude / Opus 子代理(子代理 = 并行派出去独立干活的 AI 调查员)分别从四个视角逐行读 SAA 源码——API 面、能力盘点、接入部署、用法范式——再彼此交叉验证,结论一致。原计划还有 Codex(另一个模型)跑第 5 路,在 51 分钟时卡死被取消;因为前四路已互相印证,这不影响任何结论。

所有取证都钉在同一个代码基线上,引用结论时请认准这个版本:

  • 被调查代码库:/root/oss/spring-ai-alibaba,checkout 到 tag v1.1.2.2(HEAD commit 7405a7d)——这是 GA 正式版(GA = General Availability,正式发布版,区别于预览 / SNAPSHOT 快照版),不是预览版。
  • 底层依赖:SAA v1.1.2.2 之下压着 spring-ai 1.1.2(Spring 官方的 AI 抽象层,SAA 在它之上做编排扩展)。
  • 关联文档:本调查服务于编号 HJ-AGI-002 的那条决策(用 SAA 裸 StateGraph 做确定性编排;裁定与不变量见SAA 编排);spike(spike = 一次性的技术验证小实验)执行稿与目标架构 review 在 docs/agent-specs/ 留痕层。

一个口径要先点明:本文档里凡出现具体类名 / 方法名 / 文件行号,都是真实读到的源码事实,不是设计设想。带"⚠️"标记的是框架已知缺陷或反直觉行为——这些正是最容易让人想当然写错的地方,务必当心。


0. 五条决策级结论(锚点)

下面五条是整轮调查蒸馏出的最高层结论。它们既是后续各节的总纲,也是落地时反复要回看的定盘星。

第一,我们的生成流水是确定性多步编排,因此用「裸 StateGraph + 自定义 NodeAction」,不用 agent 自治那套。 StateGraph 是 SAA 提供的"有向状态图"原语——你用它声明一个个节点和节点之间的条件边;NodeAction 是每个节点里那段实打实的业务代码(你自己实现的一个接口)。"裸着用"指我们直接拿这层图原语手写编排,而ReactAgent / flow agent——那一类是给"LLM 自己挑工具、自由决定下一步"的自治场景准备的,与我们"步骤预先固定"的形态不匹配。生成里的 repair(修复重试)环,靠 addConditionalEdges(添加条件边)画一条回边,再配一个放在状态里的计数器来控制轮次,不用 SAA 的 LoopAgent

第二,模型这一层 SAA 没有自己的封装,直接复用上游 Spring AI 的 OpenAiChatModel / OpenAiApi 也就是说 SAA 圈里凡调 OpenAI 兼容接口的,都是 Spring AI 原生那两个类。具体接法是:baseUrl 指向 new-api(new-api = 绘境AI 内部统一的模型成本 / 凭证网关,负责 API key 管理与用量计费),每个图节点配一个 OpenAiChatModel bean,这些 bean 共用同一个 OpenAiApi 实例、仅 defaultOptions.model(默认用哪个模型)不同,节点用 @Qualifier 注解取到对应那个。便宜模型(如 DeepSeek)结构完全一样,还有官方原生 starter(starter = Spring Boot 的一键依赖包)可用。

第三,状态持久化用 MysqlSaver 作引擎唯一权威 saver,Redis 只作业务旁路缓存。 saver(保存器)就是把图执行到一半的状态存盘、以便续跑的组件;MysqlSaver 直接收一个 DataSource(数据源),可以把项目现成的 yudao Druid 连接池注进去(yudao 是 game-cloud 后端所基于的那套脚手架,Druid 是它用的数据库连接池)。配合 RunnableConfig.threadId(任务id)(把每个生成任务的 id 当线程标识),就能精确续跑。关键约束:一张图只能注册一个 saver,注册两个会直接抛异常——所以 MySQL 当权威、Redis 当旁路缓存,而不是两个都注册成 saver。另外必须设 releaseThread(false),图才能长期续跑而不被框架回收线程。

第四,把 SAA 引进 game-cloud 的依赖冲突风险是「低到中、可控」。 只引 graph-core(图引擎核心)加三个 BOM(BOM = Bill of Materials,依赖版本管理清单,统一锁定一组相关依赖的版本);绝不引 SAA 自带的 Boot BOM(那会让项目自己的 Spring Boot 3.5.14 输给 SAA 内置的 3.5.8);只要不用 RedisSaver,就避开了 Redisson(一个 Redis 的 Java 客户端)3.x 与 4.x 的版本冲突。其余依赖双方完全一致:fastjson、okhttp 版本都对齐;而且 graph-core 不引入任何 web 框架(只依赖 reactor-core),不会和 yudao 的 spring-mvc 抢运行栈。

第五,运行形态是「进程内嵌 + 后台线程池跑图」,而不是独立 worker。 我们抽一个 GenerationDispatcher(生成派发器)接口,挂在现有 AigcTaskService.submitGenerate → completeWithVersion 这条契约后面;它有两个实现——HttpWorkerDispatcher(走外部 HTTP worker)和 SaaGraphDispatcher(进程内跑 SAA 图),灰度切换、框架可换。图用 SAA 自带的阻塞式 invoke()后台线程池里跑(禁止占用 Tomcat 请求线程或 Reactor 事件循环线程),而且图的执行过程不包大事务。独立 worker 进程那条路留到 Phase 2(第二阶段)再说。


1. 关键 API(照着写代码那一层)

这一节是"打开 IDE 就能照抄"的 API 速查。按图怎么搭、状态怎么读写、怎么执行、模型怎么配、怎么持久化、怎么流式输出六块组织。每一行括号里的注解,是这个 API 在我们场景下的用法要点或坑。

搭图(StateGraph 的骨架 API)

  • new StateGraph(name, KeyStrategyFactory) —— 新建一张图;KeyStrategyFactory(键策略工厂)声明状态里每个 key 在合并时用什么策略,见下一块。
  • addNode(id, node_async(NodeAction)) —— 加一个节点;node_async(...) 是把你写的 NodeAction 包装成框架内部的异步节点。
  • addEdge(START, ..) / addEdge(.., END) —— 加一条无条件边;START / END 是框架预定义的起止虚拟节点。
  • addConditionalEdges(from, edge_async(EdgeAction→String), Map<key,目标节点>) —— 加条件边:EdgeAction 返回一个字符串 key,框架按 Map 把这个 key 映射到下一个节点。生成里的 repair 回边就靠它;⚠️ 那个 Map(mappings)不能为空,空了会报错。
  • compile(CompileConfig) → CompiledGraph —— 把声明好的图编译成可执行的 CompiledGraph;CompileConfig 里挂 saver、中断点等编译期配置。

状态读写契约(最反直觉、最容易写错的一块)

节点里读写状态不是用 setter / getter,而是:

  • :在 NodeAction.apply(OverAllState) 方法里,用 state.value("k", T.class) 按 key 取值(OverAllState 是贯穿整张图的那个全局状态对象)。
  • :靠 return Map.of("k", v) —— 你返回一个 Map,框架按每个 key 配置的 KeyStrategy(键策略)去合并进全局状态,而不是你直接改对象。ReplaceStrategy = 覆盖旧值,AppendStrategy = 追加(比如把新消息追加进 messages 列表)。
  • ⚠️ 记住这条:写状态 = 返回 Map,不是调 set。习惯了命令式写法的人在这里几乎必踩。

执行图

  • invoke(inputs, RunnableConfig) → Optional<OverAllState> —— 同步阻塞执行,跑完返回最终状态。它内部其实就是 stream().last().block(),自带阻塞——这正是第 3 节要强调"必须丢后台线程池跑"的原因。
  • stream(...) → Flux<NodeOutput> —— 流式执行,返回一个 Flux(reactor 的响应式流),逐个吐出节点输出。

模型(上游 Spring AI 三件套,照此装配)

OpenAiApi.builder()
    .baseUrl(NEW_API)          // 指向 new-api 网关
    .apiKey(KEY)
    .build();

OpenAiChatModel.builder()
    .openAiApi(api)            // 共用同一个 OpenAiApi
    .defaultOptions(
        OpenAiChatOptions.builder()
            .model("deepseek-chat")   // 各节点仅此处不同
            .build())
    .build();

持久化(saver 的装配链)

  • MysqlSaver.builder().dataSource(ds).createOption(CREATE_IF_NOT_EXISTS).build() —— MySQL 保存器,CREATE_IF_NOT_EXISTS 表示表不存在就自动建。
  • RedisSaver.builder().redisson(redissonClient).build() —— Redis 保存器(⚠️ 引它就拖进 Redisson,见第 3 节冲突 C2;我们不用它当 saver)。
  • 装配链:SaverConfig.builder().register(saver).build()CompileConfig.builder().saverConfig(..).releaseThread(false)RunnableConfig.builder().threadId("..")releaseThread(false) 是长期续跑的开关,threadId 是续跑的定位键。

流式输出(往前端推 token 增量)

  • stream() → Flux<NodeOutput> —— 拿到节点输出流。
  • instanceof StreamingOutput 区分哪个输出是 LLM 吐出的 token chunk(增量片段),再 .message().getText() 取这一段增量文本。
  • ⚠️ SAA 没有 streamEvents 这个 API(有些框架有,SAA 没有)——只能走 stream() + instanceof 这条路判类型,别去找 streamEvents

2. 能力盘点:本期(只有 MySQL + Redis)能用 vs 要等 Phase 2

这一节回答一个很实际的问题:在只部署了 MySQL + Redis 的现状下,SAA 的哪些能力当下就能用,哪些必须等更多基建到位? 误判这条边界,要么白白等基建、要么起了个根本起不来的重组件。下面这张图先给全景,再逐条列。

flowchart TB
  subgraph NOW["✅ 本期即可落 — 纯 JVM 或仅需 MySQL+Redis"]
    direction TB
    C1["图编排全集<br/>条件 / 并行 / 嵌套子图 / 循环"]
    C2["agentic ReAct + 工具调用"]
    C3["多 agent<br/>sequential / parallel / routing / conditional / loop"]
    C4["持久化 checkpoint / resume<br/>MysqlSaver(仅 MySQL) · RedisSaver(仅 Redis)<br/>均已验证零 Nacos / 零 MQ import"]
    C5["流式 · HITL 中断恢复"]
    C6["Hook ×10 / 拦截器 ×13"]
    C7["OpenAI 兼容(new-api) + DeepSeek"]
    C8["A2A 点对点 · 可观测埋点"]
  end
  subgraph LATER["⛔ 需 Phase 2 基建"]
    direction TB
    P1["完整 Admin 平台<br/>MySQL+Redis+ES+RocketMQ+Nacos 五件套全硬<br/>缺一启动失败"]
    P2["A2A-Nacos 服务发现 · config-nacos · MCP-Nacos 网关"]
    P3["sandbox(Docker 容器隔离)"]
    P4["RAG 走 ES · Prompt 线上热更(写 Nacos)"]
  end
  style NOW fill:#eafaf1
  style LATER fill:#fdedec

本期即可落(纯 JVM,或仅依赖 MySQL + Redis,覆盖我们的核心用例):

图编排全集(条件分支 / 并行 / 嵌套子图 / 循环都有);agentic ReAct(ReAct = Reason+Act,让模型"边推理边调工具"的经典范式);工具调用;多 agent 的全部组合(sequential 顺序 / parallel 并行 / routing 路由 / conditional 条件 / loop 循环);持久化的 checkpoint(检查点存盘)与 resume(续跑)——MysqlSaver 只需要 MySQL、RedisSaver 只需要 Redis,二者都已逐行验证零 Nacos、零 MQ(消息队列)的 import(Nacos = 服务注册与配置中心,MQ = 消息中间件);流式输出;HITL(Human-In-The-Loop,人工介入环节)的中断与恢复;Hook(钩子)10 个、拦截器 13 个;OpenAI 兼容接口(经 new-api)加 DeepSeek;A2A(Agent-to-Agent,智能体间通信)的点对点形态;可观测埋点。

需要 Phase 2 基建(现状起不来):

完整的 Admin 平台——它把 MySQL + Redis + ES(Elasticsearch,搜索 / 向量库)+ RocketMQ(消息队列)+ Nacos 五件套全部硬依赖,缺一个就启动失败;A2A 的 Nacos 服务发现;config-nacos(配置走 Nacos);MCP-Nacos 网关(MCP = Model Context Protocol,模型上下文协议,给模型挂外部工具 / 数据的标准接口);sandbox(基于 Docker 的容器沙箱隔离);RAG(检索增强生成)走 ES;以及 Prompt 的线上热更新(它要写 Nacos)。

两条最容易误判的边界,单独拎出来强调:

  1. 「Admin 整包」≠「有 MySQL + Redis 就够」。 Admin 是个重组件,要五件套。如果只是想用编排引擎,千万别起 admin,直接用 graph-core + agent-framework 的库依赖即可——这是本期能落地的关键前提。
  2. 向量库在这份 clone 里只有 ES 一个具体实现。 想做 RAG / 向量检索,本期实质上被 ES 这一依赖绑住,属于 Phase 2。

3. 接入 game-cloud(依赖冲突解法放最前)

这一节给真正动手把 SAA 引进后端的人。最该先解决的是依赖冲突——所以把它放在最前。整轮调查的结论是:真正的冲突只有两处,其余全是零冲突

3.1 两处真冲突及解法

flowchart LR
  subgraph C1["C1 · Spring Boot 版本"]
    direction TB
    C1a["项目:3.5.14<br/>(huijing-dependencies BOM 实际生效)"]
    C1b["SAA 要:3.5.8"]
    C1c["✅ 解:不引 SAA 的 Boot BOM<br/>让 3.5.14 胜<br/>(同 minor 补丁前向兼容)"]
    C1a --- C1b --- C1c
  end
  subgraph C2["C2 · Redisson 版本"]
    direction TB
    C2a["项目:4.4.0"]
    C2b["SAA graph-core:3.22.0<br/>(但 optional=true)"]
    C2c["✅ 解:不用 RedisSaver<br/>→ 零冲突"]
    C2a --- C2b --- C2c
  end
  • C1 — Spring Boot 版本:项目实际生效的是 3.5.14(由 huijing-dependencies 这个 BOM 锁定),SAA 想要 3.5.8解法:不引 SAA 自带的 Boot BOM,让项目的 3.5.14 胜出。 二者是同一个 minor 版本(3.5.x)下的补丁差异,前向兼容——jackson / reactor / spring 都只差补丁号,实测安全。
  • C2 — Redisson 版本:项目用 4.4.0,SAA 的 graph-core 里写的是 3.22.0——但它标了 optional=true(可选依赖,不主动传递)。解法:只要不用 RedisSaver,就零冲突。 如果将来非用不可,需要实测 Redisson 4.x 对 RMap / RBucket / RLock 这三个 API 与 3.22.0 的兼容性。
  • 零冲突的部分:fastjson(1.2.83)、okhttp(4.12.0)双方版本完全一致;graph-core 不引任何 web 框架,不会与 yudao 的 spring-mvc 抢栈。

3.2 Maven 怎么加(只动一个 pom)

所有依赖只加到 game-module-aigc-server/pom.xml,不动根 POM / 根 BOM:

  • import 三个 BOM:spring-ai-bom:1.1.2 + spring-ai-alibaba-bom:1.1.2.2 + spring-ai-alibaba-extensions-bom:1.1.2.2
  • 依赖只加 spring-ai-alibaba-graph-core(图引擎,唯一必需的)+ 一个模型 starter(走 new-api 就用 spring-ai-openai)。

3.3 reactive↔blocking 桥接(防死锁的命门)

CompiledGraph.invoke() 自带阻塞(内部调了 .block()),所以必须在后台 ExecutorService(线程池)里调它,绝不能在 Web 线程或 Reactor 线程上调——否则就是经典的"在 reactor 线程里 block"死锁。超时控制用 Future.get(timeout) 从外面包一层;取消接现有的 cancelTask 逻辑。图的执行不要包在大 @Transactional 事务里,跑完之后单独开一个短事务落库即可。

3.4 最小验证门 E:6 步,在 mini-desktop 上跑

门 E(门 = gate,一道必须通过才算数的验收检查;mini-desktop 是内网那台用来跑验证的桌面机)的设计哲学是先验最便宜的那个命题——"SAA 这套库到底能不能干净地进 game-cloud 这个进程",而把模型、saver、job 链全部隔离在外、暂不接入。六步如下:

  1. 加三个 BOM + graph-core
  2. mvn -pl …aigc-server -am dependency:tree 并断言:Spring Boot 全是 3.5.14、Redisson 是 4.4.0、没有 3.22.0 泄漏进来、jackson 是 2.21.x。
  3. 写一个纯 Java 节点(不调任何 LLM)的 "hello" StateGraph
  4. mvn package 过编译门。
  5. 单体启动门:验证 SAA 与 yudao 的自动配置能共存,没有 BeanCreation 失败。
  6. invoke 跑那一个节点,断言返回了预期 state。

门 E 的精髓:这一步先不接模型、不接 MysqlSaver、不接 job 链——只隔离验证"SAA 库能进 game-cloud 进程"这个最便宜、最该先确认的命题,把昂贵的集成验证留到后面。


4. 面向我们生成流水的用法范式(7 条)

这一节是"该怎么用 SAA 把我们的生成流水搭出来"的范式手册——七条,每条都对应生成主线的一个实际需求。完整的节点骨架代码在各子代理报告里,这里给的是范式要点与对应 API。

  1. 主干 = StateGraph + 每步一个 node_async(NodeAction) 生成主线按 render → generate → validate → scaffold → build → play → emit 一步步串(解析意图 → 生成 → 校验 → 脚手架 → 构建 → 真玩 → 发布),每一步是图里一个节点。

  2. repair 环 = 条件回边 + 状态计数器。 写法:addConditionalEdges("validate", edge_async(s → ok ? "done" : cnt >= N ? "giveup" : "repair"), …)——校验通过就走 done,重试超过 N 次就 giveup,否则回到 repair。轮次靠状态里的 repair_count 计数器控制。另有一个 recursionLimit(递归上限,默认 100)作硬刹车——注意它触发时是优雅终止、不是抛异常,而且它是兜底,不替代业务自己的 max-iters(最大迭代次数)上限。

  3. build / play 节点 = 自定义 NodeAction 里用 ProcessBuilder 调子进程。 build 步在节点里 new ProcessBuilder(...) 起一个 shell 调 node / esbuild(esbuild = 一个极快的 JS 打包工具);play 步同样起子进程调 CDP 九门 harness(CDP = Chrome DevTools Protocol,Chrome 的调试协议,可用程序操控真实浏览器;"九门 harness" = 一套在真浏览器里跑九道确定性验收检测的测试夹具,详见验收门 W-G1)。用 mapper.readTree 收子进程吐回的 JSON,用 waitFor(timeout) / destroyForcibly 控超时与强杀。⚠️ 别用 SAA 的 sandbox 模块——那是给 AgentScope 远程容器用的,与我们本地子进程形态不符;节点骨架可以照抄 LocalFilesystemBackend.java 的第 457507 行。

  4. 进度推前端 = 节点返回的 Map 里塞一个 Flux 节点 apply 返回的 Map 里放一个 Flux,框架会自动在 stream() 上把它展开;出口侧用 @PostMapping(produces = TEXT_EVENT_STREAM_VALUE) Flux<ServerSentEvent<String>> 以 SSE(Server-Sent Events,服务器推送事件)往前端吐进度。

  5. checkpoint = MySQL 单权威 saver + releaseThread(false) Redis 作业务旁路;同一个 threadId 第二次 invoke 即自动续跑;要做 time-travel(回到历史某个状态点)用 getStateHistorycheckPointId

  6. 可选 HITL = 节点实现 InterruptableActioninterrupt() 里按状态里的 review_required 开关决定停下等人、还是放行(条件式,默认自动放行);恢复时用 updateState(...) + withResume() + stream(null, cfg)⚠️ HumanNode 已废弃,而且 HITL 必须配 saver 才能用(没有 saver 中断后没法续)。

  7. 模型 = N 个 OpenAiChatModel bean 全指 new-api、仅 model 不同,节点 @Qualifier 取。 ⚠️ 一个反直觉点:AGENT_MODEL_NAME 不是"选哪个模型"的机制——它只是节点 ID 加流式事件的前缀,别误以为改它能换模型。


5. 六个最容易踩的坑(四路交叉验证确认)

这一节是整份文档里最该先读的部分。下面六个坑,每一个都是框架的已知缺陷或反直觉行为,而且都经四路独立取证交叉确认。动手前过一遍,能省掉绝大多数返工。

# 坑(框架的反直觉 / 缺陷) 正确做法
1 HumanNode 整个文件被注释掉、不可用 —— 想当然去 new 一个 HumanNode 会找不到。 CompileConfig.interruptBefore / interruptAfter 设中断点 + resume() 恢复。
2 没有 streamEvents 这个 API —— 别去找。 stream() → Flux<NodeOutput> + instanceof StreamingOutput 判类型取增量。
3 ReAct 循环默认无上限(上限是 Integer.MAX_VALUE,等于没上限)—— 用 ReactAgent 时不挂限制会无限烧。 ReactAgent 时生产环境必须ModelCallLimitHook / ToolCallLimitHook;我们走裸 StateGraph,则靠状态计数器 + recursionLimit 兜底。
4 supervisor / handoff 没真正实现(只有枚举占位,不能用)—— 以为能直接用多 agent 的 supervisor 模式会落空。 用 routing(路由)模式替代。
5 一张图只能注册一个 saver(注册第二个直接抛 IllegalStateException)。 MySQL 当权威 saver + Redis 当业务旁路缓存;真要双写就自己写一个双写装饰器,而不是注册两个 saver。
6 Admin 整包硬依赖五件套(MySQL + Redis + ES + RocketMQ + Nacos,缺一启动失败)—— 只想用引擎却起了 admin 会卡在启动。 只要引擎就别起 admin,直接用 graph-core + agent-framework 的库依赖。

6. 深度参考

本文档是决策级蒸馏——把四路源码取证的结论收成一份可照着落地的参考册。再往下两个方向的细节,通过指针跳转,不在此展开:

主题 位置
全量逐行证据(每个类的 文件:行号,含 API 速查 / 能力矩阵 / 接入部署 / 用法范式四份子代理报告原文) 本轮四个子代理报告(docs/agent-specs/ 留痕层)
上层架构论证(为什么是 SAA-only、16 节点编排拓扑、六条不变量、split-brain 防线、与旧编排器边界) SAA 编排
落地坑清单的扩充版(Semaphore(1) 串行守端口 4320 / 9222、checkpoint saved_at 无 tiebreaker 的显式 checkPointId 修法等) .agents/skills/saa-graph-orchestration.md
这套编排在生成主线里生成什么、生成产物长什么样 固定游戏架构 · 生成引擎主文档

本文档定位:这是生成引擎子树里的技术参考底座——SAA 的 API / 能力 / 接入 / 坑,逐键钉在 v1.1.2.2(HEAD 7405a7d)源码上。它是SAA 编排那篇架构文档的证据附录:看设计读编排篇,照代码写实现、或复核某条结论的源码出处读本篇。承重事实(基线版本、三 BOM + graph-core、C1/C2 两处冲突、门 E 六步、六个坑、MysqlSaver 单权威 + releaseThread(false)AGENT_MODEL_NAME 非选模型机制等)均为四路独立取证交叉验证的源码事实;品牌统一为"绘境AI"。