Most agent evaluations fail because they measure trivia instead of work. To benchmark AI agents real-world tasks you need a harness that replays production constraints: erratic APIs, incomplete data, and tools that occasionally lie. This guide walks through a reproducible pipeline you can run in CI.
Step 1: Define tasks as executable contracts
A benchmark is only as honest as its spec. Pull 20–50 real traces from your production logs where a human or agent completed a task. Strip secrets, then encode each as a self-contained contract: input state, allowed tools, and a machine-checkable success predicate.
{
"task_id": "invoice_reconcile_4471",
"setup": {
"db_snapshot": "s3://traces/invoices/4471.sqlite",
"mock_apis": ["vendor_api", "erp_api"]
},
"agent_input": "Reconcile open invoices for vendor Acme and post variances > $50 to ledger.",
"tools": ["sql_query", "post_ledger", "call_vendor_api"],
"success": {
"ledger_entries": [{"vendor": "Acme", "min_amount": 50}],
"no_duplicate_posts": true
}
}
The success block is not a prompt. It is data your scorer consumes later. If you cannot express the win condition as code, the task is not benchmarkable yet.
Step 2: Sandbox the agent runtime
Agents mutate state. Run each task in a throwaway environment so a buggy tool call cannot poison the next run. A lightweight approach is a per-task subprocess with a temp directory and a mocked network layer.
import subprocess, tempfile, os
def run_agent_isolated(task: dict) -> dict:
with tempfile.TemporaryDirectory() as td:
# load db snapshot, start mock servers, etc.
env = os.environ.copy()
env["TASK_DIR"] = td
proc = subprocess.run(
["python", "agent_entrypoint.py", task["task_id"]],
env=env, capture_output=True, text=True, timeout=300
)
return {"returncode": proc.returncode, "stderr": proc.stderr, "td": td}
Docker is better when tools install system deps. The rule: identical bytes in, observable side effects out.
What to capture
At minimum, capture stdout, stderr, exit code, and any files the agent wrote to TASK_DIR. You will correlate these with model traces later.
Step 3: Route models through a single compatible endpoint
When you benchmark AI agents real-world tasks across multiple providers, you avoid rewriting clients by using an OpenAI-compatible gateway. n4n.ai fronts 240+ models on one endpoint and auto-falls-back when a provider is rate-limited, so a degraded route does not abort your batch. Your agent code stays identical whether you test gpt-4o, claude-3-5-sonnet, or a local model.
from openai import OpenAI
client = OpenAI(
base_url="https://api.n4n.ai/v1", # OpenAI-compatible, 240+ models
api_key=os.environ["N4N_KEY"],
)
def chat(model: str, messages: list):
return client.chat.completions.create(
model=model,
messages=messages,
temperature=0.2,
extra_headers={"x-routing": "cost-optimized"} # gateway honors directives
)
Per-token metering on the gateway gives you exact cost per task without instrumenting every call yourself.
Step 4: Instrument every run
You cannot optimize what you do not measure. Wrap the model client and tool executor to emit structured events to a local sink.
import time, json, logging
def traced_chat(model, messages):
t0 = time.monotonic()
resp = chat(model, messages)
dt = time.monotonic() - t0
logging.info(json.dumps({
"event": "llm_call",
"model": model,
"prompt_tokens": resp.usage.prompt_tokens,
"completion_tokens": resp.usage.completion_tokens,
"latency_s": round(dt, 3)
}))
return resp
Do the same for each tool: record input args, output shape, and exceptions. This trace becomes the backbone of your analysis.
Avoid LLM-judge noise in instrumentation
Latency and token counts are ground truth. Reserve LLM-based scoring for ambiguous tasks only, and always log the judge prompt and response.
Step 5: Score outputs with deterministic invariants
Write a pure function per task type. It reads the side effects (ledger posts, DB rows, files) and returns a pass/fail plus a reason.
def score_invoice_reconcile(task: dict, side_effects: dict) -> dict:
entries = side_effects.get("ledger_entries", [])
if not any(e["vendor"] == "Acme" and e["amount"] >= 50 for e in entries):
return {"pass": False, "reason": "missing required Acme variance post"}
if len(entries) != len({json.dumps(e, sort_keys=True) for e in entries}):
return {"pass": False, "reason": "duplicate ledger posts"}
return {"pass": True, "reason": "ok"}
If a task needs fuzzy matching (e.g., “summarize the ticket”), use a fixed hash of normalized output or a small heuristic, not a random judge model.
Step 6: Batch execution and aggregation
Run tasks in parallel with a worker pool, but cap concurrency to respect provider limits. Store results in SQLite for ad-hoc queries.
import concurrent.futures, sqlite3
conn = sqlite3.connect("bench.db")
conn.execute("CREATE TABLE IF NOT EXISTS runs (task_id, model, pass, tokens, latency)")
def evaluate_one(args):
task, model = args
side = run_agent_isolated(task)
score = score_invoice_reconcile(task, side)
return (task["task_id"], model, score["pass"], 0, 0) # fill tokens from trace
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as ex:
for row in ex.map(evaluate_one, [(t, m) for t in TASKS for m in MODELS]):
conn.execute("INSERT INTO runs VALUES (?,?,?,?,?)", row)
conn.commit()
After the batch, query pass rates by model and task cluster:
SELECT model, COUNT(*) total, SUM(pass) passes,
ROUND(100.0*SUM(pass)/COUNT(*),1) pass_pct
FROM runs GROUP BY model;
This is the core report you benchmark AI agents real-world tasks against.
Step 7: Verify success and gate regressions
Success means the harness itself is trustworthy, not that any model passed. Verify by checking:
- Every task produced a row in
runs. - No task crashed the sandbox (exit code captured).
- Scorer coverage is 100%: zero tasks scored as “unscored”.
Add a CI gate that fails if pass rate drops below the previous baseline by more than 2 percentage points for any model.
python bench.py --models gpt-4o,claude-3-5-sonnet --tasks tasks/*.json
sqlite3 bench.db "SELECT pass_pct FROM summary WHERE model='gpt-4o'"
If the number regresses, the build breaks. That is how you keep a real-world benchmark alive instead of letting it rot in a notebook.
Iterating the task set
Quarterly, replace the oldest 20% of tasks with fresh production traces. Concepts drift; your benchmark must drift faster than your agent does.