zizi e36cd57010 框架: 评测合同与提示词去支架化 + skill 三层重组(Phase 0-4)
【评测合同与提示词】
- writer/detector/judge 评测提示词去支架化:删评测身份/输出格式叮嘱/盲化反向叮嘱/机器字段
  (改由运行时 --json-schema/--tools ""/--strict-mcp-config/输入设计强制),装回各角色本业纪律
- writer 加写作纪律:戏剧化节拍禁抄细纲概述句、具体压倒抽象、每场戏三件套、篇幅用场景写够不注水不缩水
- 篇幅自纠环修订指令区分偏短(加场景)/偏长(删冗余);机械门要求清单(章末钩子/硬事件/角色/伏笔)注入写手硬约束
- 引文校验从「必须恰好1次」放宽为「0次失败关闭、≥1次绑首次」(检测+盲评同口径)
- llm 立 chat_governed 为主入口(chat 降为仅调试),clean_detect 改用 chat_governed 弃自带降级链
- rewrite/expansion/polish 输出合同对齐 writer.md(返回文本、写手不读写工作区、主会话落工作区)
- 修复 confirm/replay-eval 探针两组坏自测(夹具适配现行合同、探针测试自包含不硬编码漂移哈希);补 clean_detect 离线测试
- AGENTS.md 新增 §10「Agent 提示词与 Skill 审查标准」

【skill 三层重组:单向依赖 底座→能力→编排,断两环+修生产倒挂】
- Phase 0: 新建 runtime 底座(claude_runtime/file_cas/raw_vault),断环 C1、修 continuation 生产倒挂
- Phase 1: 评分尺+门判(writer_rubric/fine_outline_rubric/writer_gate/gate_input_builder)收进 quality-gate,断环 C2、名实相符
- Phase 2: 升格管线从 parse-book 独立成 upgrade skill(备份审计契约键名稳定、仅改路径定位)
- Phase 3: replay-eval 瘦成纯编排
- read-context 共用簇(build_snapshot/audit_leakage/check_snapshot/load_reference_work)下沉到新 snapshot skill,消除能力层向上引用
- Phase 4: run_writer_replay.py(130KB)拆成包(_common/budget/authorization/sample/blind/execute),__init__ 全量 re-export 测试零改动

【base 配置】三角色 effort 提 high;提示词更新;探针重测绑定 writer 合同;预算 writer 45 次/总 450 美元(含篇幅修订)

全量离线测试 36 个文件全绿;函数逻辑零改动(仅搬位置/改 import/改文档,capture_code_identity 仅改路径定位)。
2026-07-26 03:28:22 +08:00

