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

@@ -99,6 +99,8 @@ class ProjectSpecView(BaseModel):
logline: str | None = None
premise: str | None = None
theme: str | None = None
# 题材(每书稳定项)——供 assemble 注入 genre-aware craft 片段(灵感 F1
genre: str | None = None
# ---- Repository 协议async统一按 project_id 过滤)----

View File

@@ -11,6 +11,8 @@ import json
import uuid
from typing import Any
from ww_config import genre_craft_fragment
from ww_core.domain.injection_repo import InjectionOverride
from ww_core.domain.repositories import (
CharacterView,
@@ -31,6 +33,9 @@ from .types import AssembledContext, EntityKind
# 四级规则合并优先级global → genre → style → project越具体越靠后/越优先)
_LEVEL_RANK: dict[str, int] = {"global": 0, "genre": 1, "style": 2, "project": 3}
# 开篇黄金三章特判上限:前 N 章在 volatile 追加开篇提示(灵感①)。
OPENING_CHAPTER_LIMIT = 3
def _ser(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, sort_keys=True)
@@ -67,6 +72,16 @@ def _build_spec_section(spec: ProjectSpecView | None) -> str | None:
return _section("作品蓝本", "\n".join(lines))
def _build_genre_craft_section(genre: str | None) -> str | None:
"""按题材注入 craft 片段(语域/节奏/钩子/字数/套路,灵感 F1
genre 是每书稳定项(来自 spec→ 入缓存前缀 stable_core跨章字节稳定不变量 #9
命不中预设题材(自由填写/未设)则不注入。品类无关内核在 write_craft.md不在此处。
"""
fragment = genre_craft_fragment(genre)
return _section("本作题材写法", fragment) if fragment else None
def _build_stable(
spec: ProjectSpecView | None,
world_entities: list[WorldEntityView],
@@ -89,6 +104,8 @@ def _build_stable(
rule_lines = [f"[{r.level}] {r.content}" for r in merge_rules(rules)]
sections = [
# 题材写法领衔——同题材作品共享此前缀,利于跨书隐式前缀缓存(不变量 #9
_build_genre_craft_section(spec.genre if spec else None),
_build_spec_section(spec),
_section("世界观硬规则", "\n".join(world_lines)),
_section("定型主角", "\n".join(char_lines)),
@@ -98,6 +115,21 @@ def _build_stable(
return "\n\n".join(s for s in sections if s)
def _opening_marker(chapter_no: int) -> str | None:
"""开篇黄金三章特判(灵感①):前 N 章显式提示,不赌模型自读章号。
含章号 → 每章易变 → 只入 volatile断点后绝不进缓存前缀不变量 #9
"""
if chapter_no > OPENING_CHAPTER_LIMIT:
return None
return _section(
"开篇黄金三章",
f"本章为开篇第 {chapter_no} 章,适用黄金三章原则:"
"开篇即抛出核心冲突或身份钩子;三章之内交付第一个小爽点或转折;"
"避免大段世界观倾泻,边推进情节边带出设定。",
)
def _build_volatile(
chapter_no: int,
cards: str,
@@ -118,6 +150,8 @@ def _build_volatile(
_section("本章指令", directive.strip()) if directive and directive.strip() else None,
# 写章指令始终在场——保证 volatile断点后的 input永不为空修空 prompt 400
_section("写作指令", f"请创作第 {chapter_no} 章的正文。"),
# 开篇黄金三章标记(仅前 N 章)——每章易变,只入 volatile。
_opening_marker(chapter_no),
_section("本章注入卡片", cards),
_section("相关伏笔窗口", "\n".join(fore_lines)),
_section("近况摘要", "\n".join(digest_lines)),

View File

@@ -215,6 +215,7 @@ class SqlProjectSpecRepo:
logline=row.logline,
premise=row.premise,
theme=row.theme,
genre=row.genre,
)

View File

@@ -17,10 +17,15 @@ import uuid
from collections.abc import AsyncIterator
from typing import Protocol
from ww_agents import load_craft_doctrine
from ww_llm_gateway.types import Block, Delta, LlmRequest, Scope
from .state import ChapterState
# 写章 craft 教条灵感①品类无关内核import 期读盘 → 缺失即 fail-fast崩 import
# 字节稳定的稳定内容,作为最前的 cache=True system 块(跨书前缀复用,不变量 #9
_CRAFT_DOCTRINE = load_craft_doctrine()
class GatewayStream(Protocol):
"""write 节点对网关的最小依赖——只需 `stream`(注入真网关或 mock"""
@@ -37,11 +42,15 @@ def build_write_request(
) -> LlmRequest:
"""据组装上下文构造写章请求(纯函数,决策 2026-06-18
`stable_core` → `system` 缓存断点前块cache=True`volatile` → `input`(断点后)。
`system` = [craft 教条(cache), stable_core(cache)]——craft 品类无关内核在最前,
最大化跨书前缀复用两块均在缓存断点前cache=True。`volatile` → `input`(断点后)。
"""
return LlmRequest(
tier="writer",
system=[Block(text=stable_core, cache=True)],
system=[
Block(text=_CRAFT_DOCTRINE, cache=True),
Block(text=stable_core, cache=True),
],
input=volatile,
stream=True,
scope=Scope(user_id=user_id, project_id=project_id),