32 KiB
Raw Blame History

topic, canonical, date
topic canonical date
生成验收门 true 2026-06-24

开闸验收门 W-G1 · 生成对外放行的 6 道门

🚧 架构演进中 —— 廉价线 gameDefinition 已废(错误路线),现行 = A-model 写真 src/;tier2 富游戏自治轨建设中(0 号 spike accept)。文中结论可能随推进变化,以子树 README / 运行时 SoT agentic运行时架构图说 与最新裁定为准。

这是什么:绘境AI 生成引擎"对外开闸"那一刻必须先架好的 6 道验收门的设计文档。它回答的是一个很具体的问题——当"一句话生成游戏"第一次向真实创作者放开时,我们到底要先焊死哪些门、可以并行补哪些门、又有哪两道前置必须先验过才许放行? 给谁看:负责生成主线开闸的工程师、质量与审核台一侧的同事、做发布放行决策的产品与创始人,以及想看清"放量前的安全/可控边界"的架构评审。 怎么读:先读 §1 那张"6 门怎么排"的全景图建立整体认知,再按 §2§4 顺着三组门看每道门具体检查什么;想确认"能不能放"读 §5 的两道开闸前置验证结果;残留待办在 §6。

生成引擎此前讲的是"一句话怎么变成一款游戏"那条技术主线(见生成引擎主文档)。本文讲的是另一件事:那条主线建好之后,怎么把它对真实用户安全地"开闸"。开闸不是一个开关,而是一组前置条件——这份文档把这组条件钉成 6 道可验收的门。


1. 一句话与一张图:开闸要先架哪 6 道门

先把"开闸"这个词说清楚。开闸 = 把"一句话生成游戏"这条能力,从内部封闭测试状态,正式对真实创作者放开。 这是一个有风险的动作:放开之后,陌生用户会带着各种意图涌进来,既可能用一句违规的话试探合规底线,也可能用并发请求把后台那条串行的生成流水线压垮。所以开闸不能裸奔,但也不必等到所有质量门尽善尽美才放——那样窗口期早就过了。

我们的判断是把 6 道门按"阻塞等级"分成三类,各司其职:

  • 2 道硬阻塞门,必须先焊死,否则一律不许放行。 它们守的是"放开之后系统会不会被一个用户压垮"和"违规内容会不会直接进到玩家面前"这两条不可逆的红线。
  • 3 道落库门,可以和开闸并行上线,且第一版全部只观测、不拦截。 它们守的是"放开之后我们还管得住、追得清"——把每次生成的全过程数据落库、算出就绪分、查出重复创意,但都只记录、不卡人。
  • 1 道首局体验门,和上面同批焊好一块"地板"。 它守的是放进来的游戏"陌生人 10 秒钟觉不觉得能玩",但它判失败时不会推翻底层那套硬验收。

这套排布背后的设计理由,一句话就能说清:别等 6 门全齐才开闸(只有那 2 道硬阻塞门不做,确实不能放),但也别裸奔开闸(把可控性和合规这两条焊死,其余的边放量边观测)。 下面这张图是 6 门的全貌:

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,让提交和重试都走它——堵死重试绕过控制的口子。四道门的顺序如下:

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 客户端。 原因有两层:执行器的客户端 ExecutorLlmClientaigc.executor.enabled 这个开关门控着,服务层根本注入不到它;更重要的是,如果把安全检查塞进执行器内部去做,它会卡住那条串行执行的节拍(tick),等于自己给自己制造了一个拒绝服务(DoS)。所以 GP9 在服务层用一个独立的 SafetyCheckClientsafety.prompt-check,配短超时 8 秒 + 至多 1 次重试

第二,它必须 fail-closed(失败时从严)。 这是一条方向性的安全判断:

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)承接的是收窄之后 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 作为一个线程内的局部变量,随着那个线程结束就消亡了,富数据再也找不回来。

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_payloadresult 抽出 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.firstPlayH_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 门仍然是机制层的硬地板,首局门只是在它之上叠一层更贴近真实体验的观测。

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)

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 契约并解耦并行工作
生成引擎主文档 "一句话怎么变成一款游戏"的生成主线全景(本文的上游)
生成运行时架构 生成主线的编排基建、六条不变量、两条 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",无旧名残留。