From 78aa77a7e140de0e1a73994a35e1ce48e54f9340 Mon Sep 17 00:00:00 2001 From: zizi Date: Sun, 23 Aug 2026 22:24:00 +0800 Subject: [PATCH] =?UTF-8?q?=E9=93=BE=E8=B7=AF=E6=B8=85=E7=90=86:=20?= =?UTF-8?q?=E7=94=9F=E4=BA=A7=E5=86=99=E6=89=8B=E5=94=AF=E4=B8=80=E5=BD=A2?= =?UTF-8?q?=E6=80=81=E5=88=87=E6=8D=A2=E4=B8=BA=E4=B8=A4=E9=98=B6=E6=AE=B5?= =?UTF-8?q?=E6=B4=BE=E5=8F=91=E2=80=94=E2=80=94=E5=88=A0=E9=99=A4=E5=8D=95?= =?UTF-8?q?=E9=98=B6=E6=AE=B5=E6=B4=BE=E5=8F=91=E5=88=86=E6=94=AF=E3=80=81?= =?UTF-8?q?=E7=9B=B4=E8=B0=83=E7=BC=BA=E7=9C=81=E8=B7=AF=E5=BE=84=E4=B8=8E?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=97=97=E6=A0=87=EF=BC=88=E8=90=BD=E5=AE=9E?= =?UTF-8?q?=E5=AF=B9=E7=85=A7=E8=A3=81=E5=86=B3=EF=BC=89=EF=BC=8C=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=8F=82=E6=95=B0=E6=98=BE=E5=BC=8F=E5=BF=85=E5=A1=AB?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E4=B8=A4=E5=A4=84=E6=9E=B6=E6=9E=84=E8=BE=B9?= =?UTF-8?q?=E7=95=8C=E8=BF=9D=E8=A7=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../architecture/domains/05-创作流程领域.md | 2 +- .../scripts/judge_via_dispatch.py | 3 +- .agent/skills/write-next-chapter/SKILL.md | 6 +- .../scripts/produce_next_chapter.py | 144 ++++++------------ .../scripts/run_contrast_writer.py | 5 +- dashboard/server.py | 1 + 6 files changed, 50 insertions(+), 111 deletions(-) diff --git a/.agent/docs/architecture/domains/05-创作流程领域.md b/.agent/docs/architecture/domains/05-创作流程领域.md index 8a5c22e..ae62fda 100644 --- a/.agent/docs/architecture/domains/05-创作流程领域.md +++ b/.agent/docs/architecture/domains/05-创作流程领域.md @@ -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. 运行与落库 diff --git a/.agent/skills/score-content-quality/scripts/judge_via_dispatch.py b/.agent/skills/score-content-quality/scripts/judge_via_dispatch.py index 6f3c0f3..6af95a7 100644 --- a/.agent/skills/score-content-quality/scripts/judge_via_dispatch.py +++ b/.agent/skills/score-content-quality/scripts/judge_via_dispatch.py @@ -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)) diff --git a/.agent/skills/write-next-chapter/SKILL.md b/.agent/skills/write-next-chapter/SKILL.md index 8e3d2bf..b690d99 100644 --- a/.agent/skills/write-next-chapter/SKILL.md +++ b/.agent/skills/write-next-chapter/SKILL.md @@ -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/-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/-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-ch` / `writer-gen-work-ch`,复用=继续原写作智能体)。 - 授权继续:管线停在 `AUTHORIZATION_REQUIRED` 时留痕 `artifacts/-authorization-request.json`;人授权后以 `--continue-from <前序 run_id>` 发起新运行,编排方按缺口补证重组上下文后继续。 diff --git a/.agent/skills/write-next-chapter/scripts/produce_next_chapter.py b/.agent/skills/write-next-chapter/scripts/produce_next_chapter.py index 824e724..f3f5906 100644 --- a/.agent/skills/write-next-chapter/scripts/produce_next_chapter.py +++ b/.agent/skills/write-next-chapter/scripts/produce_next_chapter.py @@ -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

--model [--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,83 +428,39 @@ 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, - candidate_version=candidate_version, - repo_root=REPO_ROOT, - provider=dispatch_provider, - model=dispatch_model, - thinking=dispatch_thinking, - human_instruction=human_instruction, - spec_dir=ARTIFACTS, - ) - except ExplorationError as exc: - raise PipelineError(exc.code, f"两阶段写手失败: {exc}", - details=exc.details) from exc - explorations_by_version[candidate_version] = exploration - _dump(ARTIFACTS / f"{run_id}-exploration-summary-v{candidate_version}.json", - exploration) - 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 - 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 - 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')} 字),重抽……") - + try: + candidate, receipt, raw_ref, exploration = run_two_phase_writer( + current_context, + candidate_version=candidate_version, + repo_root=REPO_ROOT, + provider=dispatch_provider, + model=dispatch_model, + thinking=dispatch_thinking, + human_instruction=human_instruction, + spec_dir=ARTIFACTS, + ) + except ExplorationError as exc: + raise PipelineError(exc.code, f"两阶段写手失败: {exc}", + details=exc.details) from exc + explorations_by_version[candidate_version] = exploration + _dump(ARTIFACTS / f"{run_id}-exploration-summary-v{candidate_version}.json", + exploration) + print(f"两阶段写手: exploration_run={exploration['explorationRunId']} " + f"材料={exploration['materialCount']} 生成_run={exploration['generationRunId']}") + receipts_by_version[candidate_version] = receipt + candidates_by_version[candidate_version] = candidate + writer_raw_refs[candidate_version] = raw_ref + 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 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( diff --git a/.agent/skills/write-next-chapter/scripts/run_contrast_writer.py b/.agent/skills/write-next-chapter/scripts/run_contrast_writer.py index 8720157..b0deae3 100644 --- a/.agent/skills/write-next-chapter/scripts/run_contrast_writer.py +++ b/.agent/skills/write-next-chapter/scripts/run_contrast_writer.py @@ -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 diff --git a/dashboard/server.py b/dashboard/server.py index a028988..0d649a3 100644 --- a/dashboard/server.py +++ b/dashboard/server.py @@ -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

--model " f"--instruction \"改:{avoid}。禁止细纲原句抄进正文;门符号只在舰队医疗舱语境。\"" ) if candidates: