feat(llm): 整章重写预检澄清 spec + 教条 clarify_rewrite(WFW-9 M2)
镜像 clarify_refine(仅 name/prompt 不同,复用 ClarifyDecision,不新增 schema): 新增 clarify_rewrite_spec(analyst 档、reads=()/writes=() 只读,不变量 #2/#3)+ 章级 教条 prompts/clarify_rewrite.md(segment→整章:判作者章级重写意见是否含糊)。注册进 SPECS/SCHEMA_CATALOG,自检数量 24→25,__init__ 导出,金标准仅新增 1 条哈希无漂移。
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
"character-gen": "e0f97a5d7bb622fdd7582cdeef85ba13b3f434b2633f4d22a8c6daeba48636bf",
|
"character-gen": "e0f97a5d7bb622fdd7582cdeef85ba13b3f434b2633f4d22a8c6daeba48636bf",
|
||||||
"characterization": "643988cb899a66472acb3e18709f3299d2025f9bda132c774ee26c589266b51f",
|
"characterization": "643988cb899a66472acb3e18709f3299d2025f9bda132c774ee26c589266b51f",
|
||||||
"clarify_refine": "c2ed53c3a2d6c414c4ae5c92ead45634a56c47cd363a44f83ec1e020099480af",
|
"clarify_refine": "c2ed53c3a2d6c414c4ae5c92ead45634a56c47cd363a44f83ec1e020099480af",
|
||||||
|
"clarify_rewrite": "381864a21bfa43fa295236be981580369b3d1cbbdeb72ee10dffe52957110232",
|
||||||
"continue": "f1ce02be3b186fc966c89cf0d54ea57a400e91d435ea91f5722cb17e43d11763",
|
"continue": "f1ce02be3b186fc966c89cf0d54ea57a400e91d435ea91f5722cb17e43d11763",
|
||||||
"continuity": "1bdf9799bad9076e54de2023433ec7da7b559509d986392a0563dc1ee20cf4a1",
|
"continuity": "1bdf9799bad9076e54de2023433ec7da7b559509d986392a0563dc1ee20cf4a1",
|
||||||
"de-ai": "9ce3020cdd4b223cc4d13c289babd2811bbfece4b392711dcba4fd0301c87249",
|
"de-ai": "9ce3020cdd4b223cc4d13c289babd2811bbfece4b392711dcba4fd0301c87249",
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ from __future__ import annotations
|
|||||||
import pytest
|
import pytest
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from ww_agents import (
|
from ww_agents import (
|
||||||
|
SPECS,
|
||||||
AgentSpec,
|
AgentSpec,
|
||||||
ClarifyDecision,
|
ClarifyDecision,
|
||||||
ClarifyOption,
|
ClarifyOption,
|
||||||
ClarifyQuestion,
|
ClarifyQuestion,
|
||||||
clarify_refine_spec,
|
clarify_refine_spec,
|
||||||
|
clarify_rewrite_spec,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ---- ClarifyDecision / ClarifyQuestion / ClarifyOption schema ----
|
# ---- ClarifyDecision / ClarifyQuestion / ClarifyOption schema ----
|
||||||
@@ -123,3 +125,43 @@ def test_clarify_refine_spec_is_agent_spec_and_immutable() -> None:
|
|||||||
assert isinstance(clarify_refine_spec, AgentSpec)
|
assert isinstance(clarify_refine_spec, AgentSpec)
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
clarify_refine_spec.tier = "writer"
|
clarify_refine_spec.tier = "writer"
|
||||||
|
|
||||||
|
|
||||||
|
# ---- clarify_rewrite_spec 声明(WFW-9 M2:整章重写预检澄清,镜像 clarify_refine)----
|
||||||
|
|
||||||
|
|
||||||
|
def test_clarify_rewrite_spec_is_analyst_tier() -> None:
|
||||||
|
# 不变量 #2:整章含糊判别 + 反问构造用分析档,只声明 tier(不写 model)
|
||||||
|
assert clarify_rewrite_spec.tier == "analyst"
|
||||||
|
assert clarify_rewrite_spec.name == "clarify_rewrite"
|
||||||
|
|
||||||
|
|
||||||
|
def test_clarify_rewrite_spec_is_read_only() -> None:
|
||||||
|
# 不变量 #3:只读——不读库真相源(材料端点注入)、不写任何表
|
||||||
|
assert clarify_rewrite_spec.reads == ()
|
||||||
|
assert clarify_rewrite_spec.writes == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_clarify_rewrite_spec_output_schema_reuses_clarify_decision() -> None:
|
||||||
|
# 复用 ClarifyDecision(不新增 schema),与 refine 侧同构
|
||||||
|
assert clarify_rewrite_spec.output_schema is ClarifyDecision
|
||||||
|
|
||||||
|
|
||||||
|
def test_clarify_rewrite_spec_prompt_documents_constraints() -> None:
|
||||||
|
# v1 硬上限 1 问 + 自由输入兜底 + 防循环须在教条里显式约束(与 refine 侧同纪律)
|
||||||
|
prompt = clarify_rewrite_spec.system_prompt
|
||||||
|
assert prompt # 非空
|
||||||
|
assert "1 问" in prompt
|
||||||
|
assert "自由输入" in prompt
|
||||||
|
assert "防循环" in prompt
|
||||||
|
|
||||||
|
|
||||||
|
def test_clarify_rewrite_spec_registered_in_specs() -> None:
|
||||||
|
# 注册表同一实例(兼容期 *_spec 与 SPECS[name] 为同对象,不变量)
|
||||||
|
assert SPECS["clarify_rewrite"] is clarify_rewrite_spec
|
||||||
|
|
||||||
|
|
||||||
|
def test_clarify_rewrite_spec_is_agent_spec_and_immutable() -> None:
|
||||||
|
assert isinstance(clarify_rewrite_spec, AgentSpec)
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
clarify_rewrite_spec.tier = "writer"
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ def test_load_prompt_matches_golden(name: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
# ---- #1 注册表唯一性 ----
|
# ---- #1 注册表唯一性 ----
|
||||||
def test_specs_registry_len_is_24() -> None:
|
def test_specs_registry_len_is_25() -> None:
|
||||||
assert len(SPECS) == 24
|
assert len(SPECS) == 25
|
||||||
# name 即 key,dict 已去重;逐项确认 key == spec.name(无错位)
|
# name 即 key,dict 已去重;逐项确认 key == spec.name(无错位)
|
||||||
assert all(key == spec.name for key, spec in SPECS.items())
|
assert all(key == spec.name for key, spec in SPECS.items())
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ from .specs import (
|
|||||||
character_gen_spec,
|
character_gen_spec,
|
||||||
characterization_spec,
|
characterization_spec,
|
||||||
clarify_refine_spec,
|
clarify_refine_spec,
|
||||||
|
clarify_rewrite_spec,
|
||||||
continue_spec,
|
continue_spec,
|
||||||
continuity_spec,
|
continuity_spec,
|
||||||
de_ai_spec,
|
de_ai_spec,
|
||||||
@@ -141,6 +142,7 @@ __all__ = [
|
|||||||
"character_gen_spec",
|
"character_gen_spec",
|
||||||
"characterization_spec",
|
"characterization_spec",
|
||||||
"clarify_refine_spec",
|
"clarify_refine_spec",
|
||||||
|
"clarify_rewrite_spec",
|
||||||
"continue_spec",
|
"continue_spec",
|
||||||
"continuity_spec",
|
"continuity_spec",
|
||||||
"de_ai_spec",
|
"de_ai_spec",
|
||||||
|
|||||||
51
packages/agents/ww_agents/prompts/clarify_rewrite.md
Normal file
51
packages/agents/ww_agents/prompts/clarify_rewrite.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
你是长篇连载小说创作环节的「整章重写预检澄清器」(clarify-rewrite)——一位懂网文、会揣摩作者意图的资深责编。作者对**整整一章**不满意,想要「整章再沟通/重写」,并给出了章级的修改意见,但这条意见可能含糊、可能指向多种改法。你的唯一职责:在真正动笔重写整章**之前**,判断「这条意见清不清楚」,含糊就反问一个最关键的问题并给出几种具体走法供作者点选,清楚就确认理解、直接放行。
|
||||||
|
|
||||||
|
你不是重写器(绝不输出改后的整章正文),不是审校(不输出问题清单/评分),不写库。你只做一件事:产出一个结构化的「澄清决策」。
|
||||||
|
|
||||||
|
## 输入
|
||||||
|
|
||||||
|
注入材料为序列化文本,可能包含:
|
||||||
|
- **作者的整章修改意见/指令**(必有;正是触发反问的场景,如「节奏太慢」「人物不立」「不够爽」「高潮没起来」「感觉不对」,也可能更具体);
|
||||||
|
- **当前章节草稿的摘录**(可选;仅取开头一段供你锚定本章,用于判断意见指向本章的哪一层;**不是**要你逐句改它)。
|
||||||
|
|
||||||
|
材料会用小标题分块(如「作者的整章修改意见」「当前章节草稿(摘录)」)——据此分清哪段是意见、哪段是本章正文摘录。
|
||||||
|
|
||||||
|
## 判断:要不要反问
|
||||||
|
|
||||||
|
先想清楚——作者这条章级意见,能不能让重写器**无歧义地**重写整章?
|
||||||
|
|
||||||
|
**判定为「明确」(need_clarification=false)** 当且仅当满足其一:
|
||||||
|
- 意见具体、单一走法(如「把本章高潮提前到前半段」「删掉开头的回忆插叙、直接进冲突」「全章口语化,压缩景物描写」);
|
||||||
|
- 意见虽简短但结合本章只有一种合理改法;
|
||||||
|
- **历史里已经澄清过**(材料中已出现作者此前的选择/补充)——此时几乎永远不要再问,避免反复兜圈子。
|
||||||
|
|
||||||
|
**判定为「含糊」(need_clarification=true)** 当且仅当:
|
||||||
|
- 意见空缺或极短、无法定位整章要往哪改(如「再改改」「不满意」「重写一遍」);
|
||||||
|
- 存在多种明显不同的整章改法方向,选哪个会显著改变结果(如「节奏太慢」既可以是砍支线压缩篇幅、也可以是提前引爆冲突、还可以是删铺垫直入正题;「不够爽」既可以是加强主角高光、也可以是抬高对手威胁、还可以是加码情绪反转)。
|
||||||
|
|
||||||
|
拿不准时**倾向于放行**(need_clarification=false)——反问的成本是打断作者,只有真含糊才值得问。
|
||||||
|
|
||||||
|
## 产出纪律(硬约束)
|
||||||
|
|
||||||
|
### 明确时(need_clarification=false)
|
||||||
|
- `questions` 留空列表;
|
||||||
|
- `verification` 给**一句**「我这样理解对吗」式确认语,用你自己的话复述你打算把整章往哪个方向重写(让作者一眼看出理解是否到位)。不要复述原文,不要输出改后正文。
|
||||||
|
|
||||||
|
### 含糊时(need_clarification=true)
|
||||||
|
- `questions` **恰好 1 问**(硬上限,绝不超过一个问题);
|
||||||
|
- `question`:一句话,直指本章重写的那个分歧点;
|
||||||
|
- `options`:给 **2–4 个**锚定**本章**的**具体、可区分**走法。每个选项:
|
||||||
|
- `label` 是作者看到的简短文案;
|
||||||
|
- `value` 是选中后要落实的具体整章重写走法描述(会被折进重写意见,供重写器执行)——写成可直接照做的一句话,别写空泛的形容词。
|
||||||
|
- 选项之间必须**互斥、方向不同**,覆盖作者最可能想要的几种真实意图;不要凑数、不要近义重复。
|
||||||
|
- **凑不出**具体可区分的选项时(比如信息实在太少),把 `options` 留空列表,突出让作者自由输入——不要硬编三个含糊选项充数。
|
||||||
|
- `allow_free_text` 一律为 `true`:无论有没有选项,自由输入永远兜底(作者可以都不选、自己写)。
|
||||||
|
- `verification` 可留空(`null`)——含糊阶段还没到确认的时候。
|
||||||
|
|
||||||
|
## 防循环
|
||||||
|
|
||||||
|
如果注入材料里已经能看到作者此前的澄清选择或补充说明,说明这一轮沟通已经完成,**不要再问同样或类似的问题**——直接 `need_clarification=false`,用 `verification` 复述你综合后的整章重写理解,放行重写。反复反问是最糟的体验。
|
||||||
|
|
||||||
|
## 产出格式(严格)
|
||||||
|
|
||||||
|
只产出符合结构化 schema 的决策对象(`need_clarification` / `questions` / `verification`)。不要输出任何正文、解释、markdown 包裹或额外文字。选项要具体锚定本章,问题要短、要准、只问一个。
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Pydantic 类型承载结构化解析与 `isinstance` 校验,无法从文本推导、无法运行时安全构造,
|
Pydantic 类型承载结构化解析与 `isinstance` 校验,无法从文本推导、无法运行时安全构造,
|
||||||
必须留 Python。`SPECS[name].output_schema` 一律从这里派生(单向派生,避免双真相)。
|
必须留 Python。`SPECS[name].output_schema` 一律从这里派生(单向派生,避免双真相)。
|
||||||
`input_schema` 全 23 个恒为 None(入参为序列化文本),本波不建入参槽(YAGNI)。
|
`input_schema` 全 25 个恒为 None(入参为序列化文本),本波不建入参槽(YAGNI)。
|
||||||
|
|
||||||
`refiner` 是唯一纯文本 writer:`None` 是合法值(无结构化 schema)。
|
`refiner` 是唯一纯文本 writer:`None` 是合法值(无结构化 schema)。
|
||||||
"""
|
"""
|
||||||
@@ -65,6 +65,7 @@ SCHEMA_CATALOG: Final[dict[str, type[BaseModel] | None]] = {
|
|||||||
"teardown": BookTeardownResult,
|
"teardown": BookTeardownResult,
|
||||||
"project-plan": ProjectPlanResult,
|
"project-plan": ProjectPlanResult,
|
||||||
"clarify_refine": ClarifyDecision,
|
"clarify_refine": ClarifyDecision,
|
||||||
|
"clarify_rewrite": ClarifyDecision, # WFW-9 M2:整章重写预检,复用 ClarifyDecision
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ __all__ = [
|
|||||||
"character_gen_spec",
|
"character_gen_spec",
|
||||||
"characterization_spec",
|
"characterization_spec",
|
||||||
"clarify_refine_spec",
|
"clarify_refine_spec",
|
||||||
|
"clarify_rewrite_spec",
|
||||||
"continue_spec",
|
"continue_spec",
|
||||||
"continuity_spec",
|
"continuity_spec",
|
||||||
"de_ai_spec",
|
"de_ai_spec",
|
||||||
@@ -339,6 +340,20 @@ clarify_refine_spec = AgentSpec(
|
|||||||
scope="builtin",
|
scope="builtin",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ---- clarify_rewrite(整章重写预检澄清;分析档;纯只读 writes=(),WFW-9 M2 路线A 两阶段)----
|
||||||
|
# 镜像 clarify_refine:仅 name/prompt 不同,复用 ClarifyDecision(不新增 schema)。判「作者
|
||||||
|
# 这条章级重写意见清不清楚」,含糊则反问 1 问,清楚则确认放行;整章重写仍走既有 rewrite SSE 端点。
|
||||||
|
clarify_rewrite_spec = AgentSpec(
|
||||||
|
name="clarify_rewrite",
|
||||||
|
tier="analyst", # 不变量 #2:整章含糊判别 + 反问构造用分析档,不写 model
|
||||||
|
system_prompt=load_prompt("clarify_rewrite"),
|
||||||
|
input_schema=None, # 注入材料为序列化文本(章级意见 + 当前章草稿摘录),非结构化入参
|
||||||
|
output_schema=SCHEMA_CATALOG["clarify_rewrite"],
|
||||||
|
reads=(), # 只判本章意见是否含糊;材料经端点序列化注入,不直读库
|
||||||
|
writes=(), # 只读——只产问题/确认语,不改正文、不写库(不变量 #3)
|
||||||
|
scope="builtin",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# 集中注册表:name → spec(同一实例,兼容期 *_spec 与 SPECS[name] 为同对象,不变量)
|
# 集中注册表:name → spec(同一实例,兼容期 *_spec 与 SPECS[name] 为同对象,不变量)
|
||||||
# MappingProxyType:只读视图,运行时 `SPECS[x] = ...` / `del SPECS[x]` 抛 TypeError
|
# MappingProxyType:只读视图,运行时 `SPECS[x] = ...` / `del SPECS[x]` 抛 TypeError
|
||||||
@@ -370,9 +385,10 @@ _SPECS_BY_NAME: dict[str, AgentSpec] = {
|
|||||||
teardown_spec,
|
teardown_spec,
|
||||||
project_plan_spec,
|
project_plan_spec,
|
||||||
clarify_refine_spec,
|
clarify_refine_spec,
|
||||||
|
clarify_rewrite_spec,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
assert len(_SPECS_BY_NAME) == 24, "SPECS name 冲突或缺失" # 唯一性 + 数量自检(import 期)
|
assert len(_SPECS_BY_NAME) == 25, "SPECS name 冲突或缺失" # 唯一性 + 数量自检(import 期)
|
||||||
SPECS: Final[Mapping[str, AgentSpec]] = MappingProxyType(_SPECS_BY_NAME)
|
SPECS: Final[Mapping[str, AgentSpec]] = MappingProxyType(_SPECS_BY_NAME)
|
||||||
|
|
||||||
# 四审受信保留名 —— 独立显式白名单(安全边界锚在此,不依附派生集合)
|
# 四审受信保留名 —— 独立显式白名单(安全边界锚在此,不依附派生集合)
|
||||||
|
|||||||
Reference in New Issue
Block a user