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:
43
packages/agents/tests/test_craft_doctrine.py
Normal file
43
packages/agents/tests/test_craft_doctrine.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""写章 craft 教条加载器单测(灵感①)。
|
||||
|
||||
`load_craft_doctrine()` import 期读盘 `prompts/write_craft.md`(仿 load_prompt),
|
||||
主写章不在 SPECS、不进 prompt_hashes 金标准。文件缺失应 fail-fast(崩 import)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
|
||||
from ww_agents import load_craft_doctrine
|
||||
|
||||
|
||||
def _sha256(text: str) -> str:
|
||||
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def test_load_craft_doctrine_returns_nonempty_text() -> None:
|
||||
# Act
|
||||
doctrine = load_craft_doctrine()
|
||||
# Assert
|
||||
assert isinstance(doctrine, str)
|
||||
assert doctrine.strip()
|
||||
|
||||
|
||||
def test_load_craft_doctrine_is_byte_stable() -> None:
|
||||
# 稳定缓存前缀(不变量 #9):两次读盘字节完全一致。
|
||||
assert _sha256(load_craft_doctrine()) == _sha256(load_craft_doctrine())
|
||||
|
||||
|
||||
def test_craft_doctrine_covers_core_principles() -> None:
|
||||
# 品类无关内核应覆盖:show-don't-tell / 对话个性化 / 反注水(按情节功能分配字数)。
|
||||
doctrine = load_craft_doctrine()
|
||||
assert "反注水" in doctrine or "注水" in doctrine
|
||||
assert "对话" in doctrine
|
||||
assert "情节功能" in doctrine
|
||||
|
||||
|
||||
def test_craft_doctrine_is_genre_agnostic() -> None:
|
||||
# 品类无关内核不应写死具体题材名(题材片段归 GENRE_CRAFT)。
|
||||
doctrine = load_craft_doctrine()
|
||||
for genre in ("玄幻", "仙侠", "都市"):
|
||||
assert genre not in doctrine, f"品类无关内核不应含题材名 {genre!r}"
|
||||
@@ -58,9 +58,15 @@ def test_specs_registry_len_is_21() -> None:
|
||||
assert all(key == spec.name for key, spec in SPECS.items())
|
||||
|
||||
|
||||
# 非 SPEC 教条 prompt:主写章 craft 内核(灵感①)。它是散文教条、不是结构化产物 spec,
|
||||
# 故不入 SPECS/SCHEMA_CATALOG、不进 prompt_hashes 金标准——但仍是 prompts/*.md,需在
|
||||
# md↔spec↔catalog 一一对应校验里显式排除,否则集合等式误报。
|
||||
_DOCTRINE_STEMS = {"write_craft"}
|
||||
|
||||
|
||||
# ---- #2 md ↔ spec ↔ catalog 一一对应(覆盖 style.md / character-gen.md 连字符)----
|
||||
def test_md_spec_catalog_one_to_one() -> None:
|
||||
md_stems = {p.stem for p in PROMPTS_DIR.glob("*.md")}
|
||||
md_stems = {p.stem for p in PROMPTS_DIR.glob("*.md")} - _DOCTRINE_STEMS
|
||||
assert set(SPECS) == md_stems == set(SCHEMA_CATALOG)
|
||||
# 易错连字符 / 非变量名命名显式覆盖
|
||||
assert "style" in md_stems and "style_drift" not in md_stems
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .prompt_loader import PromptNotFoundError, load_prompt
|
||||
from .prompt_loader import PromptNotFoundError, load_craft_doctrine, load_prompt
|
||||
from .schema_catalog import SCHEMA_CATALOG, output_schema_for
|
||||
from .schemas import (
|
||||
Blurb,
|
||||
@@ -132,6 +132,7 @@ __all__ = [
|
||||
"foreshadow_spec",
|
||||
"glossary_spec",
|
||||
"golden_finger_spec",
|
||||
"load_craft_doctrine",
|
||||
"load_prompt",
|
||||
"name_spec",
|
||||
"opening_spec",
|
||||
|
||||
@@ -46,3 +46,18 @@ def load_prompt(name: str) -> str:
|
||||
|
||||
_CACHE[name] = text
|
||||
return text
|
||||
|
||||
|
||||
# 主写章 craft 教条文件名(灵感①)。它是散文教条、不是结构化产物 spec:
|
||||
# 不入 SPECS、不进 prompt_hashes 金标准、不参与计数断言——但复用同一读盘/规整/缓存路径,
|
||||
# 保证字节稳定(不变量 #9)。
|
||||
CRAFT_DOCTRINE_NAME: Final = "write_craft"
|
||||
|
||||
|
||||
def load_craft_doctrine() -> str:
|
||||
"""读 `prompts/write_craft.md`(品类无关写章 craft 内核)→ 规整 → 缓存 → 返回。
|
||||
|
||||
与 `load_prompt` 同一读盘/规整/缓存逻辑(DRY),文件缺失同样 fail-fast
|
||||
(`PromptNotFoundError`)。调用方在 import 期取常量即触发读盘。
|
||||
"""
|
||||
return load_prompt(CRAFT_DOCTRINE_NAME)
|
||||
|
||||
32
packages/agents/ww_agents/prompts/write_craft.md
Normal file
32
packages/agents/ww_agents/prompts/write_craft.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# 写章创作纪律(品类无关内核)
|
||||
|
||||
你是一位深耕中文网络小说的职业写手。下面是贯穿所有题材的创作纪律——它们是**灵活的原则**,不是逐条打勾的清单。按当下这一章真正要写的情节去取舍运用,不要机械套用、不要为了满足某条规则而牺牲叙事本身。
|
||||
|
||||
## 语域总纲
|
||||
|
||||
- 网文是**读起来快**的文体:信息密度偏低、句子偏短、段落偏碎。读者靠画面与动作往前滑,而不是靠密集的说明与铺陈。
|
||||
- **重行动、轻独白**:优先用发生的事推进故事,少写大段静止的心理分析或情绪反刍。心理活动点到即止,让位于外部动作、对话与场景。
|
||||
- **情节持续推进**:每个场景都要往前走一步——带来新信息、新冲突、新关系变化或新目标。避免原地打转的交代和没有推进的过场堆叠。
|
||||
|
||||
## show,别 tell
|
||||
|
||||
- 把情绪**外化**为可见可感的东西:动作、姿态、生理反应、环境细节、对话里的停顿与语气,而不是直接抛出「他很愤怒」「她很紧张」这类概括标签。
|
||||
- 让读者从画面里**自己推断**人物的状态,而不是由叙述者代为总结。越关键的情绪,越要用细节演出来,而不是一句话说破。
|
||||
|
||||
## 对话的声音
|
||||
|
||||
- 每个角色的说话方式要**可区分**:依据角色档案里的性格、出身、处境,赋予各自的用词习惯、句子长短、口头禅与说话节奏。
|
||||
- **忌所有人一个腔调**——反派、少年、长者、市井之徒不该共用同一套措辞与句式。对话要能让读者仅凭语气就认出是谁在说。
|
||||
- 对话标签(「他说」「她道」)的**位置要变化**:时而前置、时而后置、时而用一个动作 beat 代替,避免每句都以同一模式收尾。
|
||||
|
||||
## 反注水:按情节功能分配篇幅
|
||||
|
||||
- 字数**跟着情节功能走**,不是往一个固定目标硬凑。关键的冲突、转折、情绪高点要写**厚**——给足动作分解、感官细节与张力;过场、赶路、次要交代要写**薄**——一两句带过。
|
||||
- 密点厚、疏点薄。**绝不为凑字数注水**:删掉重复的心理复述、冗余的形容词堆叠、无推进的对话与说明。
|
||||
- 篇幅是结果而非目标——先想清楚这一章每个部分承担什么功能,再决定它值多少笔墨。
|
||||
|
||||
## 文字质感
|
||||
|
||||
- **句长错落**:长短句交替,避免连续同长度、同结构的句子带来的机械节奏。
|
||||
- **避免用情绪概括收尾**:段落或章节的结尾不要落在「他心中五味杂陈」「一切都不一样了」这类抽象总结上,用具体的动作、画面或悬念收束。
|
||||
- **慎用连续排比与套路化修辞**:偶一为之可以蓄势,密集堆砌则显空洞、露出 AI 腔。宁可朴素准确,不要华而不实。
|
||||
Reference in New Issue
Block a user