链路清理: 生产写手唯一形态切换为两阶段派发——删除单阶段派发分支、直调缺省路径与相关旗标(落实对照裁决),模型参数显式必填,修两处架构边界违规

This commit is contained in:
zizi 2026-08-23 22:24:00 +08:00
parent cc23bab482
commit 78aa77a7e1
6 changed files with 50 additions and 111 deletions

View File

@ -264,7 +264,7 @@ DRAFT/CHECKING/PASSED -> DISCARDED(用户明确决策)
当前生产编排实现(`run_writer_pipeline`)的事实:机械门 → 语义检测 → 单次收敛、证据缺口收敛为授权终态(AUTHORIZATION_REQUIRED,补证/重写经人授权后以新运行继续)、CAS 乐观锁 + 失败收敛终态、生成篇幅与机械接受底线分层合同、评测候选四层强制;业务继续/停止的授权归人,技术保护(超时、预算、取消)在执行策略层。生产写手收到 4000–7000 汉字(目标 7000),可信适配器只拒绝不满 3001 汉字或超过 10000 汉字的异常输出;两层区间由 WriterContext 机械校验保持嵌套。
写手执行的生产验证形态是两阶段写手(`produce_next_chapter.py --two-phase`):探索阶段派发写作智能体(只读工具集)自主取材(细纲、前文、文风、范式、实体)、产出探索清单;确定性回放按清单重放只读工具(无模型参与),得到写手实际依赖的材料;生成阶段无工具派发、单次成稿,生成输入只含回放材料与篇幅/文风合同,不含全量预组装。探索与生成使用独立会话、各记独立派发运行,写手事实只来自它探索到的材料;探索清单与摘要落运行工件,探索轨迹与依赖清单进事件账本,是创作台链路透视的数据源。探索与生成分离的原因:单阶段同会话形态下,长篇生成会被多回合探索挤占输出空间导致正文碎片化(2026-08-23 烟测实证)。该形态已于 2026-08-23 生产烟测双门通过并经人采纳进正典;同源对照实验裁决直调链退出创作生成(见 `07-Agent与Skill领域`)。
写手执行的唯一生产形态是两阶段写手(`produce_next_chapter.py <章> --provider P --model M`):探索阶段派发写作智能体(只读工具集)自主取材(细纲、前文、文风、范式、实体)、产出探索清单;确定性回放按清单重放只读工具(无模型参与),得到写手实际依赖的材料;生成阶段无工具派发、单次成稿,生成输入只含回放材料与篇幅/文风合同,不含全量预组装。探索与生成使用独立会话、各记独立派发运行,写手事实只来自它探索到的材料;探索清单与摘要落运行工件,探索轨迹与依赖清单进事件账本,是创作台链路透视的数据源。探索与生成分离的原因:单阶段同会话形态下,长篇生成会被多回合探索挤占输出空间导致正文碎片化(2026-08-23 烟测实证)。该形态已于 2026-08-23 生产烟测双门通过并经人采纳进正典;同源对照实验裁决直调链退出创作生成(见 `07-Agent与Skill领域`)。
## 6. 运行与落库

View File

@ -22,8 +22,7 @@ from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parents[3]
for _path in (SCRIPT_DIR,
REPO_ROOT / ".agent" / "skills" / "record-run-evidence" / "scripts",
REPO_ROOT / ".agent" / "skills" / "access-database" / "scripts"):
REPO_ROOT / ".agent" / "skills" / "record-run-evidence" / "scripts"):
if str(_path) not in sys.path:
sys.path.insert(0, str(_path))

View File

