test(qa): craft 成稿质量前后对照评估回路(灵感 F2)

固定 3 题材金标准输入(仙侠/都市/悬疑,各含最小 spec+world+outline 及开篇章),
经真实 assemble()+build_write_request() 组装 craft-on / craft-off 两版写章请求:

- dry 模式(默认,零成本、免真实 LLM,可进 CI):断言 craft 教条块、genre 片段、
  开篇黄金三章 marker 确实按 fixture 注入正确内容,且 craft-off 基线不含这三者;
  并把两版将发送给模型的 prompt 并排 dump 供人眼看。这些断言即 tests/test_craft_eval.py,
  是 craft/审查注入路径的回归门禁。
- --live 模式(真实付费 LLM,绝不进 CI):经 gateway 各生成一章 + analyst 五维裁判,
  凭据读 DB、路由走 tier_routing,与生产写章同一网关路径。

入口 `uv run python -m tests.craft_eval [--live]`;用法/成本/门禁说明见 README.md。
This commit is contained in:
Yaojia Wang
2026-07-06 17:57:31 +02:00
parent 3d2a8b8cee
commit 51bfb6e320
7 changed files with 970 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
"""金标准输入 fixtures灵感 F2——3 个不同题材的最小 spec+world+outline。
每个 fixture 是一份**自造、不依赖 DB** 的最小设定:书级蓝本(含 genre+ 世界观硬规则 +
定型主角 + 本章大纲节拍。评估脚本据此组装 craft-on / craft-off 两版写章请求。
新增题材只需追加一个 `FixtureCase`genre 必须命中 `ww_config.GENRE_CRAFT` 才有片段可注)。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from ww_core.domain.repositories import CharacterView, ProjectSpecView, WorldEntityView
@dataclass(frozen=True)
class FixtureCase:
"""一份金标准写章输入(题材 / 章号 / 书级蓝本 / 世界观 / 主角 / 本章节拍)。"""
slug: str
genre: str
chapter_no: int
spec: ProjectSpecView
world: tuple[WorldEntityView, ...]
characters: tuple[CharacterView, ...]
beats: dict[str, Any] = field(default_factory=dict)
_XIANXIA = FixtureCase(
slug="xianxia",
genre="仙侠",
chapter_no=3, # 开篇章≤3——覆盖黄金三章 marker 注入
spec=ProjectSpecView(
title="逆脉证道",
genre="仙侠",
logline="废脉少年叶尘为救绝症妹妹寻回春灵药,滴血觉醒上古逆天功法,逆修九境搅动仙门风云。",
premise="天地灵气稀薄,修行看灵脉品阶;废脉者纳灵极慢,被视作修行绝路。",
theme="卑微者的不屈与救赎——修的是道,护的是人。",
tone="热血隐忍",
narrative_pov="第三人称限知",
ending_type="正剧",
),
world=(
WorldEntityView(
type="设定",
name="九境修为",
rules={
"境界": ["淬体", "开脉", "灵动", "纳灵", "凝魂", "化虚", "通玄", "渡劫", "证道"],
"每境": "分前中后三重",
},
),
WorldEntityView(
type="设定",
name="灵脉",
rules={"作用": "决定纳灵速度", "废脉": "纳灵极慢,被视作修行绝路"},
),
),
characters=(
CharacterView(
name="叶尘",
role="主角",
motive="为身患寒髓之症、命不久矣的妹妹叶暖寻回春灵药",
backstory="青云宗外门弟子,天生废脉、常年受人轻慢,性子沉静隐忍。",
speech_tics={"风格": "话少、点到为止,逼到绝境时语气反而更冷更稳"},
),
),
beats={
"summary": "叶尘被同门构陷逐出青云宗,独行荒山避雨于古洞,"
"滴血觉醒《逆脉真解》,一招震退追杀者。",
"entities": ["叶尘"],
"text": "叶尘 古洞 滴血 觉醒 逆脉真解 震退 追杀者",
},
)
_URBAN = FixtureCase(
slug="urban",
genre="都市",
chapter_no=1, # 开篇第一章
spec=ProjectSpecView(
title="重启我的黄金时代",
genre="都市",
logline="落魄程序员林川一夜重生回到二十年前,凭前世记忆在时代风口逆天改命。",
premise="重生回到世纪初互联网草莽年代,机会遍地、格局未定。",
theme="不甘平庸者抓住的第二次机会。",
tone="爽快励志",
narrative_pov="第一人称",
ending_type="HE",
),
world=(
WorldEntityView(
type="设定",
name="重生规则",
rules={
"回溯": "重生回到 2003 年的大学时代",
"记忆": "完整保留前世记忆与二十年行业趋势",
},
),
),
characters=(
CharacterView(
name="林川",
role="主角",
motive="改写前世潦倒猝死的结局,护住家人、抓住时代机遇翻身",
backstory="前世三十五岁猝死于出租屋的落魄外包程序员。",
speech_tics={"风格": "表面随和、心里门儿清,说话带点自嘲"},
),
),
beats={
"summary": "林川在出租屋猝死后睁眼回到 2003 年的大学宿舍,"
"认出这是重来一次的机会,立下第一个目标。",
"entities": ["林川"],
"text": "林川 重生 2003 大学宿舍 认清机会",
},
)
_SUSPENSE = FixtureCase(
slug="suspense",
genre="悬疑",
chapter_no=2, # 开篇第二章
spec=ProjectSpecView(
title="雾隐镇失踪案",
genre="悬疑",
logline="刑警周砚回到多雾的故乡小镇调查连环失踪案,线索却一步步指向自己尘封的童年。",
premise="常年浓雾笼罩的封闭小镇,二十年前也发生过同样的失踪,无人被起诉。",
theme="逼近真相与自我和解的代价。",
tone="冷峻压抑",
narrative_pov="第三人称限知",
ending_type="开放",
),
world=(
WorldEntityView(
type="设定",
name="雾隐镇",
rules={
"环境": "常年浓雾、信号时断时续",
"旧案": "二十年前发生过同类失踪,卷宗离奇缺页",
},
),
),
characters=(
CharacterView(
name="周砚",
role="主角",
motive="查清连环失踪真相,也为解开自己童年缺失的那段记忆",
backstory="市局刑警,幼年从雾隐镇被送走,对故乡的记忆断片。",
speech_tics={"风格": "话短、爱用反问,审讯时不动声色"},
),
),
beats={
"summary": "周砚勘查第二名失踪者的住所,"
"在墙缝里发现一张与二十年前旧案笔迹相同的纸条,心头一凛。",
"entities": ["周砚"],
"text": "周砚 现场勘查 墙缝 纸条 二十年前 旧案",
},
)
# 金标准题材集:仙侠 / 都市 / 悬疑(各含开篇章,覆盖黄金三章 marker
FIXTURES: tuple[FixtureCase, ...] = (_XIANXIA, _URBAN, _SUSPENSE)