feat(agents,core,config): 写章 craft 教条块 + genre-aware 片段 + 开篇黄金三章特判

- 新增 write_craft.md 品类无关写章 craft 内核(网文语域/show-don't-tell/对话 voice/
  反注水/结构性文字质感,灵活原则非穷举清单)+ load_craft_doctrine()(import 期读盘,
  不入 SPECS/金标准)
- build_write_request 把 craft 教条作为最前的 cache=True system 块注入(craft→stable_core,
  守缓存前缀 #9,tier=writer 不写 model)
- 新增 packages/config GENRE_CRAFT 题材片段表,assemble 按 project.genre 注入 stable_core;
  ProjectSpecView + SqlProjectSpecRepo 透传 genre
- 开篇前 3 章在 volatile 注入黄金三章提示(含章号→每章易变,不进缓存前缀)
- 修正 toolbox_registry 注释:11 条 → 15 条(legacy 3 + 新 8 + Scope B 4)
This commit is contained in:
Yaojia Wang
2026-07-06 14:29:43 +02:00
parent 6854dac98f
commit 452bd56d95
15 changed files with 379 additions and 9 deletions

View File

@@ -383,6 +383,73 @@ async def test_chapter_directive_uses_chapter_no() -> None:
assert "请创作第 7 章的正文。" in ctx.volatile
# ---- 灵感 F1genre-aware craft 片段注入 ----
async def test_genre_fragment_injected_by_genre() -> None:
from ww_config import GENRE_CRAFT
repos = build_repos(spec=ProjectSpecView(title="", genre="玄幻"))
ctx = await assemble(repos, PROJECT, 5)
# 玄幻题材片段进 stable_core每书稳定随书缓存不进 volatile。
assert GENRE_CRAFT["玄幻"] in ctx.stable_core
assert GENRE_CRAFT["玄幻"] not in ctx.volatile
async def test_different_genre_yields_different_fragment() -> None:
from ww_config import GENRE_CRAFT
xuan = await assemble(build_repos(spec=ProjectSpecView(title="", genre="玄幻")), PROJECT, 5)
city = await assemble(build_repos(spec=ProjectSpecView(title="", genre="都市")), PROJECT, 5)
assert GENRE_CRAFT["玄幻"] in xuan.stable_core
assert GENRE_CRAFT["都市"] in city.stable_core
assert xuan.stable_core != city.stable_core
async def test_unknown_genre_injects_no_fragment() -> None:
repos = build_repos(spec=ProjectSpecView(title="", genre="不存在的题材"))
ctx = await assemble(repos, PROJECT, 5)
assert "本作题材写法" not in ctx.stable_core
async def test_no_genre_injects_no_fragment() -> None:
repos = build_repos(spec=ProjectSpecView(title="")) # genre 默认 None
ctx = await assemble(repos, PROJECT, 5)
assert "本作题材写法" not in ctx.stable_core
# ---- 灵感①/F1开篇黄金三章特判volatile仅前 3 章) ----
async def test_opening_marker_injected_for_ch1to3_only() -> None:
repos = build_repos(spec=ProjectSpecView(title=""))
for chapter_no in (1, 2, 3):
ctx = await assemble(repos, PROJECT, chapter_no)
assert "黄金三章" in ctx.volatile
assert f"开篇第 {chapter_no}" in ctx.volatile
# 第 4 章起不再注入开篇标记。
ctx4 = await assemble(repos, PROJECT, 4)
assert "黄金三章" not in ctx4.volatile
async def test_opening_marker_never_enters_stable_core() -> None:
# 不变量#9开篇标记含章号每章易变→ 只入 volatile绝不进缓存前缀。
repos = build_repos(spec=ProjectSpecView(title="", genre="玄幻"))
ctx = await assemble(repos, PROJECT, 1)
assert "黄金三章" not in ctx.stable_core
async def test_no_per_chapter_varying_field_in_cached_blocks() -> None:
# 不变量#9同书不同章stable_core含 genre 片段)必须字节稳定;
# 章号 / 开篇标记等每章易变项只在 volatile。
repos = build_repos(spec=ProjectSpecView(title="", genre="玄幻"))
ch1 = await assemble(repos, PROJECT, 1)
ch5 = await assemble(repos, PROJECT, 5)
assert ch1.stable_core == ch5.stable_core # genre 片段随书稳定,跨章不变
assert "第 1 章" not in ch1.stable_core
assert "第 5 章" not in ch5.stable_core
# ---- helpers ----

View File

@@ -8,6 +8,7 @@ from __future__ import annotations
import uuid
from fakes_orchestrator import FakeStreamGateway, RaisingGateway
from ww_agents import load_craft_doctrine
from ww_core.orchestrator import (
EVENT_DONE,
EVENT_ERROR,
@@ -37,14 +38,58 @@ def test_build_write_request_puts_stable_core_in_cached_system_block() -> None:
assert req.tier == "writer" # 不变量②:只传档位,不传 model
assert req.stream is True
assert len(req.system) == 1
assert req.system[0].text == STABLE
assert req.system[0].cache is True # 不变量#9稳定内核进缓存断点前
# 灵感①system = [craft 教条(cache), stable_core(cache)]craft 在最前。
assert len(req.system) == 2
assert all(b.cache is True for b in req.system) # 不变量#9稳定内核进缓存断点前
assert req.system[1].text == STABLE
assert req.input == VOLATILE
assert req.scope.user_id == USER
assert req.scope.project_id == PROJECT
# ---- 灵感①:写章 craft 教条块 ----
def test_write_request_includes_craft_block() -> None:
req = build_write_request(
stable_core=STABLE, volatile=VOLATILE, user_id=USER, project_id=PROJECT
)
# craft 教条作为独立的 cache=True system 块注入。
assert req.system[0].text == load_craft_doctrine()
assert req.system[0].cache is True
def test_craft_precedes_stable_core() -> None:
req = build_write_request(
stable_core=STABLE, volatile=VOLATILE, user_id=USER, project_id=PROJECT
)
# craft 教条块必须在 stable_core 块之前(跨书前缀复用最大化)。
assert req.system[0].text == load_craft_doctrine()
assert req.system[1].text == STABLE
def test_craft_is_byte_stable() -> None:
# 不变量#9craft 块字节稳定——两次构造完全一致,无每章易变项掺入。
req1 = build_write_request(
stable_core=STABLE, volatile="请创作第 1 章的正文。", user_id=USER, project_id=PROJECT
)
req2 = build_write_request(
stable_core=STABLE, volatile="请创作第 99 章的正文。", user_id=USER, project_id=PROJECT
)
assert req1.system[0].text == req2.system[0].text # craft 块与章号无关
def test_no_per_chapter_field_in_cached_system_blocks() -> None:
# 不变量#9每章易变项章号只在 input(断点后),绝不进任何 cache=True 的 system 块。
volatile = "## 写作指令\n请创作第 5 章的正文。"
req = build_write_request(
stable_core=STABLE, volatile=volatile, user_id=USER, project_id=PROJECT
)
for block in req.system:
assert "第 5 章" not in block.text
assert "第 5 章" in req.input
# ---- stream_chapter_draft转发网关 Delta、构造正确请求 ----