@ -40,10 +40,8 @@ Writer 不接收 `runId`、权限信息、manifest、hash、候选版本、验
Dashboard 与人工生产入口统一调用 `scripts/produce_next_chapter.py`。该入口只负责编排本 Skill 已登记的冻结、检索、writer、detector、CAS、候选落库和人闸步骤;不提供自动 accept。运行 artifacts 仍由只读看板按 run_id 读取。
- 写作节点有三种执行模式(写手合同相同,产出同为 `candidateBody` 信封):
- `--two-phase --provider P --model M [--thinking T]`:**已验证的生产形态**(2026-08-23 候选 164 双门通过并经人采纳入正典)。两阶段写手(`scripts/two_phase_writer.py`):探索阶段写作智能体带只读工具自主取材并产出探索清单;确定性回放按清单重放只读工具(无模型参与)得到写手实际依赖资料;生成阶段写作智能体无工具、单条成稿,生成输入只含回放资料与篇幅/文风合同,不含预组装上下文。探索与生成各记独立派发运行,探索摘要落 `artifacts/<run_id>-exploration-summary.json`。
- `--dispatch-writer`:单阶段派发(探索与生成同会话的过渡形态,已被两阶段取代,属链路清理对象)。
- 缺省:直调链 `run_writer_with_receipt`(对照裁决后只用于对照实验与回放评测,不再是创作生成的生产形态)。
- 写作节点的唯一执行形态是两阶段写手(写手合同不变,产出为 `candidateBody` 信封):`produce_next_chapter.py <章> --provider P --model M [--thinking T] [--instruction ...]`。探索阶段写作智能体带只读工具自主取材并产出探索清单;确定性回放按清单重放只读工具(无模型参与)得到写手实际依赖资料;生成阶段写作智能体无工具、单条成稿,生成输入只含回放资料与篇幅/文风合同,不含预组装上下文(`scripts/two_phase_writer.py`)。探索与生成各记独立派发运行,探索摘要落 `artifacts/<run_id>-exploration-summary.json`;2026-08-23 候选 164 以该形态双门通过并经人采纳入正典。
- `provider`/`model` 必须显式传入(模型策略归角色合同与调用方,不从环境变量推断);直调链已按 2026-08-23 对照裁决退出创作生成,对照实验走独立入口 `scripts/run_contrast_writer.py`(产 `run_type=eval` 候选,四层机械不可接受)。
- 派发链的模型证据记在派发运行下,生产候选账本以显式 `writer_raw_ref` 关联,不双套记账;一章一组稳定会话(`writer-explore-work<W>-ch<C>` / `writer-gen-work<W>-ch<C>`,复用=继续原写作智能体)。
- 授权继续:管线停在 `AUTHORIZATION_REQUIRED` 时留痕 `artifacts/<run_id>-authorization-request.json`;人授权后以 `--continue-from <前序 run_id>` 发起新运行,编排方按缺口补证重组上下文后继续。

View File

