Benchmark pipeline, LLM-as-judge and bakeoff¶
Modules:
tools.agents.benchmarks.*·tools.llm.model_bakeoff·tools.llm.bakeoff_tasks·tools.llm.bakeoff_reporting·tools.llm.rank_models
Overview¶
The benchmark infrastructure in agentic-tools serves two related but distinct purposes:
-
Model bakeoff (
tools/llm/model_bakeoff.py) — a repeatable capability comparison that runs a fixed task set across discovered models and recommends environment variable values forDEEP_RESEARCH_SMALL_MODELandDEEP_RESEARCH_HEAVY_MODEL. -
Benchmark runner + LLM-as-judge (
tools/agents/benchmarks/) — a general-purpose evaluation framework that executes benchmark tasks (HumanEval, SWE-bench, custom) against a model and scores the output on a five-dimension rubric using a second LLM as the judge.
The two systems share LLMClient as their inference backend and ModelProbe for availability
checking, but they operate independently.
Model bakeoff¶
Purpose¶
The bakeoff answers the question: "Given the models available in this environment right now,
which one should be assigned to the SMALL (fast/cheap) and HEAVY (quality/capable) tiers?"
It is designed to run from PowerShell on Windows without WSL, using whatever models are already available locally or via configured cloud endpoints.
Running a bakeoff¶
# Default: discover local ONNX + Ollama + AI Toolkit + OpenAI models
python -m tools.llm.model_bakeoff
# Explicit models
python -m tools.llm.model_bakeoff --models local:phi4 gh:gpt-4o-mini gh:llama-4-scout
# Specific providers
python -m tools.llm.model_bakeoff --providers local_onnx,github_models,ollama
# Enable remote providers
python -m tools.llm.model_bakeoff --providers openai,gemini --allow-remote
# Save alignment recommendations to env file
python -m tools.llm.model_bakeoff --write-env runs/alignment.env --out-dir reports/bakeoff
# Dry run: probe and select models but don't run prompts
python -m tools.llm.model_bakeoff --dry-run --verbose
# Limit models tested
python -m tools.llm.model_bakeoff --max-models 8 --max-models-per-provider 3
# Force fresh probe (ignore cache)
python -m tools.llm.model_bakeoff --force-probe
CLI options¶
| Flag | Default | Description |
|---|---|---|
--providers |
local_onnx,ollama,ai_toolkit,openai |
Comma-separated provider names |
--models |
(empty) | Explicit model IDs; skips discovery |
--max-models-per-provider |
6 |
Cap on models selected per provider |
--max-models |
24 |
Global cap on runnable models tested |
--temperature |
0.1 |
Generation temperature for all task calls |
--max-tokens |
900 |
Max tokens per prompt call |
--out-dir |
reports/model-bakeoff |
Output directory for JSON + Markdown reports |
--write-env |
(empty) | Optional .env file path for alignment recommendations |
--force-probe |
off | Force fresh ModelProbe checks |
--include-openai-local |
off | Include OPENAI_BASE_URL models |
--allow-remote |
off | Set PROMPTEVAL_ALLOW_REMOTE=1 |
--dry-run |
off | Probe/select only; do not run prompts |
--verbose / -v |
off | Debug-level discovery/probe logs |
Bakeoff task set¶
Tasks are defined as TaskSpec dataclasses in tools/llm/bakeoff_tasks.py. Each task specifies
a prompt, expected response format, required keys (for JSON responses), required terms (for text
responses), and a scoring weight.
| Task ID | Title | Format | Weight |
|---|---|---|---|
workflow_strategy |
Workflow Pattern Selection | JSON | 1.0 |
architecture |
Architecture Plan | JSON | 1.1 |
implementation_plan |
Implementation Plan | JSON | 1.0 |
code_task |
Coding Practicality | Text + code | 0.9 |
System prompt used for all bakeoff tasks:
"You are a pragmatic senior AI/software architect. Provide concise, technically precise outputs."
Scoring formula¶
Each task is scored out of 100 points:
score = 0
if response is non-empty: +20 pts
if JSON expected and valid: +30 pts
if required JSON keys present: +30 pts × (keys_present / keys_expected)
if required terms present: +20 pts × (terms_present / terms_expected)
latency penalty: -min(elapsed_s, 120) × 0.15
score = clamp(score, 0.0, 100.0)
Model overall score (combines task performance with latency):
task_score = weighted average of per-task scores
success_rate = successful_tasks / total_tasks
overall_score = clamp(task_score + (success_rate × 10.0) - (avg_latency × 0.2), 0.0, 100.0)
Results are sorted descending by (overall_score, task_score, success_rate, -avg_latency).
Alignment recommendation¶
After ranking, _recommend_alignment() selects:
- Heavy model: rank 1 (highest overall score)
- Small model: rank 1 by
(task_score - avg_latency × 0.8)— favors speed with acceptable quality for interactive workflows
Output written to --write-env file:
# Generated by tools/llm/model_bakeoff.py
DEEP_RESEARCH_SMALL_MODEL=gh:gpt-4o-mini
DEEP_RESEARCH_HEAVY_MODEL=local:phi4
AGENTIC_MODEL_TIER_2=gh:gpt-4o-mini
AGENTIC_MODEL_TIER_3=local:phi4
AGENTIC_MODEL_TIER_4=local:phi4
Bakeoff report formats¶
Two files are written per run, suffixed with a UTC timestamp (YYYYMMDD-HHMMSSz):
model_bakeoff_<ts>.json— full results with per-task details, probe report, discovery datamodel_bakeoff_<ts>.md— human-readable ranking table + alignment block
Markdown report example:
# Model Bakeoff Report
- Generated: `2026-05-02T14:23:00Z`
- Models tested: `12`
- Runnable models: `8`
## Recommended Alignment
```env
DEEP_RESEARCH_SMALL_MODEL=gh:gpt-4o-mini
DEEP_RESEARCH_HEAVY_MODEL=local:phi4
Ranking¶
For the full probe, limit-check, and ranking workflow — including the provider score table
generated from rank_models.py — see
Provider rate limits and model ranking.
LLM-as-Judge Evaluator¶
Overview¶
tools/agents/benchmarks/llm_evaluator.py implements a structured evaluation protocol where a
second LLM (the "judge") scores the output of a model against a gold standard. This is benchmark-
agnostic: the same evaluator works for code generation, architecture design, or any task with a
gold-standard dictionary.
Scoring Rubric¶
Scores are on a 0.0–10.0 scale per dimension:
| Score | Label | Meaning |
|---|---|---|
| 10.0 | Perfect | Flawless execution, all requirements met, exceeds expectations |
| 9.0 | Excellent | Near-perfect, trivial gaps only, production-ready |
| 8.0 | Very Good | Strong output, 1–2 minor issues, high quality |
| 7.0 | Good | Solid work, meets most requirements, some improvements possible |
| 6.0 | Adequate | Acceptable output, notable gaps but fundamentally correct |
| 5.0 | Fair | Partially correct, significant gaps, needs improvement |
| 4.0 | Below Average | Multiple issues, incomplete, misses key requirements |
| 3.0 | Poor | Major deficiencies, barely usable |
| 2.0 | Very Poor | Fundamental problems, largely incorrect |
| 1.0 | Extremely Poor | Minimal effort, almost unusable |
| 0.0 | Failed | Did not produce relevant output or completely wrong |
Evaluation Dimensions¶
Five weighted dimensions contribute to the overall score:
| Dimension | Weight | Description |
|---|---|---|
completeness |
0.25 | Does the output address all requirements? |
correctness |
0.25 | Is it technically correct, following best practices? |
quality |
0.20 | Is it well-structured, clear, and maintainable? |
specificity |
0.15 | Does it provide specific, actionable details? |
alignment |
0.15 | Does it align with the gold standard expectations? |
Overall score = Σ(dimension_score × weight). Maximum 10.0.
Grade assignment:
| Range | Grade |
|---|---|
| 9.0–10.0 | A+ |
| 8.0–8.9 | A |
| 7.0–7.9 | B |
| 6.0–6.9 | C |
| 5.0–5.9 | D |
| < 5.0 | F |
Using the Evaluator¶
from tools.agents.benchmarks.llm_evaluator import evaluate_with_llm, EvaluationResult
gold_standard = {
"required_components": ["authentication", "rate_limiting", "error_handling"],
"key_decisions": ["JWT tokens", "Redis for sessions", "exponential backoff"],
"expected_output": "FastAPI service with full auth pipeline",
}
result: EvaluationResult = evaluate_with_llm(
task_id="auth_service_design",
task_prompt="Design a secure authentication service for a multi-tenant SaaS platform.",
generated_output=model_response,
gold_standard=gold_standard,
model="gh:gpt-4o-mini", # model that produced generated_output
benchmark_id="architecture_bench",
evaluator_model="gh:gpt-4o", # judge model (defaults to same as model)
verbose=True,
)
print(f"Score: {result.overall_score:.1f}/10.0 Grade: {result.grade}")
print(f"Strengths: {result.strengths}")
print(f"Weaknesses: {result.weaknesses}")
for dim_name, dim in result.dimension_scores.items():
print(f" {dim_name}: {dim.score:.1f} (weight {dim.weight}) — {dim.reasoning[:80]}")
Evaluation Prompt Structure¶
build_evaluation_prompt() constructs a prompt containing:
- Task Prompt — the original task given to the model under evaluation
- Gold Standard Expectations — summary of
required_components,required_patterns,key_decisions,expected_output,api_endpoints,database_tables,test_cases, and any additional gold standard keys - Generated Output — the model output to be scored
- Evaluation Dimensions — the five weighted dimensions with descriptions
- Scoring Rubric — the 0.0–10.0 scale with labels
- Output Format — instructs the judge to return a strict JSON object
Required JSON output structure from the judge:
{
"dimension_scores": {
"completeness": {
"score": 8.5,
"reasoning": "All major auth components addressed, OAuth flow missing",
"evidence": ["'JWT token validation' mentioned", "'rate limiting' implemented"]
},
"correctness": { "score": 9.0, "reasoning": "...", "evidence": [...] },
"quality": { "score": 7.5, "reasoning": "...", "evidence": [...] },
"specificity": { "score": 6.5, "reasoning": "...", "evidence": [...] },
"alignment": { "score": 8.0, "reasoning": "...", "evidence": [...] }
},
"strengths": ["Comprehensive JWT implementation", "Good error handling patterns"],
"weaknesses": ["Missing OAuth2 flow", "No mention of token refresh strategy"],
"improvement_suggestions": ["Add OAuth2 provider integration", "Document token TTLs"],
"key_findings": ["Strong security posture", "Production-ready except for OAuth gap"]
}
Response Parsing¶
parse_evaluation_response() handles the judge's output robustly:
- Strip markdown code fences if present
- Attempt direct
json.loads() - Regex search for
{...}if direct parse fails - Return an empty-structure dict on parse failure, setting
parse_error: True
This ensures a score of 0.0 is returned rather than crashing when the judge produces malformed output.
EvaluationResult Dataclass¶
@dataclass
class EvaluationResult:
task_id: str
model: str # model under evaluation
benchmark_id: str
timestamp: str
dimension_scores: dict[str, DimensionScore] # per-dimension results
overall_score: float # 0.0–10.0
grade: str # "A+", "A", "B", ..., "F"
task_prompt: str
generated_output: str
gold_standard_summary: str
duration_seconds: float
evaluator_model: str # judge model used
strengths: list[str]
weaknesses: list[str]
improvement_suggestions: list[str]
key_findings: list[str]
Benchmark Runner¶
Architecture¶
BenchmarkRegistry BenchmarkConfig (preset or custom)
│ │
└──── load_benchmark() ──────┘
│
▼
BenchmarkTask list
│
for each task × model:
│
LLMClient.generate_text()
│
▼
evaluate_task_output_llm()
│
▼
EvaluationResult
│
save_evaluation_report()
Quick Start¶
# Interactive mode (prompts for benchmark, model, limit)
python -m tools.agents.benchmarks.runner
# Direct mode
python -m tools.agents.benchmarks.runner \
--benchmark humaneval \
--model gh:gpt-4o-mini \
--limit 10
# Preset configurations
python -m tools.agents.benchmarks.runner --preset quick-test
python -m tools.agents.benchmarks.runner --preset full-eval
# List available benchmarks, models, presets
python -m tools.agents.benchmarks.runner --list-benchmarks
python -m tools.agents.benchmarks.runner --list-models
python -m tools.agents.benchmarks.runner --list-presets
# Clear benchmark data cache
python -m tools.agents.benchmarks.runner --clear-cache
run_benchmark(config) API¶
from tools.agents.benchmarks.runner import run_benchmark
from tools.agents.benchmarks.registry import BenchmarkConfig
config = BenchmarkConfig(
benchmark_id="humaneval",
model="gh:gpt-4o-mini",
limit=20, # tasks to run
output_dir="reports/eval",
evaluator_model="gh:gpt-4o", # judge model
verbose=True,
)
results = run_benchmark(config)
print(results["summary"]["avg_score"]) # 0.0–10.0
print(results["summary"]["pass_rate"]) # fraction scoring >= 6.0
print(results["summary"]["total_tasks"])
Evaluation Pipeline¶
evaluation_pipeline.evaluate_task_output_llm() is the bridge between the runner and the LLM
judge. It:
- Looks up the gold standard for the task from
TEST_TASKS(if available) - Falls back to
task.expected_outputandtask.test_casesif no gold standard is registered - Calls
evaluate_with_llm()with the resolved gold standard - Prints a score line or full report depending on
verbose - Saves the evaluation report to
output_dirif provided - Falls back to legacy pattern-matching evaluation if the LLM evaluator is unavailable
from tools.agents.benchmarks.evaluation_pipeline import evaluate_task_output_llm
result_dict = evaluate_task_output_llm(
task=benchmark_task,
output=generated_code,
model="gh:gpt-4o-mini",
benchmark_id="humaneval",
verbose=False,
output_dir=Path("reports/eval"),
evaluator_model="gh:gpt-4o",
)
Research Gating¶
The agentic-v2-eval package enforces quality gates on research outputs before they are accepted
into the research library:
| Gate | Threshold | Description |
|---|---|---|
coverage_score |
≥ 0.80 | Fraction of required topics covered |
source_quality_score |
≥ 0.80 | Proportion of Tier A/B sources cited |
These thresholds are applied in agentic-v2-eval using EvaluationResult.overall_score as an
input alongside domain-specific coverage metrics. A research output that passes the LLM judge
with an overall score but fails a coverage gate is rejected and routed back to the agent for
revision.
# Example gate check (in agentic-v2-eval)
if (
result.overall_score >= 6.0 # minimum "Adequate" grade
and coverage_score >= 0.80
and source_quality_score >= 0.80
):
accept_research_output(result)
else:
request_revision(result, coverage_gaps)
Reporting¶
save_evaluation_report¶
Saves an individual EvaluationResult as both JSON and Markdown to output_dir:
output_dir/
├── task_<task_id>_eval.json # full result dict
└── task_<task_id>_eval.md # human-readable report
summarize_batch_results¶
Aggregates a list of EvaluationResult into a BatchEvaluationSummary:
from tools.agents.benchmarks.llm_evaluator import summarize_batch_results
summary = summarize_batch_results(results)
print(summary.total_tasks) # int
print(summary.avg_overall_score) # float 0.0–10.0
print(summary.pass_rate) # fraction with overall_score >= 6.0
print(summary.grade_distribution) # {"A+": 2, "A": 5, "B": 8, ...}
print(summary.top_model) # model with highest avg score
print_evaluation_report¶
Prints a formatted evaluation report to stdout:
from tools.agents.benchmarks.llm_evaluator import print_evaluation_report
print_evaluation_report(result, verbose=True)
Outputs: - Dimension scores table with reasoning snippets - Strengths and weaknesses lists - Improvement suggestions - Key findings
Module Dependency Map¶
graph LR
bakeoff["model_bakeoff.py\nBakeoff runner"] --> tasks["bakeoff_tasks.py\nTaskSpec + TASKS"]
bakeoff --> reporting["bakeoff_reporting.py\n_write_reports"]
bakeoff --> probe["model_probe.py\nModelProbe"]
bakeoff --> client["llm_client.py\nLLMClient"]
runner["runner.py\nrun_benchmark"] --> pipeline["evaluation_pipeline.py"]
runner --> registry["registry.py\nBenchmarkConfig"]
runner --> loader["loader.py\nload_benchmark"]
pipeline --> llm_eval["llm_evaluator.py\nevaluate_with_llm"]
pipeline --> legacy["test_tasks.py\npattern matching (optional)"]
llm_eval --> models["evaluator_models.py\nEvaluationResult · DimensionScore"]
llm_eval --> eval_report["evaluator_reporting.py\nprint · save · summarize"]
llm_eval --> client
rank["rank_models.py\nRank by probe+limits"] --> probe
Testing¶
All benchmark tests in tools/tests/ are mocked — no live inference or judge calls.
| Test file | Coverage |
|---|---|
test_llm_evaluator.py |
build_evaluation_prompt, parse_evaluation_response, evaluate_with_llm |
test_evaluation_pipeline.py |
evaluate_task_output_llm, get_gold_standard_for_task |
test_benchmark_pipeline.py |
run_benchmark, config loading, preset resolution |
test_workflow_pipeline.py |
extract_workflow_data, save_workflow_phases_md |
# Run benchmark tests only
python -m pytest tools/tests/test_llm_evaluator.py tools/tests/test_evaluation_pipeline.py -v
Common Patterns¶
Run bakeoff and apply recommendations¶
python -m tools.llm.model_bakeoff \
--providers local_onnx,ollama,github_models \
--write-env .env.alignment \
--out-dir reports/bakeoff
# Apply to environment
export $(cat .env.alignment | xargs)
Evaluate a single model output programmatically¶
from tools.agents.benchmarks.llm_evaluator import evaluate_with_llm
from tools.llm.llm_client import LLMClient
import os
os.environ["PROMPTEVAL_ALLOW_REMOTE"] = "1"
prompt = "Write a Python function that validates an email address using a regex."
output = LLMClient.generate_text("gh:gpt-4o-mini", prompt)
result = evaluate_with_llm(
task_id="email_validator",
task_prompt=prompt,
generated_output=output,
gold_standard={
"required_components": ["regex pattern", "return bool", "handle edge cases"],
"expected_output": "Python function returning True/False",
},
model="gh:gpt-4o-mini",
benchmark_id="coding_tasks",
evaluator_model="gh:gpt-4o",
)
print(f"{result.grade}: {result.overall_score:.1f}/10.0")
Run a batch evaluation with summary¶
from pathlib import Path
from tools.agents.benchmarks.llm_evaluator import summarize_batch_results, save_evaluation_report
results = []
for task_id, prompt, output, gold in task_triples:
result = evaluate_with_llm(
task_id=task_id, task_prompt=prompt,
generated_output=output, gold_standard=gold,
model="gh:gpt-4o-mini", benchmark_id="my_bench",
)
results.append(result)
save_evaluation_report(result, Path("reports/my_bench"))
summary = summarize_batch_results(results)
print(f"Avg score: {summary.avg_overall_score:.2f} Pass rate: {summary.pass_rate:.1%}")