ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+640
View File
@@ -0,0 +1,640 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
实验 5-2:用代码生成工具提升逻辑思考能力
对比在三种模式下求解「骑士与无赖」(Knights & Knaves) 谜题的准确率:
1) 纯思考(pure) —— LLM 仅靠自然语言链式推理直接给出答案;
2) 代码辅助(code) —— LLM 配备 Code Interpreter(预装 python-constraint)
把谜题形式化为约束满足问题(CSP),调用求解器搜索答案;
3) 约束求解(solver) —— 【离线,无需 API】直接用 python-constraint 求解结构化
陈述,作为确定性基线(理论上 100% 正确)。
结论预期:约束求解把逻辑推理外包给确定性求解器,准确率应达 90%+
且显著高于纯思考模式(纯思考在多人、含计数/自指的谜题上容易出错)。
用法:
# 离线约束求解基线(不花钱、不联网,演示核心论点):
python demo.py --mode solver
# LLM 对照实验(需要 OPENAI_API_KEY)
export OPENAI_API_KEY=your-openai-api-key
python demo.py # 默认 both:跑 纯思考 vs 代码辅助 全部题目
python demo.py --mode pure # 只跑纯思考
python demo.py --limit 4 # 只跑前 4 题(省钱冒烟测试)
python demo.py --max-people 3 # 只跑不超过 3 人的谜题(按难度筛选)
python demo.py --model gpt-4o-mini # 指定模型(默认 gpt-4o-mini)
python demo.py --puzzles my.json # 换一份谜题数据集
"""
import argparse
import datetime as dt
import json
import math
import os
import re
import sys
import time
from pathlib import Path
from csp_solver import solve_labeled
from sandbox import run_python
# ---- 读取 .env(如果存在)。避免额外依赖,手写一个极简解析器。----
def _load_dotenv(path=".env"):
if not os.path.exists(path):
return
with open(path, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'"))
_load_dotenv()
MODEL = os.environ.get("MODEL", "gpt-4o-mini")
PROVIDER = "unknown"
# --- 通用 OpenRouter 兜底:无直连 key 时自动改走 OpenRouter ---
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
def map_model_to_openrouter(model: str) -> str:
"""把直连模型名映射为 OpenRouter 上的 id(非可映射 id 统一兜底到当前廉价旗舰)。"""
if not model or "/" in model:
return model or "openai/gpt-5.6-luna"
m = model.lower()
if m.startswith(("gpt-", "o1", "o3", "o4")):
return "openai/" + model
if m.startswith("claude"):
if "haiku" in m:
return "anthropic/claude-haiku-4.5"
if "sonnet" in m:
return "anthropic/claude-sonnet-4.6"
return "anthropic/claude-opus-4.8"
if m.startswith("gemini"):
return "google/" + model
return "openai/gpt-5.6-luna"
def build_client_and_model(provider="auto"):
"""构造 OpenAI 客户端并返回 (client, model)。
- 有 OPENAI_API_KEY:直连(默认模型 gpt-4o-mini 是普通 gpt id,可直连 OpenAI)。
仅当模型是 gpt-5.x 且同时设置了 OPENROUTER_API_KEY 时才优先走 OpenRouter
(直连 gpt-5.x 需组织实名认证)。
- 无 OPENAI_API_KEY 但有 OPENROUTER_API_KEY:整体改走 OpenRouter。
"""
from openai import OpenAI
global MODEL, PROVIDER
choices = {
"ollama": ("ollama", os.environ.get(
"OLLAMA_BASE_URL", "http://127.0.0.1:11434/v1"
), MODEL),
"openai": (os.environ.get("OPENAI_API_KEY"),
os.environ.get("OPENAI_BASE_URL"), MODEL),
"openrouter": (os.environ.get("OPENROUTER_API_KEY"),
OPENROUTER_BASE_URL, map_model_to_openrouter(MODEL)),
"moonshot": (os.environ.get("MOONSHOT_API_KEY"),
"https://api.moonshot.cn/v1", MODEL),
"ark": (os.environ.get("ARK_API_KEY"),
"https://ark.cn-beijing.volces.com/api/v3", MODEL),
}
if provider == "auto":
provider = next(
(name for name in ("openai", "openrouter", "moonshot", "ark")
if choices[name][0]),
"openai",
)
if provider not in choices:
raise ValueError(f"unsupported provider: {provider}")
api_key, base_url, MODEL = choices[provider]
if not api_key:
raise SystemExit(f"错误:provider={provider} 缺少对应 API key")
PROVIDER = provider
kw = {"api_key": api_key, "timeout": 180.0, "max_retries": 5}
if base_url:
kw["base_url"] = base_url
return OpenAI(**kw), MODEL
def _reasoning(model: str) -> bool:
"""推理模型(gpt-5 / o 系列 / *thinking 等)不接受 temperature=0。"""
return any(k in (model or "").lower()
for k in ("gpt-5", "o1", "o3", "o4", "thinking", "reasoner", "kimi-k3"))
# run_python 工具的 function calling 定义
TOOLS = [{
"type": "function",
"function": {
"name": "run_python",
"description": (
"在预装了 python-constraint 库的沙箱中执行 Python 代码,返回 stdout/stderr。"
"用它把逻辑谜题建模为约束满足问题并求解。记得用 print() 打印结果。"
),
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "要执行的完整 Python 代码"}
},
"required": ["code"],
},
},
}]
ANSWER_HINT = (
'推理结束后,请在最后单独用一行输出 JSON 形式的最终答案,'
'键为每个居民的名字,值为 "knight""knave",例如:'
'{"A": "knight", "B": "knave"}'
)
PURE_SYSTEM = (
"你是逻辑推理专家。在「骑士与无赖」谜题中,骑士永远说真话,无赖永远说假话。"
"请仅凭自己的推理,逐步分析每位居民的身份,找出满足所有陈述的唯一解。\n" + ANSWER_HINT
)
CODE_SYSTEM = (
"你是逻辑推理专家,擅长把谜题转化为形式化约束并用代码求解。"
"在「骑士与无赖」谜题中,骑士永远说真话,无赖永远说假话。\n"
"请务必使用 run_python 工具,用 python-constraint 库把谜题建模为约束满足问题(CSP)来求解。\n\n"
"【最关键的建模规则】不要把某人的陈述直接当成事实约束!"
"正确做法是对每位居民 X 加一条【双条件(等价)约束】:\n"
" X 的布尔值 == (X 那句话在语义上为真)\n"
"含义:X 是骑士(True) 当且仅当 他的话为真;X 是无赖(False) 当且仅当 他的话为假。\n"
"这条规则对每一句话都适用,包括计数类('恰好有两个骑士')和自指类('我和 B 同类')的陈述——"
"都要写成 `X == (那句话的真值表达式)`,绝不能把 `(那句话的真值表达式)` 单独当作硬约束。\n\n"
"示例(设 True=骑士)\n"
" from constraint import Problem\n"
" p = Problem()\n"
" for name in ['A','B','C']:\n"
" p.addVariable(name, [True, False])\n"
" # A 说'我们中恰好有一个骑士' -> A == ( (A+B+C)==1 )\n"
" p.addConstraint(lambda a,b,c: a == ((a+b+c)==1), ['A','B','C'])\n"
" # B 说'C 是无赖' -> B == (not C)\n"
" p.addConstraint(lambda b,c: b == (not c), ['B','C'])\n"
" # C 说'我和 A 是同一类人' -> C == (C == A)\n"
" p.addConstraint(lambda a,c: c == (c == a), ['A','C'])\n"
" for s in p.getSolutions():\n"
" print({k:('knight' if v else 'knave') for k,v in s.items()})\n\n"
"步骤:1) 每人一个布尔变量;2) 每句话写成上面的双条件约束;"
"3) 调用 getSolutions() 枚举所有解并 print。\n"
"最终答案必须严格采用求解器打印出的解,不要用自己的直觉去推翻它。"
"若求解器输出为空,说明约束建错了(很可能漏了双条件),请检查并重跑。\n" + ANSWER_HINT
)
def parse_answer(text, names):
"""从模型输出里提取最后一个形如 {name: knight/knave} 的 JSON 答案。"""
norm = {
"knight": "knight",
"knave": "knave",
"骑士": "knight",
"无赖": "knave",
"true": "knight",
"false": "knave",
"1": "knight",
"0": "knave",
}
# 找出所有 {...} 片段,从后往前尝试解析
for m in reversed(list(re.finditer(r"\{[^{}]*\}", text))):
try:
obj = json.loads(m.group(0))
except json.JSONDecodeError:
try:
import ast
obj = ast.literal_eval(m.group(0))
except (SyntaxError, ValueError):
continue
if not isinstance(obj, dict):
continue
got = {}
for n in names:
if n not in obj:
break
v = str(obj[n]).strip().lower()
v = norm.get(v, norm.get(str(obj[n]).strip(), None))
if v is None:
break
got[n] = v
else:
return got
return None
def call_model(client, system, user, use_tools):
"""Run one trajectory and retain credential-free provider receipts."""
messages = [{"role": "system", "content": system},
{"role": "user", "content": user}]
codes = []
receipts = []
for turn in range(8): # 最多 8 轮,防止无限循环
kwargs = (dict(model=MODEL, messages=messages, temperature=1, max_tokens=8192)
if _reasoning(MODEL)
else dict(model=MODEL, messages=messages, temperature=0))
if use_tools:
kwargs.update(
tools=TOOLS,
tool_choice="required" if not codes else "auto",
)
resp = client.chat.completions.create(**kwargs)
msg = resp.choices[0].message
usage = getattr(resp, "usage", None)
receipts.append({
"turn": turn + 1,
"response_id": getattr(resp, "id", None),
"response_model": getattr(resp, "model", None),
"finish_reason": getattr(resp.choices[0], "finish_reason", None),
"usage": {
"prompt_tokens": getattr(usage, "prompt_tokens", None),
"completion_tokens": getattr(usage, "completion_tokens", None),
"total_tokens": getattr(usage, "total_tokens", None),
"cached_prompt_tokens": getattr(
getattr(usage, "prompt_tokens_details", None),
"cached_tokens", None,
),
},
"tool_calls": len(getattr(msg, "tool_calls", None) or []),
})
if use_tools and msg.tool_calls:
messages.append(msg)
for tc in msg.tool_calls:
try:
code = json.loads(tc.function.arguments).get("code", "")
except json.JSONDecodeError:
code = ""
codes.append(code)
result = run_python(code)
messages.append({"role": "tool", "tool_call_id": tc.id,
"content": result})
continue
return msg.content or "", codes, receipts
return "", codes, receipts
def run_mode(client, puzzles, mode, existing=None, checkpoint=None):
"""跑一种 LLM 模式(pure/code),返回逐题记录列表。"""
system = CODE_SYSTEM if mode == "code" else PURE_SYSTEM
existing_by_id = {
record["id"]: record for record in (existing or [])
if record.get("id")
}
records = []
for p in puzzles:
previous = existing_by_id.get(p["id"])
if (
previous
and not previous.get("provider_error")
and previous.get("provider_receipts")
and (mode != "code" or previous.get("codes"))
):
record = previous
else:
started = time.monotonic()
try:
text, codes, receipts = call_model(
client, system, p["description"], mode == "code"
)
pred = parse_answer(text, p["names"])
record = dict(
id=p["id"], num=p["num_people"], pred=pred,
gold=p["solution"], correct=pred == p["solution"],
source=p.get("source"), codes=codes, text=text,
used_python_constraint=any(
re.search(r"(^|\s)(from|import)\s+constraint\b", code)
for code in codes
),
duration_s=round(time.monotonic() - started, 3),
provider_receipts=receipts,
provider_error=None,
)
except Exception as exc:
record = dict(
id=p["id"], num=p["num_people"], pred=None,
gold=p["solution"], correct=False,
source=p.get("source"), codes=[], text="",
used_python_constraint=False,
duration_s=round(time.monotonic() - started, 3),
provider_receipts=[],
provider_error=f"{type(exc).__name__}: {exc}",
)
records.append(record)
if checkpoint:
checkpoint(mode, records)
pred = record.get("pred")
correct = bool(record.get("correct"))
mark = "" if correct else ""
print(f" [{mode:6}] {p['id']} ({p['num_people']}人) {mark} "
f"预测={pred}")
return records
def run_solver(puzzles):
"""离线约束求解模式:直接用 python-constraint 求解结构化陈述,无需 LLM/API。"""
records = []
for p in puzzles:
struct = p.get("statements_struct")
if not struct:
sys.exit(f"错误:谜题 {p['id']} 缺少 statements_struct 字段,"
"请用新版 build_puzzles.py 重新生成 puzzles.json。")
sols = solve_labeled(p["names"], struct)
pred = sols[0] if len(sols) == 1 else None
correct = pred == p["solution"]
records.append(dict(id=p["id"], num=p["num_people"], pred=pred,
gold=p["solution"], correct=correct,
codes=[], text="", num_solutions=len(sols)))
mark = "" if correct else ""
print(f" [solver] {p['id']} ({p['num_people']}人) {mark} "
f"解数={len(sols)} 预测={pred}")
return records
LABELS = {"pure": "纯思考", "code": "代码辅助", "solver": "约束求解"}
def _wilson(successes, total, z=1.959963984540054):
if total <= 0:
return [None, None]
p = successes / total
denominator = 1 + z * z / total
center = (p + z * z / (2 * total)) / denominator
half = z * math.sqrt(p * (1 - p) / total + z * z / (4 * total * total)) / denominator
return [center - half, center + half]
def paired_statistics(pure, code):
"""Preregistered paired accuracy analysis (exact McNemar/binomial test)."""
if [r["id"] for r in pure] != [r["id"] for r in code]:
raise ValueError("paired modes do not contain the same ordered task ids")
pure_only = sum(a["correct"] and not b["correct"] for a, b in zip(pure, code))
code_only = sum(not a["correct"] and b["correct"] for a, b in zip(pure, code))
discordant = pure_only + code_only
if discordant:
tail = sum(math.comb(discordant, i) for i in range(min(pure_only, code_only) + 1))
p_value = min(1.0, 2 * tail / (2 ** discordant))
else:
p_value = 1.0
code_ok = sum(r["correct"] for r in code)
pure_ok = sum(r["correct"] for r in pure)
code_accuracy = code_ok / len(code)
pure_accuracy = pure_ok / len(pure)
library_rate = sum(r["used_python_constraint"] for r in code) / len(code)
return {
"test": "two-sided exact McNemar/binomial test on discordant pairs",
"n": len(code),
"contingency": {"pure_only": pure_only, "code_only": code_only,
"discordant": discordant},
"pure_accuracy": pure_accuracy,
"code_accuracy": code_accuracy,
"accuracy_delta": code_accuracy - pure_accuracy,
"code_accuracy_wilson_95": _wilson(code_ok, len(code)),
"p_value": p_value,
"python_constraint_tool_use_rate": library_rate,
"acceptance": {
"code_accuracy_over_90_percent": code_accuracy > 0.90,
"code_significantly_higher_than_pure": (
code_accuracy > pure_accuracy and p_value < 0.05
),
"all_code_trajectories_used_python_constraint": library_rate == 1.0,
},
}
def campaign_completion(results, puzzles, manifest, mode):
"""Check exact protocol execution without requiring a positive result."""
expected_ids = [puzzle["id"] for puzzle in puzzles]
manifest_exact = bool(
manifest
and manifest.get("dataset") == "K-and-K/perturbed-knights-and-knaves"
and manifest.get("revision")
== "bc7ee75a15ee8196ccbdb7df3ab46284340412e2"
and (manifest.get("sampling") or {}).get("total") == 84
and (manifest.get("sampling") or {}).get("cells") == 42
and (manifest.get("sampling") or {}).get("per_cell") == 2
and len(manifest.get("source_files") or []) == 42
and manifest.get("label_validation")
== "all rows independently solved with python-constraint"
)
exact_coverage = (
len(expected_ids) == 84 and len(set(expected_ids)) == 84
)
errors = []
arm_checks = {}
for arm in ("pure", "code"):
records = results.get(arm) or []
arm_errors = [
{"id": record.get("id"), "error": record.get("provider_error")}
for record in records if record.get("provider_error")
]
errors.extend({"arm": arm, **error} for error in arm_errors)
arm_checks[f"all_{arm}_trajectories_complete"] = (
len(records) == 84
and [record.get("id") for record in records] == expected_ids
and not arm_errors
and all(record.get("provider_receipts") for record in records)
)
code_records = results.get("code") or []
checks = {
"mode_is_full_paired_campaign": mode == "both",
"exact_pinned_stratified_dataset_manifest": manifest_exact,
"all_84_unique_tasks_present": exact_coverage,
**arm_checks,
"zero_provider_errors": not errors,
"every_code_trajectory_used_python_constraint": (
len(code_records) == 84
and all(record.get("used_python_constraint") for record in code_records)
),
}
return {
"status": "complete" if all(checks.values()) else "incomplete",
"checks": checks,
"provider_errors": errors,
"expected_tasks_per_arm": 84,
}
def print_table(columns, puzzles):
"""打印多列准确率对比表。columns = [(mode, records), ...],顺序即列顺序。"""
accs = {m: sum(r["correct"] for r in recs) / len(recs) for m, recs in columns}
header = f"{'题号':<8}{'人数':<6}" + "".join(f"{LABELS[m]:<10}" for m, _ in columns)
print("\n" + "=" * 60)
print("准确率对比表")
print("=" * 60)
print(header)
print("-" * 60)
n = len(puzzles)
for i in range(n):
row = f"{puzzles[i]['id']:<8}{puzzles[i]['num_people']:<6}"
for _, recs in columns:
row += f"{('' if recs[i]['correct'] else ''):<10}"
print(row)
print("-" * 60)
tail = f"{'准确率':<8}{'':<6}" + "".join(
f"{accs[m]*100:>6.1f}% " for m, _ in columns)
print(tail)
print("=" * 60)
for m, recs in columns:
n_ok = sum(r["correct"] for r in recs)
print(f"{LABELS[m]:<6} 准确率: {accs[m]*100:5.1f}% ({n_ok}/{len(recs)})")
# 若同时有 solver/code 与 pure,报告提升幅度
baseline = next((m for m in ("pure",) if m in accs), None)
best = next((m for m in ("solver", "code") if m in accs), None)
if baseline and best and best != baseline:
print(f"提升({LABELS[best]} - {LABELS[baseline]}): "
f"{(accs[best]-accs[baseline])*100:+.1f} 个百分点")
def main():
global MODEL
ap = argparse.ArgumentParser(
description="实验 5-2:对比纯思考 / 代码辅助 / 约束求解 三种模式求解"
"「骑士与无赖」逻辑谜题的准确率",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__)
ap.add_argument("--mode", choices=["both", "pure", "code", "solver"],
default="both",
help="运行模式:both=纯思考+代码辅助(默认);pure=仅纯思考;"
"code=仅代码辅助;solver=离线约束求解基线(无需 API)")
ap.add_argument("--model", default=MODEL,
help=f"LLM 模型名(默认 {MODEL}solver 模式忽略)")
ap.add_argument(
"--provider",
choices=["auto", "ollama", "openai", "openrouter", "moonshot", "ark"],
default="auto",
help="explicit API provider; recorded in the saved evidence",
)
ap.add_argument("--limit", type=int, default=0,
help="只跑前 N 题(0=全部)")
ap.add_argument("--min-people", type=int, default=0,
help="只跑居民数 >= 该值的谜题(按难度筛选,0=不限)")
ap.add_argument("--max-people", type=int, default=0,
help="只跑居民数 <= 该值的谜题(按难度筛选,0=不限)")
ap.add_argument("--puzzles", default="puzzles.json",
help="谜题数据集路径(默认 puzzles.json)")
ap.add_argument("--output", default="last_run.json",
help="逐题完整记录的输出路径(默认 last_run.json)")
ap.add_argument(
"--manifest", default=None,
help="optional dataset manifest; defaults to the matching .manifest.json",
)
ap.add_argument(
"--resume", action="store_true",
help="resume successful per-arm rows from OUTPUT.checkpoint.json",
)
args = ap.parse_args()
MODEL = args.model
with open(args.puzzles, encoding="utf-8") as f:
puzzles = json.load(f)
if args.min_people:
puzzles = [p for p in puzzles if p["num_people"] >= args.min_people]
if args.max_people:
puzzles = [p for p in puzzles if p["num_people"] <= args.max_people]
if args.limit:
puzzles = puzzles[:args.limit]
if not puzzles:
sys.exit("错误:筛选后没有任何谜题,请放宽 --min-people/--max-people/--limit。")
# solver 模式完全离线,不需要 API;其余模式需要 OPENAI_API_KEY。
llm_modes = {"both": ["pure", "code"], "pure": ["pure"],
"code": ["code"], "solver": []}[args.mode]
results = {}
checkpoint_path = Path(str(args.output) + ".checkpoint.json")
resumed_results = {}
if args.resume and checkpoint_path.is_file():
prior = json.loads(checkpoint_path.read_text(encoding="utf-8"))
if prior.get("provider") != args.provider or prior.get("model") != MODEL:
raise ValueError("resume rejected: provider/model changed")
if prior.get("puzzle_ids") != [puzzle["id"] for puzzle in puzzles]:
raise ValueError("resume rejected: selected puzzle set changed")
resumed_results = prior.get("results") or {}
checkpoint_results = {
arm: list(records) for arm, records in resumed_results.items()
}
def persist_checkpoint(arm, records):
checkpoint_results[arm] = list(records)
checkpoint_path.parent.mkdir(parents=True, exist_ok=True)
checkpoint_path.write_text(json.dumps({
"schema_version": "1.0", "experiment": "5-2",
"provider": args.provider, "model": MODEL,
"puzzle_ids": [puzzle["id"] for puzzle in puzzles],
"results": checkpoint_results,
}, ensure_ascii=False, indent=2), encoding="utf-8")
if args.mode == "solver":
print(f"离线约束求解基线 题目数:{len(puzzles)}\n")
print("== 约束求解(solver,离线) ==")
results["solver"] = run_solver(puzzles)
else:
client, MODEL = build_client_and_model(args.provider)
print(f"供应商:{PROVIDER} 模型:{MODEL} 题目数:{len(puzzles)} 模式:{args.mode}\n")
for m in llm_modes:
print(f"== {LABELS[m]}({m}) ==")
results[m] = run_mode(
client, puzzles, m,
existing=resumed_results.get(m),
checkpoint=persist_checkpoint,
)
print()
# ---- 准确率对比表(按 pure -> code -> solver 的固定列序) ----
columns = [(m, results[m]) for m in ["pure", "code", "solver"] if m in results]
print_table(columns, puzzles)
# ---- 展示一题的约束建模代码与求解结果 ----
code_recs = results.get("code")
if code_recs:
sample = next((r for r in code_recs if r["correct"] and r["codes"]), None)
if sample:
print("\n" + "=" * 60)
print(f"示例:{sample['id']} 的约束建模代码(模型生成)")
print("=" * 60)
print(sample["codes"][0])
print("-- 求解 & 最终答案 --")
print(f"预测={sample['pred']} 真值={sample['gold']}")
# 保存完整记录,便于复盘
puzzle_path = Path(args.puzzles)
manifest_path = (
Path(args.manifest) if args.manifest
else puzzle_path.with_name(puzzle_path.stem + ".manifest.json")
)
manifest = None
manifest_sha256 = None
if manifest_path.is_file():
import hashlib
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
manifest_sha256 = hashlib.sha256(manifest_path.read_bytes()).hexdigest()
payload = dict(
schema_version="2.0",
experiment="5-2",
generated_at_utc=dt.datetime.now(dt.timezone.utc).isoformat(),
provider=PROVIDER,
model=MODEL,
mode=args.mode,
tasks=len(puzzles),
dataset_manifest=manifest,
dataset_manifest_sha256=manifest_sha256,
)
for m, recs in results.items():
payload[m] = recs
payload[f"{m}_acc"] = sum(r["correct"] for r in recs) / len(recs)
if "pure" in results and "code" in results:
payload["paired_analysis"] = paired_statistics(results["pure"], results["code"])
payload["completion"] = campaign_completion(
results, puzzles, manifest, args.mode
)
payload["official_complete"] = payload["completion"]["status"] == "complete"
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
print(f"\n完整逐题记录已保存到 {args.output}")
if __name__ == "__main__":
main()