muse-agent-example/docs/write-chapter/step2a_write_chapter1.py
zizi 091b66a9bb 重构: 收敛 Agent/Skill 运行时与创作质量闭环
将角色与 Skill 从 .claude 迁入 .agent,移除 Claude CLI 运行时并接入固定 Opus 角色 profile、完整 schema、预算 deadline、raw 与回执证据链。

同步拆分 Skill 职责、复利 lesson、Gate 回放、Dashboard 人审入口、数据库登记和机械门禁;候选设计正文不包含在本提交中。
2026-08-22 02:12:32 +08:00

213 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""用 prompt 管理 writer 角色生成第 1 章基线候选。
第 1 章没有合法 asOf,不能构造完整 WriterContext,因此本脚本直接把冻结的
WriterCreativeInput 交给 muse_role。角色 prompt、schema、预算、上下文上限和模型策略由
RoleExecutionProfile 绑定;调用回执和候选仍落既有证据表。第 1 章不伪造机械/语义门通过,
正式采纳仍受 write_canonical 的实时状态门约束。
"""
import hashlib
import json
import sys
import uuid
from datetime import datetime
from decimal import Decimal
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
AGENT_ROOT = SCRIPT_DIR.parents[1]
SKILLS = AGENT_ROOT / ".agent" / "skills"
for sub in (
"write-next-chapter/scripts",
"decide-candidate/scripts",
"assemble-context/scripts",
):
p = str(SKILLS / sub)
if p not in sys.path:
sys.path.insert(0, p)
from muse_db import connect # noqa: E402
from muse_role import run_role # noqa: E402
from run_writer import build_writer_execution_profile # noqa: E402
from writer_contract import han_count, normalize_text, validate_writer_draft # noqa: E402
from write_canonical import accept # noqa: E402
WORK_ID = 12
TARGET_CHAPTER = 1
# 冻结 writer 提示词:从 replay-eval config 读(不复制粘贴,sha256 可与配置核对)
GATE_A_CONFIG_PATH = (
SKILLS
/ "replay-writer-gate"
/ "configs"
/ "writer-gate-a-deep-space-v1.json"
)
GATE_A_WRITER_PROFILE = json.loads(GATE_A_CONFIG_PATH.read_text(encoding="utf-8"))[
"executionProfiles"]["writer"]
SYSTEM_PROMPT = GATE_A_WRITER_PROFILE["systemPrompt"]
SYSTEM_PROMPT_ID = GATE_A_WRITER_PROFILE["systemPromptId"]
SYSTEM_PROMPT_SHA256 = "sha256:" + hashlib.sha256(SYSTEM_PROMPT.encode("utf-8")).hexdigest()
assert SYSTEM_PROMPT_SHA256 == GATE_A_WRITER_PROFILE["systemPromptSha256"], \
"config 里的 systemPrompt 与其冻结 sha256 不一致,停止(事实来源已漂移)"
ARTIFACTS_DIR = SCRIPT_DIR / "artifacts"
# 第 1 章细纲硬骨架(与库内 planning section id=5 confirmed payload 同源)
HARD_CONSTRAINTS = [
"主角是天才机师,初次驾驶生物机甲。",
"深渊异族(克苏鲁式)首次出现,营造微恐/精神压迫。",
"建立黑暗森林/舰队/巨舰的世界观基调。",
"章末钩子:主角觉醒生物机甲的代价(精神污染的预兆)。",
]
ADJUSTABLE_BEATS = [
"开场节奏(侦察船日常 → 异常信号 → 异族现身)可调整。",
"精神压迫的描写强度可调整(微恐基调)。",
]
DECLARED_NEW_FACTS = [
{"factId": "fact-ch1-1", "text": "主角林深:天才机师。"},
{"factId": "fact-ch1-2", "text": "生物机甲:以异种核心驱动,机师与机甲精神链接。"},
{"factId": "fact-ch1-3", "text": "深渊异族:克苏鲁式,造成精神污染。"},
]
# 篇幅合同:细纲显式 targetChars=4000,区间按生产算法 = 目标值 ±30%(硬边界 2000-10000)
LENGTH_CONTRACT = {"targetChars": 4000, "minChars": 2800, "maxChars": 5200,
"frontmatterRequired": False}
# 文风约束:投影自设定包 style 字段(planning section id=3)。第 1 章无历史原文摘录,
# 文风画像是唯一文风锚点;生产投影从第 2 章起有 proseEvidence,本字段可为空。
STYLE_TEXT = ("冷峻、克制、微恐。战斗场面宏大(舰队/巨舰/机甲),心理描写细腻"
"(克苏鲁式精神压迫)。句式偏短,节奏紧。")
# 创作输入——严格按 build_writer_creative_input 的生产投影形状
CREATIVE_INPUT = {
"fineOutline": {
"hardConstraints": HARD_CONSTRAINTS,
"adjustableBeats": ADJUSTABLE_BEATS,
"declaredNewFacts": DECLARED_NEW_FACTS,
},
"narrativeState": {
"time": "故事开篇",
"location": "深渊星域,人类舰队侦察船",
"characterPositions": {"林深": "侦察船"},
"immediateSituation": "侦察船在深渊星域捕获异常信号,未知存在逼近。",
},
"factConstraints": [],
"proseExcerpts": [],
"patternReferences": [],
"lengthContract": LENGTH_CONTRACT,
"styleConstraints": [{"styleId": "style-baseline-v1", "text": STYLE_TEXT}],
}
def main():
run_id = f"run-prod-work12-ch1-{uuid.uuid4().hex[:8]}"
ARTIFACTS_DIR.mkdir(exist_ok=True)
(ARTIFACTS_DIR / f"{run_id}-creative-input.json").write_text(
json.dumps(CREATIVE_INPUT, ensure_ascii=False, indent=1), encoding="utf-8")
profile = build_writer_execution_profile(
max_budget_usd_per_call=Decimal("5.0"),
timeout_seconds=1200,
max_context_chars=200000,
system_prompt=SYSTEM_PROMPT,
system_prompt_id=SYSTEM_PROMPT_ID,
)
print(
f"writer 角色调用开始: run_id={run_id}, 模型策略={profile.model_alias}, "
f"提示词={SYSTEM_PROMPT_ID}"
)
started = datetime.now()
invocation = run_role(
profile,
CREATIVE_INPUT,
business_validator=validate_writer_draft,
run_id=run_id,
caller="writer",
)
finished = datetime.now()
receipt = invocation.receipt
body = normalize_text(invocation.structured_output["candidateBody"])
actual_han = han_count(body)
candidate_sha256 = hashlib.sha256(body.encode("utf-8")).hexdigest()
print("── 回执 ──")
print(f" actual_model_id: {receipt.actual_model_id} model_match: {receipt.model_match}")
print(f" terminal_reason: {receipt.terminal_reason} stop_reason: {receipt.stop_reason} "
f"is_error: {receipt.is_error} exit_code: {receipt.exit_code}")
print(f" total_cost_usd: {receipt.total_cost_usd} usage: "
f"{json.dumps(dict(receipt.usage or {}), ensure_ascii=False)[:160]}")
print(f"── 候选 ──\n 汉字数: {actual_han}(合同 {LENGTH_CONTRACT['minChars']}"
f"-{LENGTH_CONTRACT['maxChars']},目标 {LENGTH_CONTRACT['targetChars']})")
print(f" sha256: {candidate_sha256[:16]}...")
print(f" 正文前 120 字: {body[:120]}")
# 落库前硬门:篇幅不出界才落库(3 字级垃圾不许入库污染看板)
if not LENGTH_CONTRACT["minChars"] <= actual_han <= LENGTH_CONTRACT["maxChars"]:
print(f"[拒绝落库] 汉字数 {actual_han} 超出篇幅合同,候选只留档 artifacts 不入库")
raise SystemExit(1)
if not receipt.model_match:
print("[拒绝落库] 实际模型不属于冻结治理策略")
raise SystemExit(1)
# ── 落库:run + 候选 + 回执 + 实跑长度检测(同事务)──
with connect() as conn:
conn.execute(
"INSERT INTO example_run(run_id, work_id, target_chapter, trigger_source, "
"trigger_detail, started_at, finished_at, terminal_state, creator) "
"VALUES (%s,%s,%s,'user',%s,%s,%s,'completed','writer')",
(run_id, WORK_ID, TARGET_CHAPTER,
json.dumps({"note": "第1章基线:writer 受治理角色调用;asOf 合同不适用,"
"未走生产组装与门",
"systemPromptId": SYSTEM_PROMPT_ID,
"systemPromptSha256": SYSTEM_PROMPT_SHA256,
"executionProfileSha256": receipt.execution_profile_sha256},
ensure_ascii=False),
started, finished))
cand_id = conn.execute(
"INSERT INTO example_candidate(work_id, target_chapter, run_id, attempt, run_type, "
"candidate_version, candidate_sha256, candidate_body, state, mode, source_role, "
"acceptance_eligible, creator) "
"VALUES (%s,%s,%s,1,'production','1',%s,%s,'passed','continuation','writer',true,"
"'writer') RETURNING id",
(WORK_ID, TARGET_CHAPTER, run_id, candidate_sha256, body)).fetchone()[0]
receipt_id = conn.execute(
"INSERT INTO example_run_receipt(run_id, sample_id, revision, adapter_role, "
"stage_kind, attempt, candidate_version, candidate_sha256, requested_model_id, "
"actual_model_id, model_match, effort, total_cost_usd, usage, stop_reason, "
"terminal_reason, is_error, creator) "
"VALUES (%s,'writer-ch1',1,'writer','generation',1,'1',%s,%s,%s,%s,%s,%s,"
"%s::jsonb,%s,%s,%s,'writer') RETURNING id",
(run_id, candidate_sha256, receipt.requested_model_id, receipt.actual_model_id,
receipt.model_match, receipt.effort, str(receipt.total_cost_usd),
json.dumps(dict(receipt.usage or {}), ensure_ascii=False),
receipt.stop_reason, receipt.terminal_reason,
bool(receipt.is_error))).fetchone()[0]
# 长度检测是脚本真跑过的机械检查,如实落账;语义门对基线章不适用,不伪造通过。
conn.execute(
"INSERT INTO example_quality_result(run_id, receipt_id, candidate_sha256, "
"judge_kind, dimension, scale_version, conclusion, detail, creator) "
"VALUES (%s,%s,%s,'detection','length','writer-production-v1','pass',%s::jsonb,"
"'writer')",
(run_id, receipt_id, candidate_sha256,
json.dumps({"actualHanChars": actual_han, **LENGTH_CONTRACT,
"note": "基线章:仅篇幅机械检查;无 WriterContext,语义门不适用"},
ensure_ascii=False)))
conn.commit()
print(f"候选落库: candidate_id={cand_id}, run_id={run_id}")
# 接受为正式正文(confirm 唯一通道)——第 1 章无正文块,expected_revision=0
accept_result = accept(
cand_id, decided_by="1",
rationale=("开篇基线章:writer 受治理角色调用产出,联合回执校验与篇幅"
"机械检查通过。第 1 章无前文,asOf 合同无解,生产组装与机械/语义门不适用;"
"文风锚定设定包 style。"),
basis_ref="细纲@v1 (planning_section id=5)",
expected_revision=0,
command_id=f"accept-work12-ch1-{run_id}")
print(f"接受结果: {json.dumps(accept_result, ensure_ascii=False)}")
print(f"\nCANDIDATE_ID={cand_id}\nRUN_ID={run_id}")
if __name__ == "__main__":
main()