179 lines
8.5 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
"""对照直调臂(阶段 F 终态裁决):同一份探索依赖上下文,直调链生成对照正文。
设计(总 plan §6):直接调用保留为对照模式,使用智能体实际收集的依赖上下文
生成对照正文,比较质量、成本与遗漏率。本入口读取某次两阶段生产运行的生成
阶段输入(探索回放资料 + 篇幅/文风合同),经直调链(角色运行时固定 Opus
策略)单次调用生成对照候选。
显式对照模式纪律:
- 独立对照运行(run-contrast-*),trigger_detail 记录来源生产运行与版本;
- 候选 run_type='eval'(登记枚举)、acceptance_eligible=FALSE、semantic_status 空——
四层机械不可接受,永不进正式正文;
- 不跑语义检测与人闸:对照候选没有接受路径,只做机械门留证与盲评比较。
跑法(真实调用需人授权):
.venv/bin/python .agent/skills/write-next-chapter/scripts/run_contrast_writer.py \
--source-run run-prod-work12-ch3-42beb650 --source-version 9
"""
from __future__ import annotations
import argparse
import json
import sys
import uuid
from decimal import Decimal
from pathlib import Path
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"):
if str(_path) not in sys.path:
sys.path.insert(0, str(_path))
from produce_next_chapter import ( # noqa: E402
ARTIFACTS,
GATE_ANCHORS,
SYSTEM_PROMPT,
SYSTEM_PROMPT_ID,
build_writer_execution_profile,
)
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
from persist_llm_call import persist_call as persist_llm_event # noqa: E402
from run_registry import finish_run, start_run # noqa: E402
from check_writer_candidate import check_writer_candidate # noqa: E402
CREATOR = "contrast-experiment"
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="对照直调臂:同一份探索依赖上下文的直调链生成")
parser.add_argument("--source-run", required=True, help="来源两阶段生产运行 ID")
parser.add_argument("--source-version", type=int, required=True, help="来源候选版本")
args = parser.parse_args(argv)
# 1) 同源输入:来源运行的生成阶段任务包(探索回放资料 + 合同)与冻结上下文
spec_path = ARTIFACTS / f"{args.source_run}-writer-task-v{args.source_version}-gen.json"
ctx_path = ARTIFACTS / f"{args.source_run}-writer-context.json"
if not spec_path.is_file() or not ctx_path.is_file():
raise SystemExit(f"来源运行工件缺失:{spec_path.name} / {ctx_path.name}")
creative_input = json.loads(spec_path.read_text(encoding="utf-8"))["input"]["creativeInput"]
base_context = json.loads(ctx_path.read_text(encoding="utf-8"))
work_id = int(base_context["workId"])
target = int(base_context["targetChapter"])
if target not in GATE_ANCHORS:
raise SystemExit(f"第{target}章门锚合同未登记")
contrast_run = f"run-contrast-work{work_id}-ch{target}-{uuid.uuid4().hex[:8]}"
start_run(
run_id=contrast_run, work_id=work_id, target_chapter=target,
trigger_detail={"stage": "contrast-direct", "sourceRun": args.source_run,
"sourceVersion": args.source_version},
creator=CREATOR,
)
try:
# 2) 对照上下文:同一冻结快照,仅运行身份替换(信封绑定校验所需;
# 合同要求检索计划与上下文 runId 一致,同步替换)
context = dict(base_context)
context["runId"] = contrast_run
retrieval_plan = dict(context.get("retrievalPlan") or {})
retrieval_plan["runId"] = contrast_run
context["retrievalPlan"] = retrieval_plan
with connect(readonly=True) as conn:
next_version = conn.execute(
"SELECT COALESCE(MAX(candidate_version::integer),0)+1 FROM example_candidate "
"WHERE work_id=%s AND target_chapter=%s",
(work_id, target),
).fetchone()[0]
# 3) 直调链生成:角色运行时固定策略,单次调用,无框架循环
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,
)
invocation = run_role(
profile,
creative_input,
business_validator=validate_writer_draft,
run_id=contrast_run,
caller="writer",
persist_call=persist_llm_event,
)
candidate = build_candidate_envelope(
context, invocation.structured_output, candidate_version=int(next_version)
)
# 4) 机械门:与生产链同一门锚合同,产出可比证据
mechanical = check_writer_candidate(context, candidate, GATE_ANCHORS[target])
state = "passed" if mechanical.get("passed") else "rejected"
# 5) 评测候选落库:机械不可接受(run_type=eval 登记枚举 + 不可接受 + 语义空;
# mode 无登记枚举可用,留 NULL;哈希列存裸 64 位)
def _bare(h):
return str(h)[7:] if str(h).startswith("sha256:") else str(h)
with connect() as conn:
candidate_id = conn.execute(
"INSERT INTO example_candidate(work_id,target_chapter,run_id,attempt,run_type,"
"candidate_version,candidate_sha256,candidate_body,context_sha256,quality_policy_version,"
"mode,source_role,state,acceptance_eligible,semantic_status,semantic_report_sha256,creator) "
"VALUES (%s,%s,%s,1,'eval',%s,%s,%s,%s,'writer-production-v1',NULL,"
"'writer',%s,FALSE,NULL,NULL,%s) RETURNING id",
(work_id, target, contrast_run, next_version, _bare(candidate["candidateSha256"]),
candidate["candidateBody"], _bare(base_context["contextSnapshot"]["contextSha256"]),
state, CREATOR),
).fetchone()[0]
conn.execute(
"INSERT INTO example_quality_result(run_id, judge_kind, conclusion, detail) "
"VALUES (%s, 'detection', %s, %s::jsonb)",
(contrast_run, "pass" if mechanical.get("passed") else "fail",
json.dumps(mechanical, ensure_ascii=False)),
)
conn.commit()
finish_run(contrast_run, "completed", creator=CREATOR,
trigger_detail={"stage": "contrast-direct", "candidateId": candidate_id,
"mechanicalPassed": bool(mechanical.get("passed")),
"sourceRun": args.source_run, "sourceVersion": args.source_version})
summary = {
"contrastRunId": contrast_run,
"sourceRun": args.source_run,
"sourceVersion": args.source_version,
"candidateId": candidate_id,
"candidateVersion": next_version,
"candidateSha256": candidate["candidateSha256"],
"mechanicalPassed": bool(mechanical.get("passed")),
"blockingFailures": mechanical.get("blockingFailures") or [],
"hanChars": len([ch for ch in candidate["candidateBody"] if "\u4e00" <= ch <= "\u9fff"]),
"note": "run_type=evaluation:四层机械不可接受,仅供对照与盲评。",
}
ARTIFACTS.mkdir(exist_ok=True)
(ARTIFACTS / f"{contrast_run}-contrast-summary.json").write_text(
json.dumps(summary, ensure_ascii=False, indent=1), encoding="utf-8")
print(json.dumps(summary, ensure_ascii=False)[:600])
print(f"RUN_ID={contrast_run}")
print("对照候选已落库(评测态,机械不可接受);下一步盲评比较与成本对账。")
return 0
except BaseException as exc:
try:
finish_run(contrast_run, "failed", creator=CREATOR,
trigger_detail={"stage": "contrast-direct", "error_type": type(exc).__name__})
except Exception:
pass
raise
if __name__ == "__main__":
raise SystemExit(main())