@ -6,15 +6,17 @@
→ build_retrieval_plan + retrieve_writer_sources(生产仓储;新书无卡诚实返空)
→ assemble_context 冻结 WriterContext v1
→ run_writer_pipeline(持久 CAS 状态链 + 机械门 + 语义 detector,单次收敛 + 授权终态合同)
writer 真调经 run_writer_with_receipt:runtime 自动把模型输入/输出原文与调用明细原子落库;
篇幅越界在 writer 适配层自动重抽(最多三遍);语义 detector 走冻结 detector profile 真调;
writer 经两阶段框架派发(探索取材 → 单次成稿):模型证据记派发运行,生产候选账本以
writer_raw_ref 显式关联,不双套记账;语义 detector 走冻结 detector profile 真调;
语义 needs_evidence 时走 production_evidence_reassemble(检索已有正典摘录);零命中当新设定交人闸,不禁写不重写
→ persist_writer_execution(冻结 + 运行注册 + 候选[含语义状态] + 回执 + 机械/语义质量证据一次落库)
→ accept_preflight(check_writer_acceptance 纯函数 + acceptance_state 实时重读)
→ 停止并展示候选,等待用户明确选择改 / 丢弃 / 采纳
用法:.venv/bin/python .agent/skills/write-next-chapter/scripts/produce_next_chapter.py [目标章号]
[--instruction "本轮人指令原文"]
--provider <P> --model <M> [--thinking T] [--instruction "本轮人指令原文"]
写手唯一执行形态是两阶段框架派发(2026-08-23 对照裁决:直调链退出创作生成);
--provider/--model 必须显式给出,不从环境变量推断。
缺省写下一章(库内最大章序 +1)。前置:目标章已建立、存在 confirmed 细纲,且门锚合同
GATE_ANCHORS 已登记该章。
--instruction:写入本轮输入第 7 项(人指令),经 styleConstraints 注入 writer;
@ -59,9 +61,8 @@ from retrieve_writer_sources import ( # noqa: E402
load_confirmed_pattern_bindings, load_confirmed_style,
)
from run_writer import ( # noqa: E402
WriterAdapterError, build_writer_execution_profile,
build_production_length_contracts, calculate_dynamic_output_contract,
run_writer_with_receipt,
build_production_length_contracts, build_writer_execution_profile,
calculate_dynamic_output_contract,
)
from run_writer_pipeline import PipelineError, run_writer_pipeline # noqa: E402
from production_evidence_reassemble import ( # noqa: E402
@ -82,14 +83,12 @@ from muse_role import ( # noqa: E402
from run_writer_replay import profile_from_mapping # noqa: E402
from persist_llm_call import persist_call as persist_llm_event # noqa: E402
from persist_writer_run import persist_writer_execution # noqa: E402
from dispatch_writer_bridge import DispatchWriterError, run_writer_via_dispatch # noqa: E402
from two_phase_writer import ExplorationError, run_two_phase_writer # noqa: E402
from run_registry import finish_run, start_run # noqa: E402
from check_writer_acceptance import AcceptanceError, check_writer_acceptance # noqa: E402
from acceptance_state import LiveStateError, build_live_acceptance_state # noqa: E402
WORK_ID = 12
MAX_ATTEMPTS = 3 # 篇幅越界重抽上限(编排层响应,对齐 replay 自纠环语义)
GATE_A_CONFIG = json.loads((SKILLS / "replay-writer-gate" / "configs" /
"writer-gate-a-deep-space-v1.json").read_text(encoding="utf-8"))
@ -288,19 +287,13 @@ def main():
argv = argv[:idx] + argv[idx + 2:]
return value
dispatch_mode = "--dispatch-writer" in argv
if dispatch_mode:
argv = [arg for arg in argv if arg != "--dispatch-writer"]
two_phase_mode = "--two-phase" in argv
if two_phase_mode:
argv = [arg for arg in argv if arg != "--two-phase"]
dispatch_mode = True
# 写手唯一执行形态是两阶段框架派发(2026-08-23 对照裁决);模型参数必须显式传入。
dispatch_provider = _take("--provider")
dispatch_model = _take("--model")
dispatch_thinking = _take("--thinking")
continue_from = _take("--continue-from")
if dispatch_mode and (not dispatch_provider or not dispatch_model):
raise SystemExit("--dispatch-writer 必须显式给出 --provider 与 --model")
if not dispatch_provider or not dispatch_model:
raise SystemExit("生产写手必须显式给出 --provider 与 --model(两阶段框架派发)")
args = [arg for arg in argv if not arg.startswith("--")]
target = resolve_target_chapter(int(args[0]) if args else None)
as_of = target - 1
@ -412,14 +405,7 @@ def main():
f"范式绑定 {len(pattern_references)} 张,"
f"文风约束 {len(style_constraints)} 条")
# 4) 生产 pipeline:持久 CAS + writer 真调 + 机械门 + 语义 detector(先审后入)
writer_profile = build_writer_execution_profile(
max_budget_usd_per_call=Decimal("5.0"),
timeout_seconds=1200,
max_context_chars=token_budget["maxContextChars"],
system_prompt=SYSTEM_PROMPT,
system_prompt_id=SYSTEM_PROMPT_ID,
)
# 4) 生产 pipeline:持久 CAS + 两阶段写手派发 + 机械门 + 语义 detector(先审后入)
detector_profile = build_semantic_detector_profile()
semantic_runner = ProductionSemanticRunner(detector_profile, run_id=run_id)
state_store = PostgresCasStateStore(
@ -442,11 +428,9 @@ def main():
print(f"AI 味诊断(v{candidate_version}):发现 {len(deai_artifact['findings'])} 条")
def production_writer(current_context: Mapping[str, Any], candidate_version: int):
"""writer 适配:直调链篇幅越界自动重抽(≤3 遍);派发链失败关闭。"""
"""writer 适配:两阶段框架派发是唯一形态,探索取材后单次成稿,失败关闭。"""
contexts_by_attempt[current_context["attempt"]] = dict(current_context)
if dispatch_mode:
if two_phase_mode:
try:
candidate, receipt, raw_ref, exploration = run_two_phase_writer(
current_context,
@ -464,23 +448,8 @@ def main():
explorations_by_version[candidate_version] = exploration
_dump(ARTIFACTS / f"{run_id}-exploration-summary-v{candidate_version}.json",
exploration)
print(f"两阶段写手模式: exploration_run={exploration['explorationRunId']} "
print(f"两阶段写手: exploration_run={exploration['explorationRunId']} "
f"材料={exploration['materialCount']} 生成_run={exploration['generationRunId']}")
else:
try:
candidate, receipt, raw_ref = run_writer_via_dispatch(
current_context,
candidate_version=candidate_version,
repo_root=REPO_ROOT,
provider=dispatch_provider,
model=dispatch_model,
thinking=dispatch_thinking,
human_instruction=human_instruction,
spec_path=ARTIFACTS / f"{run_id}-writer-task-v{candidate_version}.json",
)
except DispatchWriterError as exc:
raise PipelineError(exc.code, f"writer 派发失败: {exc}",
details=exc.details) from exc
receipts_by_version[candidate_version] = receipt
candidates_by_version[candidate_version] = candidate
writer_raw_refs[candidate_version] = raw_ref
@ -491,34 +460,7 @@ def main():
"AI_FLAVOR_DIAGNOSIS_FAILED", "候选 AI 味诊断或落库失败",
details={"errorType": type(exc).__name__, "message": str(exc)},
) from exc
print(f"writer 派发模式: dispatch_run={receipt.dispatch_run_id}")
return candidate
for retry in range(1, MAX_ATTEMPTS + 1):
try:
candidate, receipt = run_writer_with_receipt(
current_context,
profile=writer_profile,
candidate_version=candidate_version,
run_id=run_id,
caller="writer",
persist_call=persist_llm_event,
)
receipts_by_version[candidate_version] = receipt
candidates_by_version[candidate_version] = candidate
try:
_diagnose_candidate(candidate["candidateBody"], candidate_version)
except Exception as exc:
raise PipelineError(
"AI_FLAVOR_DIAGNOSIS_FAILED", "候选 AI 味诊断或落库失败",
details={"errorType": type(exc).__name__, "message": str(exc)},
) from exc
return candidate
except WriterAdapterError as exc:
if exc.code != "candidate_length_out_of_range" or retry == MAX_ATTEMPTS:
raise PipelineError(exc.code, f"writer 真调失败: {exc}",
details=exc.details) from exc
print(f" 第{retry}遍越界({exc.details.get('actualHanChars')} 字),重抽……")
def production_semantic_detector(current_context, candidate, mechanical_report):
"""语义 detector 适配:构造冻结输入、真调模型、留档输入输出。"""
@ -586,7 +528,7 @@ def main():
trigger_detail={"stage": "writer-production-pipeline",
"contextSha256": writer_context["contextSnapshot"]["contextSha256"],
**({"continueFrom": continue_from} if continue_from else {}),
**({"writerMode": "dispatch"} if dispatch_mode else {})},
"writerMode": "two-phase"},
creator="continuation")
try:
pipeline_result = run_writer_pipeline(

View File

@ -29,8 +29,7 @@ SCRIPT_DIR = Path(__file__).resolve().parent
for _path in (SCRIPT_DIR,
SCRIPT_DIR.parents[1] / "assemble-context" / "scripts",
SCRIPT_DIR.parents[1] / "record-run-evidence" / "scripts",
SCRIPT_DIR.parents[1] / "check-content-consistency" / "scripts",
SCRIPT_DIR.parents[1] / "access-database" / "scripts"):
SCRIPT_DIR.parents[1] / "check-content-consistency" / "scripts"):
if str(_path) not in sys.path:
sys.path.insert(0, str(_path))
@ -39,8 +38,8 @@ from produce_next_chapter import ( # noqa: E402
GATE_ANCHORS,
SYSTEM_PROMPT,
SYSTEM_PROMPT_ID,
build_writer_execution_profile,
)
from run_writer import build_writer_execution_profile # noqa: E402
from writer_contract import build_candidate_envelope, validate_writer_draft # noqa: E402
from muse_role import run_role # noqa: E402
from muse_db import connect # noqa: E402

View File

@ -2498,6 +2498,7 @@ def _run_decision_menu_panel(run_id: str, candidates: list, terminal_state, pipe
avoid = ";".join(f"避开「{h}」" for h in gap_hints[:2]) if gap_hints else "按语义/诊断理由修改"
rewrite_cmd = (
f".venv/bin/python .agent/skills/write-next-chapter/scripts/produce_next_chapter.py 3 "
f"--provider <P> --model <M> "
f"--instruction \"改:{avoid}。禁止细纲原句抄进正文;门符号只在舰队医疗舱语境。\""
)
if candidates: