310 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
topic: 生成验收门
canonical: true
date: 2026-06-24
---
# 开闸验收门 W-G1 · 生成对外放行的 6 道门
> 🚧 **架构演进中** —— 廉价线 **gameDefinition 已废(错误路线),现行 = A-model 写真 `src/`**;tier2 富游戏自治轨建设中(0 号 spike accept)。文中结论可能随推进变化,以子树 [README](README.md) / 运行时 SoT [`agentic运行时架构图说`](agentic运行时架构图说.md) 与最新裁定为准。
> **这是什么**:绘境AI 生成引擎"对外开闸"那一刻必须先架好的 6 道验收门的设计文档。它回答的是一个很具体的问题——**当"一句话生成游戏"第一次向真实创作者放开时,我们到底要先焊死哪些门、可以并行补哪些门、又有哪两道前置必须先验过才许放行?**
> **给谁看**:负责生成主线开闸的工程师、质量与审核台一侧的同事、做发布放行决策的产品与创始人,以及想看清"放量前的安全/可控边界"的架构评审。
> **怎么读**:先读 §1 那张"6 门怎么排"的全景图建立整体认知,再按 §2§4 顺着三组门看每道门具体检查什么;想确认"能不能放"读 §5 的两道开闸前置验证结果;残留待办在 §6。
生成引擎此前讲的是"一句话怎么变成一款游戏"那条技术主线(见[生成引擎主文档](README.md))。本文讲的是另一件事:那条主线建好之后,**怎么把它对真实用户安全地"开闸"**。开闸不是一个开关,而是一组前置条件——这份文档把这组条件钉成 6 道可验收的门。
---
## 1. 一句话与一张图:开闸要先架哪 6 道门
先把"开闸"这个词说清楚。**开闸 = 把"一句话生成游戏"这条能力,从内部封闭测试状态,正式对真实创作者放开。** 这是一个有风险的动作:放开之后,陌生用户会带着各种意图涌进来,既可能用一句违规的话试探合规底线,也可能用并发请求把后台那条串行的生成流水线压垮。所以开闸不能裸奔,但也不必等到所有质量门尽善尽美才放——那样窗口期早就过了。
我们的判断是把 6 道门按"阻塞等级"分成三类,各司其职:
- **2 道硬阻塞门,必须先焊死,否则一律不许放行。** 它们守的是"放开之后系统会不会被一个用户压垮"和"违规内容会不会直接进到玩家面前"这两条不可逆的红线。
- **3 道落库门,可以和开闸并行上线,且第一版全部只观测、不拦截。** 它们守的是"放开之后我们还管得住、追得清"——把每次生成的全过程数据落库、算出就绪分、查出重复创意,但都只记录、不卡人。
- **1 道首局体验门,和上面同批焊好一块"地板"。** 它守的是放进来的游戏"陌生人 10 秒钟觉不觉得能玩",但它判失败时不会推翻底层那套硬验收。
这套排布背后的设计理由,一句话就能说清:**别等 6 门全齐才开闸(只有那 2 道硬阻塞门不做,确实不能放),但也别裸奔开闸(把可控性和合规这两条焊死,其余的边放量边观测)。** 下面这张图是 6 门的全貌:
```mermaid
flowchart TD
subgraph A["组A · 硬阻塞 · 先做 · 已落地"]
D12["D12 控制平面 v0<br/>降级 + 配额并发 + 背压 + 记账骨架"]
GP9["GP9 合规先行段<br/>生成前 prompt 审查<br/>10 负例 100% 阻断"]
end
subgraph B["组B · 落库组 · v0 并行 · 全非阻断 · 已落地"]
N9D["9d trace 账本<br/>worker 富数据落 trace_json"]
D11["D11 就绪评分<br/>读 trace 算 0-100 落库 + 审核台透出"]
D9["D9 反同质化<br/>归一查重 · 撞重只告警"]
end
subgraph C["组C · 同批焊地板 · 已落地"]
FIRST["首局体验子门<br/>可玩≤2s / 首反馈即时 / 60s 品类闭环"]
end
A -->|2 门焊死| OPEN(("对外开闸"))
OPEN -.-> |"放了要能管 / 追溯"| B
C -.-> |"随补 L1 driver 自产"| OPEN
M4[["跨轨 · 随 M4:真实计费扣退<br/>(网关硬编码 UserId 阻塞,推迟)"]] -.-> D12
style D12 fill:#f5b7b1
style GP9 fill:#f5b7b1
style OPEN fill:#a9dfbf
```
图里出现了几个内部代号,在这里一次解释清楚,后面各节会逐个展开:
- **D12 / GP9 / D11 / D9** 是这套生成体系里给各道质量与控制门排定的编号(D 系列是质量/控制门,GP 系列是合规门)。本文只涉及开闸这一批,完整门体系不在此展开。
- **trace(轨迹)** 指一次生成任务从头到尾产生的全过程结构化记录——走了哪几道门、花了多少成本、用了哪些模型、重试了几次。"9d trace"是其中第 9 维度的全链轨迹账本。
- **worker** 指真正跑生成的那个独立工作进程(Python 写的),它在后台一轮轮地把一句话变成游戏。
- **落库** 就是把数据写进数据库持久化保存的口语说法。
- **L1 / driver** 是生成侧的概念:L1 指"模板成游戏"那一档最轻量的生成能力;driver 指九门验收时用来真玩游戏的那个操作脚本(它知道这款游戏该点哪、该划哪)。
这张图最该记住的是那条主轴:**只有 D12 和 GP9 这两道硬阻塞门焊死了,才允许"对外开闸";开闸之后,组B 三门负责"放了还管得住"、组C 一门负责"放进来的游戏体验有地板"。** 图右下角那个虚线挂着的 M4,是一件被刻意推迟的事,§1.2 单独讲。
### 1.1 6 门一览
| 门 | 一句话:它检查什么 | 阻塞分级 | 归属轨 |
|---|---|---|---|
| **D12 控制平面** | 放开前先有降级开关、配额并发限制、背压保护、记账骨架,保住后台那条全局串行的生成流水线不被压垮 | **硬阻塞** | 生成引擎(计费扣退跨网关) |
| **GP9 合规先行** | 生成开始**之前**先审查用户那句 prompt,违规的不入队、不进玩家信息流 | **硬阻塞** | 生成引擎 |
| **9d trace** | 把 worker 已经产出的富生成轨迹结构化落库,供 D11/D9 复用 | v0 并行,不卡门 | 生成引擎 + 契约 |
| **D11 就绪评分** | 读 trace 算出一个 0-100 的"就绪度"分数,落库并在审核台可见 | v0 并行,不卡门 | 质量 + 产品 · 审核台 |
| **D9 反同质化** | 把游戏配置归一化后查重,撞到重复创意只告警、不拦截 | v0 并行,不卡门 | 生成引擎 / 质量 |
| **首局体验子门** | 品类化的首局服务等级目标,3 条断言,判失败也不推翻底层九门 | 不卡门 · 开闸前补 | 质量 / harness |
这三组门的取舍理由,可以浓缩成三条核心决策:
1. **D12 和 GP9 为什么必须硬阻塞?** 因为这两件事不做,后果都不可逆。后台的生成 worker 是**全局串行**的(一次只处理一个任务),不做控制平面,单个用户的并发请求就能把它压垮,所有人都生成不了;不做合规先行,违规内容会直接生成出来进入玩家信息流,这是法务红线,一旦发生无法回收。
2. **那 3 道落库门为什么第一版只观测、不拦截?** 因为底层那套"九门 harness"(在真实浏览器里跑游戏、用九道确定性检查判定能不能玩的测试夹具)已经是挡住坏游戏的**硬地板**了。这三道落库门是更上一层的质量观测,第一版若一上来就生效拦截,反而会**误杀好游戏**——所以先只记录数据,等积累够了再考虑收紧。(这里"九门硬地板"说的是**开闸放行**那一侧:一款游戏要进信息流,仍按完整九门要求。生成环迭代生成时的消费口径曾在 2026-06-20 收窄为"只认五道客观健康门 AE",但已于 2026-07-02 由质量轨裁定回到 **driven 时九门全量 AND**、收窄 AE 归为历史方案——沿革详见 §2.4。)
3. **真实计费为什么不在这一批?** 见 §1.2。
### 1.2 真实计费扣退为什么被拆出去(随 M4)
图里那个虚线挂着的方块写着"M4:真实计费扣退"。M4 是后续一个独立的里程碑代号。**真实的"按量扣费 / 失败退费"被刻意从开闸这一批里拆出去,推迟到 M4 再做**,原因有两个,都很实在:
- **网关层有一处阻塞。** 所有模型调用统一走的 new-api 网关(new-api = 绘境AI 内部统一管理模型 key 与用量计费的网关),它的 `AddToken` 接口当前把 UserId 硬编码死了,没法按真实用户记账。
- **MVP 阶段本就没有真支付。** 既然还收不到钱,也就谈不上真扣费。
所以这一批里,D12 控制平面用的是一套"额度记账"的**骨架**先顶上:配额和背压该限的限、该挡的挡,但记账只是占位字段和日志,**不是真扣费**。等 M4 把网关那处阻塞解了、真支付也接上了,再把骨架换成真账。把这两件事拆开排期,是为了让开闸不被一个网关 bug 卡住。
---
## 2. 组A:两道硬阻塞门(D12 控制平面 + GP9 合规先行)
这是开闸的承重墙,已落地并合入主干(命名空间 `com.wanxiang.huijing`)。两道门一道管"系统不被压垮",一道管"违规不进信息流"。
### 2.1 D12 控制平面:在唯一入口前焊四道门
D12 的核心思路是:**在生成任务的唯一入口处,入队之前,按"从最便宜到最贵"的顺序焊四道门,层层拦截,把最贵的那道(要调大模型的合规检查)放在最后**——这样超配额、超背压的请求根本走不到调模型那一步,省钱也省算力。
这里有一个容易被绕过的坑,先说清:生成任务的提交入口是 `AigcTaskServiceImpl.submitGenerate`,但还有一个 `retryTask`(重试)路径。如果只在 `submitGenerate` 上焊门,用户走重试就能直接插库、绕过所有控制。所以实现上把控制逻辑抽成了一个共用方法 `enqueueWithControlPlane`,让提交和重试都走它——**堵死重试绕过控制的口子**。四道门的顺序如下:
```mermaid
flowchart LR
REQ["生成请求<br/>submitGenerate / retryTask"] --> G1{"① 降级门<br/>aigc.generate.paused?"}
G1 -->|"已暂停"| R1["拒绝:GENERATE_PAUSED"]
G1 -->|"放行"| G2{"② 配额 + 并发门<br/>per-creator×level 当日计数"}
G2 -->|"超上限"| R2["拒绝:QUOTA_EXCEEDED"]
G2 -->|"放行"| G3{"③ 背压门<br/>全局在飞 queued+running"}
G3 -->|"超队列深度"| R3["拒绝:BACKPRESSURE_REJECTED"]
G3 -->|"放行"| G4{"④ GP9 安全门<br/>(最贵,放最后)"}
G4 -->|"违规"| R4["拒绝:UNSAFE_PROMPT"]
G4 -->|"安全"| INS["insert 入队<br/>落 level=L1 + 额度记账骨架"]
style G4 fill:#f9e79f
style INS fill:#a9dfbf
```
逐道门的含义:
- **① 降级门**:读基础设施配置中心(infra `ConfigApi`)里 `aigc.generate.paused` 这个键,运营可以热改它一键暂停全站生成。读取失败时**容错放行**(fail-open on read)——配置中心抖动不该把生成全堵死。
- **② 配额 + 并发门**:按"每个创作者 × 会员档(level)"统计当日生成计数,达到或超过(`>=`)上限就拒。
- **③ 背压门**:统计全局在飞的任务数(排队中 queued + 运行中 running),达到或超过队列深度上限就拒。这道门守的就是那条全局串行 worker。
- **④ GP9 安全门**:把要调大模型的合规检查放在最后,前面已被配额/背压拒掉的请求,**不会白白付一次安全检查的成本**。
四门全过,才 insert 入队,同时落两样东西:一个 `level` 字段(会员档,v0 阶段统一记为 L1);一套额度记账骨架(只是日志和占位字段,**不是真扣费**,见 §1.2)。
### 2.2 GP9 合规先行:为什么必须独立、且必须 fail-closed
GP9 这道门有两个设计决定很关键,都源自踩过的坑:
**第一,它必须用一个独立的安全检查客户端,不能复用生成执行器的那个 LLM 客户端。** 原因有两层:执行器的客户端 `ExecutorLlmClient``aigc.executor.enabled` 这个开关门控着,服务层根本注入不到它;更重要的是,如果把安全检查塞进执行器内部去做,它会卡住那条串行执行的节拍(tick),等于自己给自己制造了一个拒绝服务(DoS)。所以 GP9 在服务层用一个**独立的** `SafetyCheckClient``safety.prompt-check`,配**短超时 8 秒 + 至多 1 次重试**。
**第二,它必须 fail-closed(失败时从严)。** 这是一条方向性的安全判断:
```mermaid
flowchart TD
P["用户 prompt"] --> CHK["SafetyCheckClient<br/>调 safety.prompt-check<br/>超时 8s + 至多 1 重试"]
CHK -->|"safe=false"| REJ["同步拒绝<br/>抛 UNSAFE_PROMPT<br/>不入队"]
CHK -->|"safe=true"| PASS["放行,继续生成"]
CHK -->|"超时 / 调用失败"| FC["fail-closed<br/>抛中性 AIGC_LLM_SAFETY_ERROR<br/>(失败归类仍记 llm_error)"]
style REJ fill:#f5b7b1
style FC fill:#f9e79f
style PASS fill:#a9dfbf
```
- 检查结果 `safe=false`(判定违规):同步拒绝,抛 `UNSAFE_PROMPT`,不入队。
- 检查超时或调用失败:**fail-closed**,抛一个**中性**的业务码 `AIGC_LLM_SAFETY_ERROR`(提示"安全检查暂不可用,请稍后重试")。这里有两处刻意的设计——一是合规判不出来就放过,等于让违规内容在窗口期直入信息流,风险不可逆,所以宁可从严挡掉;二是用中性提示而非复用 `UNSAFE_PROMPT`,是为了避免 new-api 网关偶尔抖动时,把一个正常用户误标成"发了违规内容"。注意这里的"不新增枚举"指的是**失败归类**——它复用既有的 `FailureReasonEnum.LLM_ERROR`(归因 tag 仍记 `llm_error`),不在那个跨契约共享枚举上单边加值;但 ErrorCode 这一侧确实新增了 `AIGC_LLM_SAFETY_ERROR` 这个可抛业务码,因为"提交侧同步拒绝"必须有一个独立的数值码、且文案要中性。
### 2.3 组A 的错误码、回滚与契约
- **错误码**:在 aigc 模块的错误码里新增了 001 子段(`1-101-001-***`),共五个码——其中 D12 三个控制平面码 `AIGC_QUOTA_EXCEEDED`(超配额)、`AIGC_BACKPRESSURE_REJECTED`(背压拒绝)、`AIGC_GENERATE_PAUSED`(已暂停),再加上 GP9 的两个合规码 `AIGC_UNSAFE_PROMPT`(真违规同步拒绝,004)与上一节那个 fail-closed 的 `AIGC_LLM_SAFETY_ERROR`(安全检查暂不可用,005)。这五个都是**业务码、走 HTTP 200**——验收时要断言返回的是业务码,而**不是** HTTP 429 / 503 这种协议层状态码。
- **回滚**:整个控制平面挂在 `aigc.control-plane.enabled` 这个功能开关(feature-flag)后面,**默认关闭 = 现行逻辑逐字不变**(全部门加 GP9 都旁路)。
- **契约与数据迁移**:合规负例语料放在契约目录 `contracts/prompts/eval/safety.prompt-check/{inputs,labels}.jsonl`,共 **10 条负例,覆盖 10 类合规红线类目**;数据库迁移 `V15.0.0__aigc_task_add_level.sql` 给任务表加 `level` 列,只增不改(additive)、默认值 1,存量数据回填为 L1。
### 2.4 生成环消费九门的口径:硬门收窄成客观健康门 AE(创始人 2026-06-22 历史回收判定捡回)
> **历史化注记(2026-07-02,质量轨裁定)**:本节记述的"生成环把九门收窄成只认 AE 客观健康门",是 gamedef 时代 driver 常缺、G/H/I 三门不可靠时的消费策略。现行便宜档在门跑前自动生成 play-spec 使 driven=true,cheap 与 tier2 的判据代码都按九门全量与富游戏门取 AND(M1 达标 bake-off 15/15 正是这个口径);undriven 时 E_live/H_progress 自动降 advisory 的分级仍保留在 harness。因此 **driven 时九门全量 AND 为现行 L1 口径,"收窄 AE"归为历史方案、不再是待选路线**;判定归属见同目录现行 canonical《游戏质量与爆火能力》(裁定 L1-a)。以下原文按历史记录保留,不删。
前面几节把九门 harness 讲成一道统一的"硬地板",这个说法在"开闸放行"这个语境里仍然成立——一款游戏要进信息流,确实要先过这套真玩检测。但要把一件 2026-06-20 拍下的口径变化讲清楚,否则架构档会和现行真相脱节:**生成环(SAA 管线)在迭代生成时怎么"消费"九门的裁决,已经从"九门全过才算成功"收窄成了"只认五道客观健康门 AE"。** 这是九门收窄后、gamedef 路实测生成质量从 all-9 口径下的约 50% 做到 91.7% 的直接原因之一——实测样本 n=12:关闭 thinking 时过 8/12,启用 Anthropic 原生 thinking 分离协议后过 11/12(即 91.7%);创始人 2026-06-20 拍板以 n=12/91.7% 宣告达标(n≥30 只多给方差信息,不改"thinking-on 更好且过门"的结论)。gamedef 路虽已判错误路线废弃,但"九门收窄成 AE 客观门"这条验收消费策略引擎无关、对现行 A-model 真 src/ 同样适用。
先把九道门按"判什么"分成两类。一类是**客观健康门**,它们不依赖 driver(driver 指真玩脚本里那段"知道这款游戏该点哪、该划哪"的操作逻辑)就能判定,纯看运行时本身健不健康:A_boot(能不能起来)、B_uncaught(有没有未捕获异常)、C_frame(跑不跑帧)、D_render(画面有没有真渲染)、E_live(进程活不活着)。另一类是**driver 依赖门**:G_input(响不响应输入)、H_progress(机制有没有进展)、I_control(可控性),这三道必须有一段能真玩这款游戏的 driver 才判得了;而 F_wiring(游戏事件有没有真触发 `rt.fx` 之类特效)同样依赖游戏被真玩起来,没有 driver 必挂。
收窄后的口径是:
- **生成环的硬门只认 AE 五道客观健康门**。便宜模型新生成一款游戏、还没有为它写专属 driver 时,G/H/I 三道 driver 依赖门**不再否决生成、也不进救场回喂**——它们降为"参考门",失败只记录、不把这一轮判死,免得因为缺一段 driver 就把一款其实健康的游戏空跑八轮救场烧掉。
- **F_wiring 移出客观硬门**。"有没有真用引擎、有没有真特效"这层语义,改由 VLM 看截图来判(对应 README/OpenGame对照里那条"player 软门做厚"),不再让一道必然挂的事件触发门去卡生成。
- **九门 harness 本身一行不改**:它照常跑、照常把九门逐门结果写进 `verdict.json`,门的判定逻辑动都不动。变的只是"生成环如何读这份 verdict"。这条边界很关键——动的是消费侧策略,不是裁判本身,所以"开闸放行"那一侧仍可以按完整九门来要求。两套口径用 `-Dsaa.gen.gateMode=all9` 双轨可回退:默认走收窄后的客观门模式,加这个参数就退回旧的"九门全过"模式做对比。
这个收窄背后还藏着一条**必须记住的实现约束(030)**,它是客观门解耦能不能真正生效的前提。`SaaGenNodes.playNode` 判 pass 时,如果前置条件写成 `rc==0`(rc = 真玩脚本 `play.cdp.cjs` 的退出码),问题就来了:这个退出码本身是**九门聚合**的——只要任意一道门(含 driver 门)挂了,脚本就 exit 1。于是一旦某道 driver 门失败让 rc=1,它会**反向否决**掉"只认客观门 AE"的解耦,让收窄形同虚设——2026-06-20 当天就是这个 bug 让 Phase 1 空转、白烧了 302K token。修法是在客观门模式下把判据改成 `pass = (rc==0 || rc==1) && objectiveGatesPass(verdict)`:不再拿聚合退出码当门,而是直接读 `verdict.json` 里 AE 五道门的结果。谁要在生成环里重写 verdict 消费逻辑,这条耦合坑必须先避开,否则解耦白做。
> 口径归属:那条 rc 耦合坑(客观门解耦被九门聚合退出码反向否决)的实现教训仍然有效,是日后重写生成环 verdict 消费逻辑时必须先避开的坑。它此前蒸馏在 `.agents/skills/saa-graph-orchestration.md` 的一节里,但该 skill 已于 2026-07-01 重写为 cheap-worker 主线、原 §3.5 不复存在;"收窄 AE" 现属历史消费策略,现行口径与四层质量模型的裁定改以同目录 canonical《游戏质量与爆火能力》为准。这条收窄的原始决策与 Phase 04 路线仍可 `git show 8ea97234:docs/plans/2026-06-20-关闭九门判定-全面参考OpenGame-决策与Phase0-4.md` 查阅。OpenGame对照档里那条 Phase 04 落地路线([OpenGame对照 §3.5](OpenGame对照.md))承接的是收窄之后 SAA 线的远期设想,现行 cheap/tier2 主线已回到九门全量 AND。
---
## 3. 组B:三道落库门(9d trace + D11 就绪评分 + D9 反同质化)
组B 是开闸的"可控性"层,已落地、全部非阻断(已合入主干)。理解组B 的关键,是先理解一个反直觉的洞见。
### 3.1 命门洞见:不是缺数据,是富数据在边界被丢了
做这三道门之前,直觉会以为"我们缺数据,要从零造一套生成数据采集"。真相恰恰相反:**worker 早就产出了富数据,只是在回调边界上被丢弃了。**
worker 每跑完一轮,它的 `result` 里其实已经包含了非常丰富的信息——九门逐门的通过情况(guards)、成本(cost)、用了哪些模型(models)、重试了几次(attempts)、验收规格(gatespec)、裁决(verdict)、修复记录(repairs)、墙钟耗时(wall)。问题出在:worker 往后端回调时,`build_callback_payload` 这个函数只往回传了 5+2 个字段,`result` 作为一个线程内的局部变量,随着那个线程结束就消亡了,富数据再也找不回来。
```mermaid
flowchart LR
subgraph W["worker(已产富数据)"]
RES["result<br/>guards/cost/models/attempts<br/>gatespec/verdict/repairs/wall"]
BCP["build_callback_payload<br/>只塞 5+2 字段"]
RES -->|"⚠️ 富数据在此被丢弃"| BCP
end
subgraph DB["后端落库"]
TJ[("trace_json")]
RS[("readiness_score")]
end
BCP -.->|"修法:把被丢的富数据接过来"| TJ
TJ --> SCORE["D11 ReadinessScorer 读 trace 算分"]
SCORE --> RS
style RES fill:#f9e79f
```
所以这三件事的本质,**都不是从零造数据,而是把那份在回调边界被丢弃的富数据接过来落库**。
### 3.2 三道门各自做什么
- **① 9d trace(公共底座)**:这是另外两道门的数据来源,所以**契约先行**——先改契约 `contracts/api-schemas/aigc.yaml` 里的 `DifyCallbackReqVO`,给它加一个 `trace` 字段(只增不改、可选),再改 Java 侧的值对象和 worker。worker 的 `build_callback_payload``result` 抽出 9d 子集回传,后端 `DifyCallbackTxService` 在回填段尽力(best-effort)落到 `game_aigc_task.trace_json`。这里定了一个**必填子集七项**作为"轨迹完整率"的分母:`{pass, repairs, wallS, models, attempts, gameId, stage}`;其余字段因为和具体生成路径相关、worker 走哪条路尚未定死,所以 schema 放宽、不绑死任何一条路径。
- **② D11 就绪评分(`ReadinessScorer`)**:读 trace,加权算出一个 0-100 的就绪度分数。权重是:可玩性 playability 占 0.5、首局体验 firstPlay 占 0.25、稳定性 stability 占 0.15、效率 efficiency 占 0.1(**这套权重的管辖自 2026-07-02 起归质量轨;四维数值 0.5/0.25/0.15/0.1 不动,任何调整都走质量轨校准单提案 —— 见同目录现行 canonical《游戏质量与爆火能力》**)。一个重要的容错设计:**字段缺失时取中性值 0.5,而不是直接打 0 分**——免得因为某个可选字段没采到就把分数打到地板。算出的分落到 `readiness_score` 字段,并在审核台接口 `/task/page` 的返回里透出(前端那个 aigc 列表页是产品轨的后续工作,当前 game-admin 还没有这个页)。
- **③ D9 反同质化**:把生成早期 agent-loop-v1 编排器里的 `_norm_text`(归一化纯函数,几乎是原样复制)和 `mint_design_id` 的思路移植过来,在 worker 侧写一个 `dedup.py`,把游戏配置归一化后算出一个签名去查重。**撞到重复创意时只 `log.warn` 告警 + 把 `trace.similarity.dupHit` 落库,绝不拦截。**
### 3.3 组B 的非阻断硬约束、回滚与关键修复
- **非阻断硬约束(命门)**:这是组B 不可破的红线——上面三件事**任意一件**的落库、算分、查重失败,都必须 `try-catch` 把异常吞掉 + `log.error` 记录,主回调链照常走到 `PUBLISHED`(参照既有 `DifyCallbackServiceImpl` 的 best-effort 范式)。**落库这一组绝不允许因为观测失败,反过来拦截了主链。**
- **回滚**:三件各有一个开关(`aigc.trace.enabled` / `aigc.dedup.enabled` 等),字段都是 additive 可空的,停止写入即等于回滚。
- **数据迁移**:`V16.0.0__aigc_task_add_trace_readiness.sql`,加 `trace_json`(JSON 类型)和 `readiness_score`(SMALLINT 类型)两列。
- **两处关键修复(已合入)**:
- 提交 `703e462c` 闭合了一处 **split-brain**(直译"裂脑",指同一份事实在两条代码路径上各落各的、迟早不一致的隐患)——当派发器走 SAA 进程内路时,也要落 `trace_json` / `readiness_score`,保证 HTTP worker 路和 SAA 路两路一致,不会切了路就静默丢字段。
- 提交 `5f6cdd0c` 修了 `ReadinessScorer.firstPlay``H_progress` 嵌套对象的 `.pass` 时的一个 bug——修好后 HTTP 和 SAA 两路的 firstPlay 分能从恒为 0.5 走到可达 1.0。
---
## 4. 组C:首局体验子门
组C 是和组A/B 同批焊好的那块"体验地板",已落地。它要验的是生成主线对外的那句口号——**"陌生人 10 秒钟觉得能玩"**——能不能兑现。
具体落地是在九门 harness 的真玩脚本(`play.cdp.cjs`)里加一道首局门,验**3 条断言**:
1. **可玩 ≤ 2 秒**:从打开到能真正操作,不超过 2 秒。
2. **首反馈即时**:第一次操作要立刻有反馈。
3. **60 秒内品类核心反馈闭环可达**:一分钟之内,能走完这个游戏品类最核心的那个反馈闭环(比如打砖块要能消掉一块砖)。
这里有一个边界必须说清:**首局门是 H 门的"additive 派生超集"**。H 门是九门里判"机制有进展"的那道门;首局门复用 H 门的裁决结果,但即使首局门判 FAIL,**也不会推翻** H 门的 pass/fail——H 门仍然是机制层的硬地板,首局门只是在它之上叠一层更贴近真实体验的观测。
```mermaid
flowchart LR
GAME["生成产物"] --> H["H 门<br/>机制有进展(硬地板)"]
H --> FIRST["首局体验子门<br/>(H 的 additive 超集)"]
FIRST --> A1["① 可玩 ≤ 2s"]
FIRST --> A2["② 首反馈即时"]
FIRST --> A3["③ 60s 品类闭环可达"]
FIRST -.->|"FAIL 不推翻 H"| H
style H fill:#a9dfbf
style FIRST fill:#f9e79f
```
落地上还有三个要点:
- **v0 走的是"路A"——零改生成侧。** 不动生成那一头,而是从现成的验收规格(gatespec)里的 `driver` 家族反推游戏品类:`tap-targets` 对应"放置类"、`+safeOnly` 对应"规避类"、`paddle-intercept` 对应"技巧类"。然后复用现成的三款游戏(打砖块 breakout / 井字棋 tictactoe / 扫雷 saolei)来验,**零新生成**。
- **阈值在真实环境校准。** 2 秒 / 60 秒这两个阈值是在 mini-desktop 的无头(headless)环境里实测校准的——无头浏览器冷启动比真机慢,所以阈值按它来定更稳。
- **回滚**:harness 上的一个开关,**默认开**作为验收门,但 FAIL 不阻断九门 pass。
---
## 5. G0/G1 开闸前置验证(已 PASS · 无 blocker)
6 道门架好之后,放行之前,还有两道"开闸前置"必须在 mini-desktop 的预发(staging)环境里真验过——这两道叫 G0、G1(G 是 gate 的首字母,这里特指"开闸前置验证项")。两道都已 **PASS、无阻塞项(blocker)**
```mermaid
flowchart LR
GATES["6 门已焊好"] --> G0["G0 创作页核实"]
G0 -->|"PASS"| G1["G1 GP9 staging 真验"]
G1 -->|"PASS · 无 blocker"| READY["具备开闸条件<br/>(放行决策待创始人)"]
style READY fill:#a9dfbf
```
### 5.1 G0:创作页核实——空模板列表是"按设计的过渡态",不是缺陷
G0 验的是创作页的状态。这里有一个很容易被误判成"断点缺陷"的现象,必须说准:**创作页的模板列表是空的(返回 HTTP 200,不是故障),这是按设计的过渡态,代号 W-CLEAN。** 它的完整形态是:一个诚实的空态提示"玩法模板体系升级中",一个被置灰(disabled)的"开始生成"按钮,外加一个"去游戏流"的出口。这是**有意为之的过渡态,不是断点缺陷**。
背后的真相是:后端的模板白名单 `SUPPORTED_TEMPLATE_IDS=["generic"]` 是**非空**的(保住 W-G1 这条一句话生成链路能跑),只是前端的创作入口暂时不暴露模板而已。需要和技术决策口径对齐的一点是:**被废掉的是"游戏模板/填参线"那套东西(W-CLEAN),而"玩法模板"这个品类引导框架并没有被废,只是等 W-G1 落地后回填**。(W-G1 = 第一批对外开闸的游戏;W-CLEAN = 把旧的"游戏模板/填参"产线清理掉的那项工作。两者别混。)
### 5.2 G1:GP9 合规门 staging 真验——10 条违规负例 100% 被挡
G1 验的是 GP9 这道合规门在真环境里到底挡不挡得住。结论很硬:**10 条违规负例经过真实的 new-api 网关(模型用 `MiniMax-M2.7`)判定,100% 返回 `safe=false`**,进而 `submitGenerate` **100% 抛出 `AIGC_UNSAFE_PROMPT`、0 条落库入队**——这一点在日志层和数据库层做了**双重实证**。这意味着 §2.2 设计的那道合规先行门,在真环境里确实做到了"违规一条都进不来"。
---
## 6. 待办 / Follow-up(残留)
6 门已焊死、两道前置已验过,真正剩下的是放行决策本身和几项需要拍板的数值。这些都是已知的、被显式追踪的残留,不是隐藏缺口:
1. **开闸放行决策本身。** 焊死门已就绪,放行与否由创始人裁定(此决策优先于任何自动门)。
2. **待创始人拍的数值。** 组A 只落了 `level` 列加门结构,具体数值是占位的,需要拍板:会员档 L1 / L2 / L3 的每日配额与并发数(组A 占位)、D11 的评分权重(§3.2 占位)、D9 的模糊近似阈值——v0 只做了"精确撞重 + 归一化完全相等",模糊相似度怎么量,要等真实数据观察到分布之后再定。
3. **产品轨。** admin 的 aigc 任务列表页要不要近期就排上"就绪分"展示(后端契约已经透出了,前端需要新建 aigc 列表页,或在 review 页 join 取这个分)。
4. **承接自 SAA 治理(round3)的两项(已随 SAA 冻结)。** 这两项挂在 SAA 裸图那条线上:F3 = SAA 成本字段只落 token 数、未折人民币;早期 boot 阶段 SAA 图前几次派发瞬态失败需预热。SAA 已降为最低优先级远期(非 MVP runtime),这两项随之冻结、不再是活残留;现行 cheap/tier2 线的成本已按 new-api quota 折 ¥ 落 trace.cost(Anthropic 路由 `RecordingChatModel` 补齐取证,见运行时 SoT §5.7)。F5 早已修复(即 §3.3 那个 `5f6cdd0c`)。
---
## 7. 源档导航
本文档是开闸验收门 W-G1 的策展层主档,把一份 canonical 活档(它本身又合并了 5 份历史 spec)整理成了这份给人读的全景。要看更深的门序、接线、契约细节与决策史,从下面进去:
| 去处 | 回答什么 |
|---|---|
| `.agents/skills/cheap-model-game-generation.md` | 便宜模型造游戏的 worker loop、九门真玩 harness、成本与模型选择(深落地权威源之一) |
| `.agents/skills/saa-graph-orchestration.md` | SAA 裸图编排的拓扑、加节点、checkpoint、dispatcher 契约与门(深落地权威源之一) |
| `.agents/skills/contract-first-development.md` | Contract-first:如何对齐 API/DB/SDK/event 契约并解耦并行工作 |
| [生成引擎主文档](README.md) | "一句话怎么变成一款游戏"的生成主线全景(本文的上游) |
| [生成运行时架构](agentic运行时架构图说.md) | 生成主线的编排基建、六条不变量、两条 split-brain 治理铁律 |
> **纪律**:本档是开闸验收门 W-G1 的策展层 SoT——读它 = 当前真相。门序/接线/契约的逐条落地在 `.agents/skills/`,决策史在 git;两者职责不同,不互相重复。当现行真相与某份带日期的历史 spec 冲突时,以本档与它引用的最新裁定为准。
---
> **验证状态**:本文档为架构策展文档,由开闸验收门 W-G1 canonical 活档改写而成,未改任何代码。承重硬事实——D12 四门门序、GP9 短超时 8s + 1 重试 + fail-closed、10 负例 100% 阻断、错误码 001 子段五码走 HTTP200(D12 三控制码 QUOTA_EXCEEDED/BACKPRESSURE_REJECTED/GENERATE_PAUSED + GP9 两合规码 AIGC_UNSAFE_PROMPT/AIGC_LLM_SAFETY_ERROR)、Flyway V15/V16 迁移、9d trace 必填七项、D11 权重(0.5/0.25/0.15/0.1)、首局门 3 断言(≤2s / 即时 / 60s)、G0 白名单 `["generic"]`、G1 真验 `MiniMax-M2.7` 100% safe=false / 0 落库、split-brain 修复 `703e462c` 与 firstPlay 修复 `5f6cdd0c`——均沿用源档已抽查属实的结论;品牌统一为"绘境AI",无旧名残留。