"""craft A/B 评估脚本入口(灵感 F2)。 # dry(默认,零成本、免真实 LLM)——组装+校验+并排 dump prompt uv run python -m tests.craft_eval # live(真实付费生成 + LLM 五维裁判) uv run python -m tests.craft_eval --live 选项:`--genre 仙侠` 只跑某题材;`--out PATH` 指定报告路径;`--quiet` 少打印。 退出码:dry 任一断言失败 → 非零(可当回归门禁)。详见 README.md。 """ from __future__ import annotations import argparse import asyncio import sys import tempfile from pathlib import Path from .fixtures import FIXTURES, FixtureCase from .harness import ( CaseReport, all_passed, build_prompt_pair, render_report, verify_case, write_report, ) def _select(genre: str | None) -> list[FixtureCase]: if genre is None: return list(FIXTURES) picked = [c for c in FIXTURES if c.genre == genre] if not picked: raise SystemExit(f"没有题材为 {genre!r} 的 fixture;可选:{[c.genre for c in FIXTURES]}") return picked def _default_out(live: bool) -> Path: name = "craft_ab_live.md" if live else "craft_ab_dry.md" return Path(tempfile.gettempdir()) / name async def _run(args: argparse.Namespace) -> int: cases = _select(args.genre) reports: list[CaseReport] = [] for case in cases: pair = await build_prompt_pair(case) reports.append(CaseReport(case=case, pair=pair, checks=verify_case(case, pair))) failed = [(r.case.slug, c) for r in reports for c in r.checks if not c.passed] if not args.quiet: for r in reports: status = "OK" if all_passed(r.checks) else "FAIL" print(f"[dry] {r.case.slug:10s} {r.case.genre} 第{r.pair.chapter_no}章 {status}") for slug, chk in failed: print(f"[dry][FAIL] {slug} · {chk.name}: {chk.detail}", file=sys.stderr) if args.live: if failed: print("[live] dry 校验未全绿,拒绝进入付费生成——先修正注入。", file=sys.stderr) return 1 from .live import run_live, summarize print("[live] 开始真实生成(付费)……", flush=True) reports = await run_live(reports) print("\n[live] 摘要:\n" + summarize(reports)) out = Path(args.out) if args.out else _default_out(args.live) write_report(render_report(reports, live=args.live), out) print(f"\n[out] 报告写至 {out}") return 1 if failed else 0 def main() -> None: parser = argparse.ArgumentParser( prog="python -m tests.craft_eval", description="craft A/B 成稿质量前后对照评估(灵感 F2)", ) parser.add_argument( "--live", action="store_true", help="真实调用付费 LLM 生成两版正文 + 五维裁判(默认 dry,免真实 LLM)", ) parser.add_argument("--genre", default=None, help="只跑指定题材(如 仙侠/都市/悬疑)") parser.add_argument("--out", default=None, help="报告输出路径(默认写系统临时目录)") parser.add_argument("--quiet", action="store_true", help="减少 stdout 打印") args = parser.parse_args() if args.live: # get_settings() 按 cwd 读 .env;确保 cwd = 仓库根(本文件在 tests/craft_eval/ 下三层)。 import os os.chdir(Path(__file__).resolve().parents[2]) raise SystemExit(asyncio.run(_run(args))) if __name__ == "__main__": main()