Skip to content

Commit ee7c57f

Browse files
Gh-Novelclaude
andcommitted
feat(bench): reasoning-tools ablation benchmark with blind LLM judging
Fixed 6-question set, two arms (full toolset vs reasoning tools hidden), equal turn budgets. Two agent hosts: the local Qwen 3 agent and Claude Sonnet over MCP via the Claude Code CLI. Answer pairs are judged blind by Sonnet in both orderings; wins require consistency. Full per-question data committed in results*.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 9e8eefe commit ee7c57f

5 files changed

Lines changed: 894 additions & 0 deletions

File tree

‎benchmarks/judge_with_claude.py‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""
2+
Re-judge benchmark answer pairs with Claude Sonnet via the Claude Code CLI.
3+
4+
The generation arms stay local (Qwen 3 via Ollama) — this script only swaps
5+
the judge. Using a strong independent model removes the self-judging bias of
6+
the default harness (where the same local model generates and judges).
7+
8+
Each pair is judged blind in BOTH orderings; an arm wins only if Sonnet picks
9+
it both times. Inconsistent or explicit-tie verdicts count as ties.
10+
11+
Usage (after run_benchmark.py has written results.json):
12+
python benchmarks/judge_with_claude.py [--results PATH] [--model sonnet]
13+
14+
Requires the `claude` CLI (Claude Code) on PATH and an active subscription.
15+
"""
16+
17+
from __future__ import annotations
18+
19+
import argparse
20+
import json
21+
import os
22+
import re
23+
import subprocess
24+
25+
JUDGE_PROMPT = """You are an impartial judge. Two anonymous assistants answered the same question. Decide which answer is better overall, weighing factual accuracy, completeness, use of sources, and clarity. A longer answer is not automatically better.
26+
27+
QUESTION:
28+
{question}
29+
30+
ANSWER 1:
31+
{answer_1}
32+
33+
ANSWER 2:
34+
{answer_2}
35+
36+
Respond with ONLY a JSON object, no other text:
37+
{{"winner": 1 or 2 or 0, "reason": "<one sentence>"}}
38+
Use 0 only if the answers are genuinely indistinguishable in quality."""
39+
40+
41+
def ask_claude(prompt: str, model: str) -> tuple[int, str]:
42+
"""Run one judgment through the Claude Code CLI. Returns (winner, reason)."""
43+
out = subprocess.run(
44+
["claude", "-p", prompt, "--model", model],
45+
capture_output=True, text=True, timeout=120,
46+
)
47+
raw = out.stdout.strip()
48+
w = re.search(r'"winner"\s*:\s*(\d)', raw)
49+
r = re.search(r'"reason"\s*:\s*"([^"]*)"', raw)
50+
return (int(w.group(1)) if w else 0), (r.group(1) if r else raw[:120])
51+
52+
53+
def judge_pair(question: str, ans_reasoning: str, ans_ablated: str, model: str) -> dict:
54+
"""Both-orders blind judgment. Consistent winner or tie."""
55+
# Order 1: reasoning first
56+
w1, r1 = ask_claude(
57+
JUDGE_PROMPT.format(question=question, answer_1=ans_reasoning, answer_2=ans_ablated),
58+
model,
59+
)
60+
# Order 2: ablated first
61+
w2, r2 = ask_claude(
62+
JUDGE_PROMPT.format(question=question, answer_1=ans_ablated, answer_2=ans_reasoning),
63+
model,
64+
)
65+
pick1 = {1: "reasoning", 2: "ablated", 0: "tie"}[w1]
66+
pick2 = {1: "ablated", 2: "reasoning", 0: "tie"}[w2]
67+
verdict = pick1 if pick1 == pick2 else "tie"
68+
return {"verdict": verdict, "order1": pick1, "order2": pick2,
69+
"reasons": [r1, r2]}
70+
71+
72+
def main() -> None:
73+
parser = argparse.ArgumentParser(description="Re-judge benchmark pairs with Claude Sonnet")
74+
parser.add_argument("--results", default=os.path.join(
75+
os.path.dirname(os.path.abspath(__file__)), "results.json"))
76+
parser.add_argument("--model", default="sonnet")
77+
args = parser.parse_args()
78+
79+
with open(args.results, encoding="utf-8") as f:
80+
data = json.load(f)
81+
82+
wins = {"reasoning": 0, "ablated": 0, "tie": 0}
83+
for i, row in enumerate(data["results"], 1):
84+
if row["reasoning"].get("error") or row["ablated"].get("error"):
85+
row["claude_judge"] = {"verdict": "skipped (error)"}
86+
continue
87+
print(f"[{i}/{len(data['results'])}] judging: {row['question'][:60]}...", flush=True)
88+
result = judge_pair(
89+
row["question"], row["reasoning"]["answer"], row["ablated"]["answer"], args.model,
90+
)
91+
row["claude_judge"] = result
92+
wins[result["verdict"]] += 1
93+
print(f" {result['verdict']} (order1={result['order1']}, order2={result['order2']})",
94+
flush=True)
95+
96+
data["summary"]["claude_judge"] = {
97+
"model": args.model,
98+
"method": "blind, both orderings; win requires consistency",
99+
"wins": wins,
100+
}
101+
with open(args.results, "w", encoding="utf-8") as f:
102+
json.dump(data, f, indent=2, ensure_ascii=False)
103+
104+
judged = sum(wins.values())
105+
print(f"\n## Claude {args.model} verdicts ({judged} questions)\n")
106+
print("| Arm | Wins |")
107+
print("|---|---|")
108+
print(f"| With reasoning tools | **{wins['reasoning']}/{judged}** |")
109+
print(f"| Without (ablated) | **{wins['ablated']}/{judged}** |")
110+
print(f"| Tie / inconsistent | {wins['tie']}/{judged} |")
111+
112+
113+
if __name__ == "__main__":
114+
main()

0 commit comments

Comments
 (0)