#!/usr/bin/env python3 """run_output_evals.py — 输出评测的确定性环节(Phase 3,路线 C)。 prepare 为每条 output case 生成 old/new/without 三个匿名任务包(盲测:包名随机化标签, 映射表单独存放,不给评审者) score 对记录的输出跑机械断言(contains/not_contains/regex/file_exists/json_path), 机械断言先于 LLM judge;盲评分歧样本留给人工复核 outputs 目录约定: //.md(variant-label 来自 prepare 的映射表) 用法: python3 scripts/run_output_evals.py prepare --out [--variants old_skill,new_skill,without_skill] python3 scripts/run_output_evals.py score --outputs --mapping --out """ from __future__ import annotations import argparse import json import random import re import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from cangjie_common import dump_json, load_json # noqa: E402 def check_assertion(a: dict, text: str, base: Path) -> bool: kind, value = a["kind"], a["value"] if kind == "contains": return value in text if kind == "not_contains": return value not in text if kind == "regex": return re.search(value, text) is not None if kind == "file_exists": return (base / value).exists() if kind == "json_path": # 简化版: 顶层 key 存在于输出中的 JSON 块 try: data = json.loads(re.search(r"\{.*\}", text, re.S).group(0)) except Exception: return False cur = data for part in value.lstrip("$.").split("."): if not isinstance(cur, dict) or part not in cur: return False cur = cur[part] return True raise ValueError(f"未知断言类型: {kind}") def cmd_prepare(suite_path: Path, out_dir: Path, variants: list[str]) -> int: suite = load_json(suite_path) rng = random.Random(suite.get("split_seed", 42)) out_dir.mkdir(parents=True, exist_ok=True) mapping: dict[str, dict[str, str]] = {} for c in suite.get("output_cases", []): labels = [f"v{chr(65 + i)}" for i in range(len(variants))] rng.shuffle(labels) mapping[c["case_id"]] = dict(zip(labels, variants)) case_dir = out_dir / c["case_id"] case_dir.mkdir(exist_ok=True) for label in labels: dump_json(case_dir / f"task-{label}.json", { "case_id": c["case_id"], "variant_label": label, "prompt": c["prompt"], "input_files": c.get("input_files", []), "instruction": "按 prompt 完成任务,输出保存为同目录