"""T08 专项离线质检:统一适配 fixture、逐任务/用途准入、冻结 checkpoint 与上下文预算。 依据:docs/v2/11-judgment-providers.md §8、docs/v2/06-validation.md §4/§6; BR-17/18/19/20/37/38/39、E-10/13/14/15/25、AC-16/17/18/19/20/38/39/40/41。 本批**只交付离线 fixture 路径**。没有 Jev 凭据、没有 Laya 权重与授权, 因此: - live 传输层只留注入点,未注入时明确 `LiveTransportUnavailable`; - fixture 通过只证明适配代码结构成立,**不证明**中文专项效果合格 (06-validation.md §6:"fixture 通过仅证明适配代码结构")。 四条不可让渡的规则: 1. **逐任务逐用途准入**:`task_validations` 里没有 (task, usage) 的 passed 记录, 就不执行;服务能连通、别的任务通过、装好了权重,都不算准入(AC-38)。 2. **权重不可隐式换**:运行冻结 `checkpoint_ref`;提供方实际权重不一致即拒绝, 不按语言或负载偷偷路由到别的模型(AC-39)。 3. **超预算不外发**:超出已验证上下文预算时返回 E-25 并保留原证据范围, 绝不截断后半段照样发送(AC-40 / BR-39)。 4. **歧义不硬选**:映射未知、中文否定、条件例外、来源矛盾都落 unknown/人工复核, 不挑一个"看起来正面"的选项(AC-18)。 """ from __future__ import annotations import json from pathlib import Path from contracts.judgment import JudgmentProfile from contracts.prescreen import JudgmentConfig FIXTURE_ROOT = Path(__file__).resolve().parent.parent / "fixtures" / "judgment" # 离线预算估算:中文按 1 字≈1 token、ASCII 按 4 字符≈1 token 的保守上界。 # 真实预算必须按选定模型的 tokenizer 实测填写(11 §8.1),这里的估算只用于 # 离线 fixture 与"宁可拒绝"的方向,不作为生产阈值。 _CJK_FLOOR = 0x2E80 class ContextBudgetExceeded(RuntimeError): """超出已验证上下文预算(E-25)。携带原证据范围,供上层保留引用。""" def __init__(self, message: str, evidence_ids=()): super().__init__(message) self.evidence_ids = list(evidence_ids) class TaskNotValidated(ValueError): """该任务/用途没有 passed 的验收登记(AC-38)。""" class CheckpointMismatch(ValueError): """实际权重修订与运行冻结值不一致(AC-39)。""" class EgressNotApproved(PermissionError): """未获批准的外部出口:不发送(AC-16/19)。""" class LiveTransportUnavailable(RuntimeError): """没有可用的 live 传输层(本批未接入硬件与授权)。""" def estimate_tokens(text: str) -> int: """保守估算 token 数:CJK 每字 1,其它每 4 字符 1,向上取整。""" if not text: return 0 cjk = sum(1 for char in text if ord(char) >= _CJK_FLOOR) other = len(text) - cjk return cjk + (other + 3) // 4 def load_fixture(name: str) -> dict: return json.loads((FIXTURE_ROOT / name).read_text(encoding="utf-8")) # ------------------------------------------------------------------ 准入 def validation_for(profile: JudgmentProfile, task: str, usage: str): """取出该任务与用途的登记;没有登记返回 None,不做"默认通过"。""" for item in profile.task_validations: if item.task == task and item.allowed_usage == usage: return item return None def assert_task_allowed(profile: JudgmentProfile, task: str, usage: str) -> None: """逐任务逐用途准入(AC-38 / AC-41)。 offline 与 runtime 分开登记:只有 offline 通过的配置不能进 runtime; runtime 还要求验收是独立评测——同模型自评或其生成标签不算证据。 """ if profile.readiness != "ready": raise TaskNotValidated("该专项配置尚未就绪,不能执行任何任务") record = validation_for(profile, task, usage) if record is None or record.decision != "passed": raise TaskNotValidated( f"该配置未在用途 {usage} 通过任务 {task} 的验收;" "连通、安装或其它任务通过都不能替代该任务准入") if usage == "runtime" and not record.independent: raise TaskNotValidated("runtime 准入必须是独立评测结果,同模型自评不能作为效果证据") def assert_checkpoint_frozen(config: JudgmentConfig, profile: JudgmentProfile) -> None: """运行冻结值优先:配置登记被改动后,旧运行仍按原值执行(AC-20/39)。""" if profile.checkpoint_ref == "unresolved": raise CheckpointMismatch("权重修订不可复现(unresolved),不得用于任何执行") if config.checkpoint_ref and config.checkpoint_ref != profile.checkpoint_ref: raise CheckpointMismatch( "登记配置的权重修订与本次运行冻结值不一致;请新建运行而不是就地替换") def assert_within_budget(question: dict, profile: JudgmentProfile) -> None: """调用前算预算;超限即拒绝,不截断、不外发(AC-40)。 正文(含提示词)与选项分开计:选项预算不能被正文的余量顶掉,反之亦然。 """ body = "".join(str(item) for item in question.get("evidence_text") or ()) prompt = str(question.get("prompt") or "") options = " ".join(str(item) for item in question.get("options") or ()) evidence_ids = question.get("evidence_ids") or () body_tokens = estimate_tokens(body) + estimate_tokens(prompt) if body_tokens > profile.input_token_limit: raise ContextBudgetExceeded( f"正文约 {body_tokens} token,超出该模型已验证输入预算 " f"{profile.input_token_limit};请缩小问题或转人工复核", evidence_ids=evidence_ids) option_tokens = estimate_tokens(options) if option_tokens > profile.option_token_limit: raise ContextBudgetExceeded( f"选项约 {option_tokens} token,超出该模型已验证选项预算 " f"{profile.option_token_limit};请缩减候选项或转人工复核", evidence_ids=evidence_ids) # ------------------------------------------------------------------ 映射 def _unknown(reason: str) -> dict: return {"kind": "unknown", "reason_code": reason} def map_local_answer(raw: dict, question: dict, profile: JudgmentProfile) -> dict: """本地模型原始输出 → 统一契约形状。 歧义一律落 unknown/inconclusive:映射未知、中文否定、条件例外、来源矛盾 都不许"挑一个正面结果"(AC-18)。`confidence` 只作为 self_reported 指标 保留原语义,**不**换算成任何概率。 """ mapping = (raw.get("mapping") or {}) if raw.get("conflicts"): return _unknown("inconclusive") if raw.get("exception_applies"): return _unknown("inconclusive") label = raw.get("label") if not label: return _unknown("missing_evidence" if not raw.get("evidence_ids") else "inconclusive") option = mapping.get(label) if option is None: # 例如中文否定表述("不符合")未登记映射:不得当成正面选项使用 return _unknown("inconclusive") metrics = [] if raw.get("confidence") is not None: metrics.append({"name": "self_confidence", "value": float(raw["confidence"]), "semantics": "模型自报置信度,未经校准,与审批概率无关", "source": "self_reported"}) return {"kind": question["type"], "value": option, "native_metrics": metrics, "actual_checkpoint_ref": raw.get("actual_checkpoint_ref")} # jev 厂商字段名与本地完全不同:映射表按 mapping_version 固定,主审不读厂商字段 # (AC-17)。新增厂商版本要新增一行映射并单独登记验收,不改这一层的行为。 # 本行只登记 choice 题型的映射:其它题型没有登记就落 unknown,而不是借这一行 # 顺带支持(06-validation.md §6:"阈值和映射按提供方/模型/题型独立登记")。 _DEV_VENDOR_CHOICES = { "APPROVE": "supported", "REJECT": "unsupported", # COND_APPROVE(附条件)刻意**不**映射为通过:附条件不等于通过(BR-21) } def map_jev_answer(raw: dict, question: dict, profile: JudgmentProfile) -> dict: """Jev 厂商响应 → 统一契约形状(fixture 验证用,不发送任何请求)。""" if question["type"] != "choice": return _unknown("inconclusive") envelope = raw.get("result") or {} code = envelope.get("decision_code") value = _DEV_VENDOR_CHOICES.get(code) if value is None: return _unknown("inconclusive") metrics = [] for name, item in (raw.get("metrics") or {}).items(): # 厂商原生指标保留语义与来源;不跨提供方套阈值,也不补齐缺失概率(AC-18) metrics.append({"name": str(name), "value": float(item), "semantics": "厂商原生指标,定义与兴邦校准口径不一致", "source": "provider_native"}) return {"kind": question["type"], "value": value, "native_metrics": metrics, "actual_checkpoint_ref": raw.get("checkpoint")} MAPPERS = {"local": map_local_answer, "jev": map_jev_answer} # ------------------------------------------------------------------ 适配入口 def build_provider_call(config: JudgmentConfig, profile: JudgmentProfile, task: str, usage: str, *, fixture=None, transport=None, call_log=None): """构造 `judge()` 用的 provider_call:先过准入与冻结,再决定是否真的发送。 `fixture(question) -> raw` 是离线路径(不联网);`transport(target, payload)` 是 live 路径。两者不能互相冒充:call_log 会记录 mode,测试据此断言 "local 运行期间没有任何 jev 请求"(AC-17)。 """ assert_task_allowed(profile, task, usage) assert_checkpoint_frozen(config, profile) mapper = MAPPERS[profile.provider] def call(config: JudgmentConfig, question: dict) -> dict: # 预算在发送之前判定:超限即结束,调用记录里不会出现任何发送 assert_within_budget(question, profile) payload = json.dumps({ "question_id": question["id"], "version": question["version"], "type": question["type"], "prompt": question.get("prompt", ""), "options": list(question.get("options") or ()), "evidence_ids": list(question.get("evidence_ids") or ()), "checkpoint_ref": config.checkpoint_ref, "prompt_version": config.prompt_version, "mapping_version": config.mapping_version, }, ensure_ascii=False) if fixture is not None: raw = fixture(question) mode = "fixture" else: if config.provider == "jev" and not profile.egress_approved: raise EgressNotApproved("该外部出口未获批准,已拒绝发送") if transport is None: raise LiveTransportUnavailable( "本批未接入 live 传输层(缺硬件与授权),只能走离线 fixture") raw = transport(profile.provider, payload) mode = "live" if call_log is not None: call_log.append({"target": config.provider, "mode": mode, "question_id": question["id"], "payload": payload}) adapted = mapper(raw, question, profile) actual = adapted.get("actual_checkpoint_ref") if actual and actual != config.checkpoint_ref: raise CheckpointMismatch( f"提供方实际权重 {actual} 与运行冻结值 {config.checkpoint_ref} 不一致") return adapted return call