From 96ebbc1d4cd444acfd96d9313d6bc2966080bfc7 Mon Sep 17 00:00:00 2001 From: zizi Date: Mon, 20 Jul 2026 21:23:04 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20=E9=98=BB=E6=96=AD?= =?UTF-8?q?=E7=9B=AE=E6=A0=87=E4=BA=8B=E5=AE=9E=E6=94=B9=E5=86=99=E9=80=A0?= =?UTF-8?q?=E6=88=90=E7=9A=84=E5=9B=9E=E6=94=BE=E6=B3=84=E9=9C=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/skills/replay-eval/SKILL.md | 4 +- .../replay-eval/scripts/audit_leakage.py | 63 +++++++++++++++ .../replay-eval/scripts/test_audit_leakage.py | 81 +++++++++++++++++++ .../2026-07-19-回放评测-细纲首跑设计与计划.md | 26 +++--- 4 files changed, 159 insertions(+), 15 deletions(-) diff --git a/.claude/skills/replay-eval/SKILL.md b/.claude/skills/replay-eval/SKILL.md index 18d750d..37d9b72 100644 --- a/.claude/skills/replay-eval/SKILL.md +++ b/.claude/skills/replay-eval/SKILL.md @@ -36,7 +36,7 @@ disable-model-invocation: true 授权、来源版本、目标章禁读或内容级泄露检查任一失败,都必须 fail-closed,返回明确的阻断状态,不靠重试绕过。 -`run_replay.py` 会同时审计公共快照和三臂卡注入区。没有 `leakageAudit` 配置、发现未来章记录或目标事实内容匹配时,不生成 `snapshot_manifest`,也不进入 planner。 +`run_replay.py` 会同时审计公共快照和三臂卡注入区。没有 `leakageAudit` 配置、发现未来章记录、目标事实完整匹配,或原子事件子句达到多强锚点与覆盖率门时,不生成 `snapshot_manifest`,也不进入 planner。 ## 产物边界 @@ -55,7 +55,7 @@ disable-model-invocation: true - planner、detector、judge 子进程统一受 `--timeout-seconds` 限制,默认 300 秒;任一超时分别落盘 `planner_timeout`、`detector_timeout`、`judge_timeout`,不得继续进入后续阶段或标记 `completed`。 - `scripts/write_report.py`:从 `run_result.json` 生成独立严格 schema 的安全摘要,只接受受限标识符、枚举、数字、短安全摘要和 SHA-256;不会读取候选正文,也不会把候选路径以外的原始响应写入报告。 -真实作品运行前必须先从权威来源取得不可变授权快照。当前状态:DDL 已实现待 apply,真实记录未写,实跑未开始。用户授权只能登记为 `research_only` 且 `allowedPurpose=["offline_evaluation"]`,不得伪造为 `licensed`;缺记录继续保持 `blocked_authorization`。 +真实作品运行前必须先从权威来源取得不可变授权快照。当前 work=8 已有 `research_only` 且 `allowedPurpose=["offline_evaluation"]` 的有效记录;真实 489 dry-run 因安若雪卡含错标目标事件被 `invalid_snapshot` 阻断,尚未进入 planner。其他作品缺记录时继续保持 `blocked_authorization`,不得伪造为 `licensed`。 ## 正文 A/B/C 回放 diff --git a/.claude/skills/replay-eval/scripts/audit_leakage.py b/.claude/skills/replay-eval/scripts/audit_leakage.py index f8bfc11..d1aab5c 100644 --- a/.claude/skills/replay-eval/scripts/audit_leakage.py +++ b/.claude/skills/replay-eval/scripts/audit_leakage.py @@ -8,7 +8,10 @@ from __future__ import annotations import copy +import difflib import json +import re +import unicodedata from typing import Any, Iterator, Mapping from build_snapshot import normalize_chapter, normalize_chapter_range, sha256_value @@ -21,6 +24,9 @@ STATUS_INVALID_INPUT = "invalid_audit_input" _CHAPTER_KEYS = frozenset( {"chapter", "chapter_no", "order_no", "章", "章号", "from_order", "to_order"} ) +_FACT_SEGMENT_SPLIT = re.compile(r"[,,;;。!?!?、\n]+") +_FACT_LABEL_PREFIX = re.compile(r"^(?:章目标|关键事件|出场角色|伏笔动作|章末钩子)[::]") +_NON_FACT_CHARACTER = re.compile(r"[^0-9a-z\u3400-\u9fff]+") def _chapter_bounds(record: Mapping[str, Any]) -> tuple[int, int] | None: @@ -113,6 +119,54 @@ def _finding( return result +def _normalize_fact_text(value: str) -> str: + """统一宽窄字符并移除标点,只保留可稳定比较的中文、字母和数字。""" + + normalized = unicodedata.normalize("NFKC", value).lower() + return _NON_FACT_CHARACTER.sub("", normalized) + + +def _target_fact_segments(text: str) -> list[str]: + """把复合目标细纲拆成原子事件子句,避免只能逐字匹配整段长文本。""" + + segments: list[str] = [] + for raw_segment in _FACT_SEGMENT_SPLIT.split(text): + stripped = _FACT_LABEL_PREFIX.sub("", raw_segment.strip()) + normalized = _normalize_fact_text(stripped) + # 太短的角色名或栏目词缺乏事件区分度,不能单独触发失败关闭。 + if len(normalized) >= 6: + segments.append(normalized) + return segments + + +def _has_partial_target_fact(value: str, fact_text: str) -> bool: + """识别目标事件的局部改写,同时用多锚点阈值控制历史事实误报。""" + + normalized_value = _normalize_fact_text(value) + if len(normalized_value) < 6: + return False + for segment in _target_fact_segments(fact_text): + if segment in normalized_value: + return True + blocks = [ + block + for block in difflib.SequenceMatcher( + None, segment, normalized_value, autojunk=False + ).get_matching_blocks() + if block.size >= 3 + ] + # 三个互不重叠的强锚点可表达“实体 + 能力/动作 + 数值/结果”的同一事件; + # 两个常见词(例如角色名 + 同步率)不足以判定泄露。 + matched_characters = sum(block.size for block in blocks) + if ( + len(blocks) >= 3 + and matched_characters >= 9 + and matched_characters / len(segment) >= 0.5 + ): + return True + return False + + def audit_snapshot( snapshot: Mapping[str, Any], target_facts: Mapping[str, Any], @@ -172,6 +226,15 @@ def audit_snapshot( findings.append( _finding(reason="target_fact_match", path=path, value=value, fact=fact) ) + elif _has_partial_target_fact(value, fact["text"]): + findings.append( + _finding( + reason="target_fact_partial_match", + path=path, + value=value, + fact=fact, + ) + ) return { "status": STATUS_INVALID_SNAPSHOT if findings else STATUS_READY, diff --git a/.claude/skills/replay-eval/scripts/test_audit_leakage.py b/.claude/skills/replay-eval/scripts/test_audit_leakage.py index ae3f00f..f169689 100644 --- a/.claude/skills/replay-eval/scripts/test_audit_leakage.py +++ b/.claude/skills/replay-eval/scripts/test_audit_leakage.py @@ -65,6 +65,87 @@ class LeakageAuditTest(unittest.TestCase): self.assertIn("factHash", result["findings"][0]) self.assertIn("valueHash", result["findings"][0]) + def test_paraphrased_real_target_event_is_blocked(self): + """真实错标卡不会逐字复制目标细纲,仍须识别同一事件的多个强锚点。""" + + facts = { + "targetChapter": 489, + "forbiddenFacts": [ + { + "id": "key-events-489", + "firstChapter": 489, + "text": "关键事件:苏铭投两枚核雷撕开缺口、安若雪同步率100%强攻加特朗、铎叔反水贯杀加特朗", + } + ], + } + result = audit_snapshot( + { + "cards": [ + { + "chapter": 488, + "fact": "与加特朗激战中深陷压力,转交主攻位置后用生命进发让同步率飙至100%,击碎加特朗能量屏障", + } + ] + }, + facts, + as_of=488, + target=489, + ) + encoded = json.dumps(result, ensure_ascii=False) + self.assertFalse(result["ok"]) + self.assertEqual(result["findings"][0]["reason"], "target_fact_partial_match") + self.assertNotIn("加特朗", encoded) + self.assertNotIn("同步率", encoded) + + def test_related_but_distinct_history_does_not_false_positive(self): + """只共享旧角色和体系术语、但缺少目标事件关键锚点时不得误报。""" + + facts = { + "targetChapter": 489, + "forbiddenFacts": [ + { + "id": "key-events-489", + "firstChapter": 489, + "text": "安若雪同步率100%强攻加特朗", + } + ], + } + result = audit_snapshot( + {"events": [{"chapter": 387, "fact": "安若雪开启拘束解除,同步率突破95%重创死亡收割者"}]}, + facts, + as_of=488, + target=489, + ) + self.assertTrue(result["ok"]) + self.assertEqual(result["findingCount"], 0) + + def test_entities_scattered_across_long_history_do_not_form_one_event(self): + """多个实体分散在旧细纲的不同事件里,不能仅凭名字凑成目标事件。""" + + facts = { + "targetChapter": 489, + "forbiddenFacts": [ + { + "id": "network-attack-489", + "firstChapter": 489, + "text": "赛莉丝入虚拟城外圈夺控防御闸门并坑杀凯多萨康里弗两方", + } + ], + } + historical_outline = ( + "佛罗科密授康里弗追求安若雪以图谋王位;" + "赛莉丝入侵要塞惊见囚禁的高等级灵械后退出;" + "另一场景中凯多萨与康里弗争执,搅乱苏铭和安若雪的会面。" + ) + result = audit_snapshot( + {"chapters": [{"chapter": 484, "outline": historical_outline}]}, + facts, + as_of=488, + target=489, + ) + self.assertTrue(result["ok"]) + self.assertEqual(result["findingCount"], 0) + def test_target_and_as_of_must_form_next_chapter_pair(self): result = audit_snapshot({}, target_facts(), as_of=487, target=489) self.assertFalse(result["ok"]) diff --git a/docs/2026-07-19-回放评测-细纲首跑设计与计划.md b/docs/2026-07-19-回放评测-细纲首跑设计与计划.md index 7aa6bed..d248ddc 100644 --- a/docs/2026-07-19-回放评测-细纲首跑设计与计划.md +++ b/docs/2026-07-19-回放评测-细纲首跑设计与计划.md @@ -8,17 +8,17 @@ **技术栈:** `agent-example` 文件版实验台、PostgreSQL `muse-example` 只读查询、现有 `planning`/`read-context`/`detect`/`quality-gate` skill、Opus 规划与评审子代理;确定性冻结和校验脚本使用仓库 `.venv` 的 Python。 -## 执行状态(2026-07-19) +## 执行状态(2026-07-20) -- 已落地并同步到 `agent-example/main`:`fbb262c`(冻结/细纲/评分合同)、`a54a3a4`(审查反馈收紧与回放编排)、`1243d9b`(外部 planner execute 链路测试)。 -- 已验证:replay-eval 70 个离线测试、fine-outline 合同 3 个测试全部通过;fake runner 已跑通三臂 planner、真正不识别臂卡内容的逐候选 detector、双 judge 反序盲评、子进程超时失败关闭、稳定性门和去盲矩阵。测试只使用合成 fixture,不代表参考作品评测结果。 +- 已合入 `agent-example/main`:冻结/细纲/评分合同、三臂 planner、逐候选 detector、双 judge 反序盲评、稳定性门、原文件授权链和安全报告;集成提交为 `843d5d7`。 +- 已验证:replay-eval 127 个离线测试全部通过;fake runner 已跑通三臂 planner、真正不识别臂卡内容的逐候选 detector、双 judge 反序盲评、子进程超时失败关闭、稳定性门和去盲矩阵。合成 fixture 不代表参考作品评测结果。 - 已新增并验证:`audit_leakage.py` 对公共快照和三臂卡注入区执行内容级事实审计;`load_reference_work.py` 通过 PostgreSQL 只读事务组装仓库外临时配置,候选卡标记为 `eval_draft`,不进入生产检索。 -- 已执行真实适配 smoke:work=8、冻结 488、目标 489,组装 31 个完整历史大纲窗、6 个近章细纲摘要、正确/错配卡各 2 张;送入回放仍为 `blocked_authorization`(`example_reference_work` 没有授权快照),未生成 `snapshot_manifest`,未调用模型。 -- 已执行真实数据库前置 smoke:深空之影 `work_id=8` 的 `example_reference_work` 表没有不可变授权快照/版权用途字段,结果为 `blocked_authorization`,三臂均未调用模型。 -- 任务 A 已实现原文件版本与 append-only 授权快照 DDL:work=8 的权威原文件 `document_id=7`、SHA-256=`02cf1f8c1ca03c26e0b839d88fe536e83c0af20fd8972235b7aedca6a33becf4`,来源版本固定为 `raw-file-v1:sha256:`,不再使用作品 revision+导入章数。 -- 当前状态:**DDL 已实现待 apply,真实记录未写,实跑未开始**。用户授权只能登记为 `research_only + offline_evaluation`,不得伪造 `licensed`;apply 与 INSERT 均不在本任务执行范围。 +- 授权表已应用,work=8 已有一条有效的 `research_only + offline_evaluation` 不可变快照;原文件、document SHA-256 和 `raw-file-v1` 来源版本三方一致,重验时间为 2026-08-18。 +- 已执行真实 work=8 / as_of=488 / target=489 dry-run:正确卡选择为安若雪与圣蒂曼行星,placebo 为同型近体量的伊蕾莉雅与鹦鹉螺号;未调用模型。 +- 首次真实 dry-run 暴露内容审计假阴性:安若雪卡把第 489 章“加特朗 / 同步率 100%”事件错标为第 488 章,旧审计只做完整字符串包含而错误放行。现已增加原子事件子句、多强锚点和目标子句覆盖率门,真实样本被正确阻断为 `invalid_snapshot`,不生成 manifest、不进入 planner。 +- 当前状态:**授权与真实装配已通,目标样本因上游卡章号错标被内容门阻断,模型实跑未开始**。必须先修正或隔离泄露来源,再执行三臂模型回放。 - `.claude/skills/llm/scripts/test_quota.py` 已统一到当前 `$24/6000` 契约:预算场景使用 `$24.5`,调用上限场景使用 `6000`,并增加策略常量断言。 -- 当前允许的结论是“冻结、变量控制、内容级泄露审计、候选结构门、detector/judge 双评编排、安全摘要机制和授权快照代码合同已通”;不能说数据库表已应用、真实授权已写入、细纲智能体或知识卡已通过。真实授权接入和 430/489/550 实样仍待完成。 +- 当前允许的结论是“授权、真实数据装配、冻结、变量控制、内容级泄露阻断、候选结构门、detector/judge 双评编排和安全摘要机制已通”;不能说细纲智能体或知识卡已通过。489 样本需先清除泄露来源,430/550 样本仍待执行。 --- @@ -301,7 +301,7 @@ arm ∈ {outline_only, outline_plus_cards, outline_plus_placebo_cards} - [x] 将卡生成 `as_of=N` 规划视图,剔除终态字段、未来里程碑、无绝对上界条目和无法溯源字段。 - [x] 预检授权双闸、来源状态、目标章禁读、候选卡选择来源和原书全文不落最终报告。 - [x] 用离线 fixture 覆盖未来里程碑、跨 N 窗口、章号归一化、嵌套章记录、终态裁剪、缺授权失败关闭和三臂公共区哈希一致。 -- [x] 增加目标章内容级事实泄露审计,发现错位事件或臂内卡未来记录时整例作废;审计结果只保留路径和哈希。 +- [x] 增加目标章内容级事实泄露审计;完整匹配外,还按原子事件子句执行多强锚点与覆盖率判断,能阻断真实错标改写并避免长历史细纲实体散落造成的假阳性。审计结果只保留路径和哈希。 - [x] 增加 PostgreSQL 只读参考作品适配器;只读取结构化大纲/细纲摘要和预注册卡,目标章仅进入临时审计 proxy,缺授权不放行。 ### Task 2:细纲智能体功能合同 @@ -329,7 +329,7 @@ arm ∈ {outline_only, outline_plus_cards, outline_plus_placebo_cards} - [x] 给 detector 增加细纲候选的检查对象、严重度和证据格式。 - [x] 给 judge 增加 `fine_outline_replay` rubric profile,明确不评正文文风和文笔。 -- [x] 在回放编排中固定两个独立 judge、匿名 arm 和第二评委候选顺序反转;fake runner 已覆盖双评、rubric 合同和稳定性失败关闭,真实样本尚未越过授权门。 +- [x] 在回放编排中固定两个独立 judge、匿名 arm 和第二评委候选顺序反转;fake runner 已覆盖双评、rubric 合同和稳定性失败关闭,真实样本当前被内容泄露门阻断。 - [x] 机械测试确保 rubric 不包含正文质量维度,且每个分数必须有证据字段。 ### Task 4:回放编排与最小首跑 @@ -341,11 +341,11 @@ arm ∈ {outline_only, outline_plus_cards, outline_plus_placebo_cards} - Test: `.claude/skills/replay-eval/scripts/test_run_replay.py` - Create: `docs/replay/-summary.md` -- [x] 先用合成 fixture 完成一个机制 dry-run,并用 fake planner 验证 execute 链路;真实样本尚未越过授权门。 -- [x] 编排器支持每个目标章 A/B/C 三臂,planner 使用相同身份段、功能段、预算和输出合同;尚未运行参考作品目标章。 +- [x] 先用合成 fixture 完成机制 dry-run,并用 fake planner 验证 execute 链路;真实 489 样本已完成确定性 dry-run 并因内容泄露正确阻断。 +- [x] 编排器支持每个目标章 A/B/C 三臂,planner 使用相同身份段、功能段、预算和输出合同;参考作品目标章尚未启动模型。 - [x] 两个独立 judge 对每章候选做顺序互换盲评;detector 阻断样本不送 judge。当前由 fake runner 离线测试验证,尚无真实样本结果。 - [x] 运行时原始候选和标准事实摘要只放仓库外临时目录;安全报告只写摘要、定位、评分入口、哈希和失败类别。 -- [x] 接入真实数据库只读适配 smoke;授权缺失时维持 `blocked_authorization`,不启动 planner。 +- [x] 接入真实数据库只读适配与授权快照;授权、来源或内容审计任一失败都不启动 planner。 - [ ] 产出真实卡增量矩阵和样本级假阴/假阳说明;不能用机制 smoke 的合成结果代替。 ### Task 5:滚动评估门