465 lines
21 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
"""运行探针刷新工具:按当前 writer 合同重测结构化输出能力探针,刷新配置授权后写到新文件。
WHY(为什么需要这个工具)
正文回放的正式执行门(run_writer_replay._validate_execute_authorization)已加固:
runtimeProbe.executionProfileSha256 必须等于当前 writer profile 的身份哈希
(ExecutionProfile.execution_profile_sha256,计算时已把 jsonSchemaSha256 +
systemPromptSha256 + 模型 + CLI + 预算一并卷入)。writer 合同(schema / prompt / 模型 /
CLI / 预算)一旦升级,旧探针的身份哈希必然失配,门会以 EXECUTE_PROBE_CONTRACT_MISMATCH
失败关闭。仓里此前没有重测探针的工具,本工具补上这一环:用当前 writer 合同实跑一次极小的
合成能力探针,产出绑定新合同的 runtimeProbe,替换旧探针并重签授权自哈希,写到 --output
指定的新文件,供主代理 review diff 后再替换正式配置。
红线(对齐 replay-eval 合同)
- 探针输入是固定、极小、全合成的能力探针 prompt,绝不使用原书全文 / 真实正文 / raw 物料。
- Claude 调用经【可注入 invoker 接口】发起。默认 invoker = claude_runtime.run_claude
(真实调用层是薄薄一层,run_claude 内部已联合校验 schema / 预算 cap / deadline / 模型 /
回执);离线测试与 --dry-run 注入假 invoker,工具主体完全确定、可离线测。
- 失败关闭:调用失败 / 结构化输出 schema 不过 / 超单次预算 cap / 超 deadline / 回执不可信
→ 不写 successful 探针、不产出刷新配置、退出非零;原配置保持不动。
- 审计日志只记录做了什么(探针身份哈希、结构化输出哈希、成本、是否成功、输出文件),
绝不打印完整 prompt / response、raw 路径或供应商原始响应。
"""
from __future__ import annotations
import argparse
import copy
import json
import sys
from dataclasses import dataclass
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
from pathlib import Path
from typing import Any, Callable, Mapping, Protocol
SCRIPT_DIR = Path(__file__).resolve().parent
RUNTIME_DIR = SCRIPT_DIR.parents[1] / "runtime" / "scripts"
QUALITY_GATE_DIR = SCRIPT_DIR.parents[1] / "quality-gate" / "scripts"
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
if str(RUNTIME_DIR) not in sys.path:
sys.path.insert(0, str(RUNTIME_DIR))
if str(QUALITY_GATE_DIR) not in sys.path:
sys.path.insert(0, str(QUALITY_GATE_DIR))
from claude_runtime import ( # noqa: E402
HASH_PATTERN,
ClaudeInvocationResult,
ClaudeRuntimeError,
ExecutionProfile,
ExecutionReceipt,
run_claude,
sha256_json,
validate_json_schema,
)
# WHY: 重签必须与门校验逐字节同源。run_writer_replay 的授权门用 gate_input_builder.canonical_sha256
# 复核 probe/budget/raw 各自的 receiptSha256,这里复用同一函数,保证重签结果与门校验完全一致。
from gate_input_builder import canonical_sha256 # noqa: E402
# WHY: 复用门自己用来从配置重建 writer profile 的函数,保证本工具算出的 execution_profile_sha256
# 与门算出的逐字节相同;它只读 executionProfiles.writer,不碰需要 oracleTruthPacks 的整适配路径。
from run_writer_replay import _profile_from_mapping # noqa: E402
# ---------------------------------------------------------------------------
# 探针输入:固定、极小、全合成。绝不引用真实作品、真实正文或 raw 物料。
# WHY: 探针只证明「当前 writer 合同能产出一小段合规 candidateBody」,输入内容本身不参与任何
# 身份哈希,故用一段自包含的合成微任务即可;固定常量保证刷新过程确定、可复跑、可审计。
# 形状对齐 WriterCreativeInput v2(writer 系统提示词只接收该形状),让真实调用能正常产出。
# ---------------------------------------------------------------------------
PROBE_BUSINESS_INPUT: dict[str, Any] = {
"fineOutline": {
"hardConstraints": ["本段为运行探针合成任务,不引用任何真实作品或真实正文。"],
"adjustableBeats": ["写一个守塔人在黎明前点亮灯塔的极短瞬间。"],
"declaredNewFacts": [],
},
"narrativeState": {
"asOfChapter": 0,
"summary": "合成探针场景:一座孤岛灯塔,黎明前。无任何真实作品人物或情节。",
},
"factConstraints": [],
"proseExcerpts": [],
"patternReferences": [],
"lengthContract": {
"targetChars": 60,
"minChars": 20,
"maxChars": 120,
"frontmatterRequired": False,
},
"styleConstraints": [],
}
# --dry-run 使用的固定合成产出(同样不含任何真实作品内容)。
DRY_RUN_CANDIDATE_BODY = (
"黎明前最暗的一刻,守塔人划亮火柴,灯芯燃起一小团光,海面被照出一线金边。"
)
class ProbeRefreshError(RuntimeError):
"""探针刷新失败关闭:携带稳定错误码,绝不携带正文 / prompt / 原始响应。"""
def __init__(self, code: str, message: str) -> None:
super().__init__(f"{code}: {message}")
self.code = code
self.message = message
class ProbeInvoker(Protocol):
"""可注入的模型调用接口:真实层与确定层之间的唯一 seam。
真实实现 = claude_runtime.run_claude(默认);离线测试 / --dry-run 注入返回预设
ClaudeInvocationResult 的假实现。签名与 run_claude 的前两个位置参数对齐。
"""
def __call__(
self, profile: ExecutionProfile, business_input: Mapping[str, Any]
) -> ClaudeInvocationResult: ...
@dataclass(frozen=True)
class RefreshResult:
"""一次成功刷新的确定性产物:刷新后配置 + 新探针 + 审计安全摘要。"""
refreshed_config: dict[str, Any]
probe: dict[str, Any]
summary: dict[str, Any]
# ---------------------------------------------------------------------------
# 确定层:构建 profile、校验回执、构建探针、重签授权。全程无模型调用、无文件 IO、无墙钟。
# ---------------------------------------------------------------------------
def build_writer_profile(config: Mapping[str, Any]) -> ExecutionProfile:
"""从 base 配置的 executionProfiles.writer 构建当前 writer 冻结 profile。
WHY: 只读 writer profile(探针只用 writer 合同),复用门同款 _profile_from_mapping,
保证 execution_profile_sha256 与门逐字节一致;不读 semantic / judge,不读 oracleTruthPacks。
"""
profiles = config.get("executionProfiles")
if not isinstance(profiles, Mapping):
raise ProbeRefreshError("PROBE_CONFIG_INVALID", "配置缺少 executionProfiles")
return _profile_from_mapping(profiles.get("writer"), role="writer")
def verify_probe_result(profile: ExecutionProfile, result: ClaudeInvocationResult) -> None:
"""失败关闭地复核 invoker 产出:任一异常都抛 ProbeRefreshError,绝不放行可疑回执。
WHY: run_claude 内部已联合校验 schema / 预算 / deadline / 模型 / 回执;这里再做一层同口径
的独立复核,既是对真实调用的纵深防御,也让「假 invoker 返回坏产出」在离线测试里可被精确触发。
所有错误码只描述失败类别,不携带正文 / prompt / 原始响应。
"""
receipt = result.receipt
# 1. 结构化输出必须符合当前 writer schema(candidateBody 非空字符串、无额外字段)。
try:
validate_json_schema(result.structured_output, profile.json_schema)
except Exception as exc: # noqa: BLE001 - 任何 schema 异常统一失败关闭。
raise ProbeRefreshError("PROBE_SCHEMA_INVALID", "结构化输出不符合 writer schema") from exc
# 2. 回执必须绑定到当前 profile 身份,证明它确实是针对当前合同跑的。
if receipt.execution_profile_sha256 != profile.execution_profile_sha256:
raise ProbeRefreshError(
"PROBE_PROFILE_BINDING_MISMATCH", "回执身份哈希与当前 writer profile 不一致"
)
# 3. 结构化输出哈希必须是合法 SHA-256,且等于实际产出的规范哈希,防止挂空壳。
output_hash = receipt.structured_output_sha256
if (
not isinstance(output_hash, str)
or not HASH_PATTERN.fullmatch(output_hash)
or output_hash != sha256_json(result.structured_output)
):
raise ProbeRefreshError(
"PROBE_OUTPUT_HASH_INVALID", "结构化输出哈希缺失、非法或与产出不一致"
)
# 4. 模型必须匹配:model_match 为真且实际模型等于冻结的完整模型 ID。
if receipt.model_match is not True or receipt.actual_model_id != profile.resolved_model_id:
raise ProbeRefreshError("PROBE_MODEL_MISMATCH", "实际模型与冻结模型不一致")
# 5. 回执不得标记错误。
if receipt.is_error is not False:
raise ProbeRefreshError("PROBE_RECEIPT_ERROR", "回执标记为错误")
# 6. 进程必须正常退出。
if receipt.exit_code != 0:
raise ProbeRefreshError("PROBE_NONZERO_EXIT", "Claude CLI 非零退出")
# 7. 终止原因必须落在冻结的正常终止集合内(writer 为 completed)。
if receipt.terminal_reason not in profile.normal_terminal_reasons:
raise ProbeRefreshError("PROBE_TERMINAL_REASON_INVALID", "终止原因不在正常终止集合内")
# 8. 不得携带 API 错误状态。
if receipt.api_error_status is not None:
raise ProbeRefreshError("PROBE_API_ERROR", "回执携带 API 错误状态")
# 9. 单次成本必须可信且不超过冻结的单次预算 cap。
if receipt.total_cost_usd is None:
raise ProbeRefreshError("PROBE_BUDGET_EXCEEDED", "回执缺少可信成本")
try:
cost = Decimal(receipt.total_cost_usd)
except (InvalidOperation, ValueError) as exc:
raise ProbeRefreshError("PROBE_BUDGET_EXCEEDED", "回执成本不可解析") from exc
if not cost.is_finite() or cost < 0 or cost > profile.max_budget_usd_per_call:
raise ProbeRefreshError("PROBE_BUDGET_EXCEEDED", "单次成本超过冻结预算 cap")
def build_probe_record(
profile: ExecutionProfile, result: ClaudeInvocationResult, *, checked_at: str
) -> dict[str, Any]:
"""用调用结果构建新的 runtimeProbe 记录并重签其 receiptSha256 自哈希。
字段集与现有 runtimeProbe 完全一致(不缺不多):身份 / 运行时绑定来自 writer profile,
能力证据(身份哈希、输出哈希、成本、终止原因、模型匹配、退出码、API 状态)来自回执,
executionReceiptSha256 绑定完整回执,receiptSha256 是记录自身(去掉 receiptSha256)的规范哈希。
"""
receipt = result.receipt
# WHY: 先构建不含 receiptSha256 的记录,对它取规范哈希作为自哈希,再回填——这与门校验
# `item.receiptSha256 == canonical_sha256({k: v for k, v in item.items() if k != "receiptSha256"})`
# 完全等价,保证重签后自检逐字节通过。
record: dict[str, Any] = {
"status": "successful",
"checkedAt": checked_at,
"claudeExecutablePath": profile.claude_executable_path,
"claudeExecutableSha256": profile.claude_executable_sha256,
"claudeCliVersion": profile.claude_cli_version,
"modelAlias": profile.model_alias,
"resolvedModelId": profile.resolved_model_id,
"executionProfileSha256": receipt.execution_profile_sha256,
"executionReceiptSha256": canonical_sha256(receipt.as_dict()),
"structuredOutputSha256": receipt.structured_output_sha256,
"terminalReason": receipt.terminal_reason,
"totalCostUsd": receipt.total_cost_usd,
"modelMatch": receipt.model_match,
"exitCode": receipt.exit_code,
"apiErrorStatus": receipt.api_error_status,
}
record["receiptSha256"] = canonical_sha256(record)
return record
def apply_probe(config: Mapping[str, Any], probe: Mapping[str, Any]) -> dict[str, Any]:
"""把新探针写入 executionAuthorization.runtimeProbe 的深拷贝,返回刷新后的完整配置。
WHY: 深拷贝保证原配置对象不被就地改动;只替换 runtimeProbe,budget / rawRetention /
profileSha256 原样保留(它们的自哈希不受探针刷新影响,门会各自复核)。
"""
authorization = config.get("executionAuthorization")
if not isinstance(authorization, Mapping):
raise ProbeRefreshError("PROBE_CONFIG_INVALID", "配置缺少 executionAuthorization")
refreshed = copy.deepcopy(dict(config))
refreshed["executionAuthorization"] = copy.deepcopy(dict(authorization))
refreshed["executionAuthorization"]["runtimeProbe"] = copy.deepcopy(dict(probe))
return refreshed
def _audit_summary(
profile: ExecutionProfile, probe: Mapping[str, Any], *, dry_run: bool
) -> dict[str, Any]:
"""生成审计安全摘要:只记录做了什么,绝不打印 prompt / response / raw / 原始响应。"""
return {
"tool": "refresh_runtime_probe",
"dryRun": dry_run,
"status": probe.get("status"),
"executionProfileSha256": probe.get("executionProfileSha256"),
"structuredOutputSha256": probe.get("structuredOutputSha256"),
"executionReceiptSha256": probe.get("executionReceiptSha256"),
"probeReceiptSha256": probe.get("receiptSha256"),
"totalCostUsd": probe.get("totalCostUsd"),
"modelAlias": profile.model_alias,
"resolvedModelId": profile.resolved_model_id,
"claudeCliVersion": profile.claude_cli_version,
"checkedAt": probe.get("checkedAt"),
}
def refresh_runtime_probe(
config: Mapping[str, Any],
*,
invoker: ProbeInvoker,
checked_at: str,
dry_run: bool = False,
) -> RefreshResult:
"""工具主体的确定链路:构建 profile → 调用 → 失败关闭复核 → 构建探针 → 重签 → 刷新配置。
无文件 IO、无墙钟(checked_at 由调用方注入)、真实调用只发生在传入的 invoker 内部。
任一步失败抛 ProbeRefreshError / ClaudeRuntimeError,调用方据此退出非零、不产出刷新配置。
"""
profile = build_writer_profile(config)
result = invoker(profile, PROBE_BUSINESS_INPUT)
verify_probe_result(profile, result)
probe = build_probe_record(profile, result, checked_at=checked_at)
refreshed = apply_probe(config, probe)
summary = _audit_summary(profile, probe, dry_run=dry_run)
return RefreshResult(refreshed_config=refreshed, probe=probe, summary=summary)
# ---------------------------------------------------------------------------
# 真实调用层(薄薄一层)与 dry-run 假调用层。
# ---------------------------------------------------------------------------
def make_dry_run_invoker() -> ProbeInvoker:
"""返回固定合成产出的假 invoker,供 --dry-run 冒烟「构建+重签+写文件」链路,绝不发起真实调用。"""
def _dry_run_invoker(
profile: ExecutionProfile, business_input: Mapping[str, Any]
) -> ClaudeInvocationResult:
structured_output = {"candidateBody": DRY_RUN_CANDIDATE_BODY}
receipt = ExecutionReceipt(
adapter_role=profile.adapter_role,
invocation_id="dry-run-probe",
execution_profile_sha256=profile.execution_profile_sha256,
requested_model_id=profile.resolved_model_id,
actual_model_id=profile.resolved_model_id,
model_match=True,
effort=profile.effort,
max_budget_usd_per_call=format(profile.max_budget_usd_per_call, "f"),
total_cost_usd="0.001000",
usage={"input_tokens": 1, "output_tokens": 1},
model_usage={profile.resolved_model_id: {"costUSD": "0.001000"}},
stop_reason="end_turn",
terminal_reason=profile.normal_terminal_reasons[0],
is_error=False,
api_error_status=None,
exit_code=0,
duration_ms=1,
input_sha256=sha256_json(business_input),
structured_output_sha256=sha256_json(structured_output),
json_schema_sha256=profile.json_schema_sha256,
)
return ClaudeInvocationResult(structured_output=structured_output, receipt=receipt)
return _dry_run_invoker
def _now_iso() -> str:
"""当前 UTC 时间的 ISO-8601 字符串,作为 checkedAt 默认值。"""
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def _write_json(path: Path, value: Mapping[str, Any]) -> None:
"""以稳定格式写出配置:UTF-8、保留中文、两空格缩进、结尾换行。"""
path.write_text(
json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
def main(argv: list[str] | None = None) -> int:
"""CLI 入口:读 base 配置 → 刷新探针 → 写到 --output 新文件;失败关闭返回非零。"""
parser = argparse.ArgumentParser(
description="按当前 writer 合同重测能力探针并刷新配置授权(写到新文件,不就地覆盖)。"
)
parser.add_argument("--config", required=True, help="base 配置路径(只读)")
parser.add_argument("--output", required=True, help="刷新后配置的写出路径(新文件)")
parser.add_argument(
"--dry-run",
action="store_true",
help="不发起真实调用,用固定假产出走通构建+重签+写文件链路(冒烟用)",
)
parser.add_argument(
"--checked-at",
default=None,
help="探针 checkedAt 时间戳(默认当前 UTC);离线复跑可显式注入以保证确定",
)
args = parser.parse_args(argv)
config_path = Path(args.config)
output_path = Path(args.output)
# WHY: 强制输出是新文件,绝不就地覆盖原配置,便于主代理 review diff 后再替换。
if output_path.resolve() == config_path.resolve():
print(
json.dumps(
{"tool": "refresh_runtime_probe", "error": "PROBE_OUTPUT_SAME_AS_CONFIG",
"message": "--output 必须是不同于 --config 的新文件"},
ensure_ascii=False,
)
)
return 2
try:
config = json.loads(config_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
print(
json.dumps(
{"tool": "refresh_runtime_probe", "error": "PROBE_CONFIG_UNREADABLE",
"message": "base 配置读取失败"},
ensure_ascii=False,
)
)
return 2
invoker: ProbeInvoker = make_dry_run_invoker() if args.dry_run else run_claude
checked_at = args.checked_at or _now_iso()
try:
result = refresh_runtime_probe(
config, invoker=invoker, checked_at=checked_at, dry_run=args.dry_run
)
except ProbeRefreshError as exc:
# 审计安全:只报错误码与类别,绝不报正文 / prompt / 原始响应。
print(
json.dumps(
{"tool": "refresh_runtime_probe", "error": exc.code, "message": exc.message,
"dryRun": args.dry_run, "status": "failed_closed"},
ensure_ascii=False,
)
)
return 1
except ClaudeRuntimeError as exc:
# WHY: 真实调用层(run_claude)的失败关闭(调用失败 / 超预算 / 超 deadline / schema /
# 回执不可信)在这里统一兜底:不写探针、不改配置、退出非零。只透传稳定主码 exc.code,
# 不透传可能携带敏感上下文的 message 正文。
print(
json.dumps(
{"tool": "refresh_runtime_probe", "error": exc.code,
"message": "Claude 调用失败关闭", "dryRun": args.dry_run, "status": "failed_closed"},
ensure_ascii=False,
)
)
return 1
try:
_write_json(output_path, result.refreshed_config)
except OSError:
print(
json.dumps(
{"tool": "refresh_runtime_probe", "error": "PROBE_OUTPUT_UNWRITABLE",
"message": "刷新配置写出失败", "dryRun": args.dry_run, "status": "failed_closed"},
ensure_ascii=False,
)
)
return 1
summary = dict(result.summary)
summary["outputFile"] = str(output_path)
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))
return 0
__all__ = [
"DRY_RUN_CANDIDATE_BODY",
"PROBE_BUSINESS_INPUT",
"ProbeInvoker",
"ProbeRefreshError",
"RefreshResult",
"apply_probe",
"build_probe_record",
"build_writer_profile",
"make_dry_run_invoker",
"main",
"refresh_runtime_probe",
"verify_probe_result",
]
if __name__ == "__main__":
raise SystemExit(main())