扩 saa-graph-orchestration「真·全图 e2e 测成功率」章一条:SaaFullGraphE2eTest 003-U1 工具化 (n≥10/外部 brief 文件、minSuccessRate 可配硬门默认不设、token 成本会计软警告、模型无关单测), 并记关键坑「SAA 路无 ¥ 数据(SaaGraphDispatcher cost 整段省略,折¥=F3)→ 别接¥门、别造假¥」。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
SAA 图编排 playbook(生成流水 · HJ-AGI-002)
定位:用 Spring AI Alibaba(SAA v1.1.2.2)裸
StateGraph在 game-cloud(Java/Boot 3.5.14)里搭"确定性多步生成编排"的标准配方。现行 agentic 基建(HJ-AGI-002,short-term SAA-only)。 何时用:把"渲染→设计→生成代码→[校验/构建/真玩 九门]→修复回环→玩家顾问→出包"这类确定性多步流程落成图。不用 ReactAgent/asNode/subAgents(那是"LLM+工具自治",与"done 由九门确定性门定、不让 LLM 自评"冲突;自治节点延 Phase1.5,见 [saa-agentic-infra-decision记忆] /docs/agent-specs/agentic编排-SAA.md §10)。 参考:能力/API 速查docs/agent-specs/2026-06-15-SAA-能力API接入-dossier.md;迁移设计(9 步计划/拓扑/坑)docs/agent-specs/_archive/2026-06-15-python-to-SAA-migration-design.md;落地实现见game-module-aigc/.../saa/SaaStudioGraph.java(唯一布线源)/SaaGraphDispatcher.java(生产派发)。
1. 依赖集(最小证成集,别贪"完整推荐集")
game-module-aigc-server/pom.xml 引 3 个 BOM(仅管版本,不引 spring-boot-dependencies) + 3 个直接依赖:
<!-- BOM:spring-ai-bom:1.1.2 / spring-ai-alibaba-bom:1.1.2.2 / spring-ai-alibaba-extensions-bom:1.1.2.2 -->
<dependency><groupId>com.alibaba.cloud.ai</groupId><artifactId>spring-ai-alibaba-graph-core</artifactId></dependency> <!-- 图引擎+MysqlSaver+observation 机芯 全在此 -->
<dependency><groupId>com.alibaba.cloud.ai</groupId><artifactId>spring-ai-alibaba-starter-graph-observation</artifactId></dependency> <!-- 仅 Boot 自动配置层 -->
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-openai</artifactId></dependency> <!-- OpenAiApi/OpenAiChatModel,接 new-api -->
别引:agent-framework(ReactAgent/asNode,Phase1.5 才要)、builtin-nodes(节点全自写 NodeAction,且它脱 BOM 硬钉 1.1.2.2 拖 tika 全家桶=版本钉债)、SAA 的 Boot BOM(让项目 3.5.14 胜,同 minor 补丁前向兼容)、RedisSaver(避 Redisson 3.22↔4.4 冲突)。
铁律:动 pom 删依赖前,先 dependency:tree/读 jar 核类的模块归属——MysqlSaver、GraphObservationLifecycleListener 都在 graph-core,删 agent-framework/builtin-nodes 不影响它们。
2. 拓扑(声明式条件边,非硬编码 while)
SaaStudioGraph.assemble() 是唯一布线源(生产派发器 + 回归测试共用,杜绝两处漂移)。节点 11 个、条件边 4 处:
START→render→design→generate→validate ─┬(ok)→ scaffold→build ─┬(ok)→ play ─┬(ok)→ player ─┬(ok)→ emit→END
├(repair)→ repair ──────┘(repair)────┘(repair)──────┘(repair)→ generate(回环)
└(giveup)→ giveup→END
- 回环/分支用
addConditionalEdges("validate"|"build"|"play"|"player", router, mapping);router 是EdgeAction返回ok|repair|giveup|emit等字符串键。 - 超步刹车:
CompileConfig.builder().recursionLimit(N)(repair 成环时优雅终止)+ state 里的 repair 计数器,不靠 LoopAgent。 - 节点 =
NodeActionlambda 经node_async()注册;状态OverAllState+KeyStrategyFactory(多数键用ReplaceStrategy)。
加一个节点:写 NodeAction(读 state→干活→Map.of(key,val) 写回)→ graph.addNode("x", node_async(xNode)) → addEdge/addConditionalEdges 接进拓扑 → 若新键,在 KeyStrategyFactory 注册其合并策略。
3. 模型接 new-api(OpenAI 兼容)
五角色(design/code/fix/player_text/player_vision)共用一个 OpenAiApi,仅 defaultOptions.model/temperature 异:
OpenAiApi api = OpenAiApi.builder().baseUrl(stripV1Suffix(newApiBase)).apiKey(key).build();
OpenAiChatModel m = OpenAiChatModel.builder().openAiApi(api).defaultOptions(opts).build();
- ⚠️ baseUrl 坑:Spring AI 自拼
completionsPath=/v1/chat/completions,故baseUrl必须是 host 根、不带/v1(http://100.64.0.8:3000);同一.env(NEWAPI_BASE_URL=.../v1)两侧通用须 Java 侧stripV1Suffix剥之,否则 404/v1/v1/...。 - 视觉位(M3)走多模态
UserMessage.builder().media(Media(IMAGE_PNG, ...))。成本:全走 new-api 单一 key→自动入newapi_cost(logs.quota) 计费平面,不内联折账。
4. checkpoint(崩溃续跑 · 迁移 step5)
MysqlSaver saver = MysqlSaver.builder().dataSource(yudaoDruid).createOption(CreateOption.CREATE_IF_NOT_EXISTS).build();
SaverConfig sc = SaverConfig.builder().register(saver).build();
// compile: CompileConfig.builder().saverConfig(sc).releaseThread(false)... run: RunnableConfig.builder().threadId(traceId)
- 一图一 saver(注册俩抛异常);
releaseThread(false)才能长期续跑;DataSource 直接收 yudaoDynamicRoutingDataSource(primary=master)。 - Flyway 共存:表
GRAPH_CHECKPOINT/GRAPH_THREAD(大写引擎私有 DDL,非 Flyway 管)与 yudao 迁移零撞名;测试隔离saa_spikeschema,生产ruoyi-vue-pro零污染。生产懒建(仅 dispatcher=saa 且 DataSource 在席时首个 dispatch 触 DDL;http 态零碰 DB)。 - 🔴 必踩的框架坑(实证):
MysqlSaver.saved_at是秒级 TIMESTAMP,load checkpoint 的 SQL 仅ORDER BY saved_at DESC无 tiebreaker。一次 interrupt 在同秒落多条 checkpoint(__START__→n1与n1→n2)→ 默认resume()(取"最新")非确定地选其一→续跑点漂移(节点可能重放)。修法(不改框架):用getStateHistory按内容(StateSnapshot.node()=="n1")确定性选中断点、取其稳定config().checkPointId(),用显式 checkPointId 续跑(走 saver by-id 精确分支,绕开 saved_at 排序)。验证见SaaCheckpointResumeTest(真恢复:查库GRAPH_CHECKPOINT行 + 跨实例N1_RUNS==1)。 - ⚠️ resume 实例不要再挂
interruptBefore(previousNodeId 非 null 会再次停在该点,后续节点永不执行)。
5. 可观测(observation 埋点)
SaaStudioGraph.build 在 ObservationRegistry 非空且非 NOOP 时挂 graph-core 的 GraphObservationLifecycleListener + CompileConfig.observationRegistry(..) → 每节点发 spring.ai.alibaba.graph.node.<id> observation(trace/耗时/失败→Micrometer)。node id 编码在 observation 名 + 低基数键里(非自定义 context 类型,测试按名前缀捕获)。ObservationRegistry 经 ObjectProvider 软注入 yudao 共享 bean。
6. 生产派发契约(框架可换、无 split-brain)
GenerationDispatcher.dispatch(job)接口 → 两实现WorkerDispatchClient(http) |SaaGraphDispatcher(进程内 SAA 图) 经aigc.executor.dispatcher(默认 http 非破坏) 二选一单写。SaaGraphDispatcher:job→state→后台单线程compiledGraph.invoke(threadId=traceId)→出口组DifyCallbackReqVO进程内直调difyCallbackService.handleCallback(消灭 HTTP+HMAC,复用唯一三表同事务写链);串行真玩门Semaphore(1)守端口;图执行不包大事务(落库走 handleCallback 内短事务);失败统一归llm_error(图内真因不在契约#6 七值枚举内)。- 🔴 铁律「多派发路·回调可观测字段必须每路都 set」(回调 trace split-brain,与下方发布裁决 split-brain 是两回事):存在多条派发路(http worker / SAA 进程内 / executor 旧路)时,任一落库可观测字段(
trace_json/readiness_score)若只在部分回调构造点 set,换dispatcher即静默丢字段。实证:SaaGraphDispatcher.buildCallbackReqVO原只 set status/engineBundle/failureReason、漏 setTrace →dispatcher=saa(未来主力路)时trace_json恒 NULL、组B 9d/D11/D9 在 SAA 路静默失效(HTTP 路因 worker_extract_trace不漏)。修法:派发器回调构造点对称镜像 worker 的_extract_trace——从图终态OverAllState抽与之字节兼容的 camelCase 子集setTrace,严格 additive + best-effort(整段 try-catch 返 null,抽不到绝不打断生成)+ 复用同一aigc.trace.enabledflag,落库走现成persistTraceQuietly,零改契约/消费侧。字节兼容红线:抽出键集必须=worker_extract_tracecamelCase 子集(stage_fail→stageFail等 snake→camel;attempts 不含 per-attempt guards——SAA 源端三处 appendAttempt 从不写,照 K_ATTEMPTS 注释抽会逼实现造假;readiness 读顶层sevenGateVerdict.guards.H_progress而非 attempts[].guards),否则审核台/D11 两路读不一致。闭合:Round3703e462c(HJ-AGI-002 线),真库验 task141trace_json真非空/readiness=74、task145(开关关)NULL(verdictagentic编排-SAA.md)。坑:readiness 真值随真跑质量浮动(repairs=5→stability 降、cost 缺→efficiency 中性,故 74<HTTP 路 96);firstPlay因H_progress是嵌套对象喂asBool(Map)恒中性 0.5 = 组B 既存 bug(HTTP/SAA 同病、字节兼容仍成立),修评分器另立 F5 票,严禁为过测把 SAA 端 H_progress 改裸 bool(破 HTTP 字节兼容)。 - 🔴 铁律「worker succeeded ≠ 准予发布」(split-brain 防线):SAA worker 跑通九门(
runnableOk= 机制可玩:能加载/不崩/有终态)不等于准予发布。发布裁决归治理层——对抗 P0 安全审查(注入/越权/恶意资源)、金丝雀完玩率、人工二审,是与"机制可玩"正交的另一维。生成路生产化前,安全门须由 W-G1 GP9 合规段接管(10 负例 staging 真验),worker 自身只产"机制可玩"信号、无权置位 published。两者混为一谈 = split-brain(worker 自评准予上架)。来源:agent-loop-v1 退役登记(Lane B)。
7. 验证门(无 harness 端口也能验大半)
- 门 E(依赖/启动):
dependency:tree断言 Boot 全 3.5.14 / redisson 4.4.0 / 零 3.22 泄漏 / 删的依赖消失;test-compile(main+test) 绿;最小 Context 共存不破 BeanCreation。 - 门 B(checkpoint 真恢复):纯 Java 2 节点小图(不调 LLM、不碰端口),跑一半 interrupt→查 MySQL 断 checkpoint 落库→全新
CompiledGraph实例 + 显式 checkPointId resume→抵 END 且节点不重放。gated 在-Dsaa.checkpoint.mysql=1 -Dsaa.mysql.pass=…(密码经-D注入不入库)。 - 真玩 e2e(图真跑九门→进程内 handleCallback→建版本/组包/入 feed)须占 4320/9222,与生成批跑串行(
flock单实例锁)。harness-gated 测试无-Dsaa.harness=1时正确 skip。
真·全图 e2e 测成功率(U7 集成顶石,SaaFullGraphE2eTest,2026-06-18 首证)
- 测法(零 DB 跑真全图):
new SaaGraphDispatcher(props, stubCallback, null, NOOP, null)——props 填llmBase/apiKey/dispatcher=saa/saaGameRuntimeDir、saaCheckpointEnabled=false+DataSource=null(内存 checkpoint,零 DB);stubDifyCallbackService只捕获 reqVO 不写库。门控-Dsaa.e2e=1+envNEWAPI_KEY+-DGAME_RUNTIME_DIR。 - 异步派发测试坑(实翻):
dispatch是异步+单线程执行器+Semaphore(1) 串行真玩门,且单款墙钟 3.5-16min(九门真玩循环+最多 8 救场轮固有,非 SAA 特有=同 Python W-G1)。别"每条 dispatch 后 arm 新 latch 等 240s"(串行+回调错配=归因全错);正解=一次性投递全 N 条→共享CountDownLatch(N)→stub 按reqVO.getTraceId()精确归位并发 map,每条超时 900s。 - 首证基线:真全图(render→…→真九门 play→…→emit/giveup)真 LLM+真九门跑通、零 prod bug;成功率 3/5=60% < 80%。成功路 engineBundle≈211KB(含引擎,经
__GameBundle全局名校验才置 succeeded)。难门 I_control/F_wiring/H_progress 卡 60%——根因非"模型天花板"(2026-06-18 根因分析 + Codex 复核证伪:无样本确认真硬天花板;scale-20 无"人工介入后仍不过"的款)。主因 = 无人值守产线的"喂不全 + 救场机制不足 + 门判据偏":命门 = 注入的 few-shot 探针_goldenpath-probe/factory.js缺_forensicsView/latch、而 prompt 硬要求它(模型抄没见过的实现);I_control 平滑跟手 harness 查、prompt 零提及;failCount 跨档不重置 + repair 不回喂源码。详见docs/brainstorms/2026-06-18-生成失败根因分析。便宜模型不稳→救场耗尽 giveup(真落盘,非崩溃)。SAA 机制 ≠ 生成质量:转默认产线(U7)的 ≥80% 门由 W-G1 生成质量域把守,非 SAA 编排 bug。 - 003-U1 验收门工具化(plan 2026-06-18-003,可作 flip 前置硬门):把本测从「内置 5 条 / 只断
succeeded>=1(留人判 80%=不可作硬门)」升成客观可运行产线门——① brief 内置扩 12 条多品类 +-Dsaa.e2e.briefFile外部行式文件(去空白/跳空行/跳#,空则回退内置);②-Dsaa.e2e.minSuccessRate可配硬断言,默认不设=仅存在性(保 003-U1「先实测真基线再收敛」、不破普通构建),显式0.8即 flip(003-U2)前置硬门;③ 成本会计 = token 代理,非¥——从回调trace.tokens.{prompt,completion}+modelTier(升 stage2 计数)汇总,-Dsaa.e2e.maxTokensPerGame仅软警告不硬阻断(创始人裁「质量优先、允许临时破价、记账对账」)。🔴 坑:SAA 路无 ¥ 数据——SaaGraphDispatchercost v0 整段省略(token≠¥,挂 cost 语义错)、折¥=follow-up F3 → 别接 ¥ 门、别造假¥,只记 token。纯逻辑抽静态方法 + 模型无关单测SaaFullGraphE2eGateLogicTest(普通mvn test即跑,不需模型/key,12/12 过)。
关联:contract-first-development.md(job/callback 即契约#6)|cheap-model-game-generation.md(被编排的便宜模型生成)|staging-ops.md(mini-desktop 构建/门)|决策与坑全集见记忆 saa-agentic-infra-decision。