n4nAI

Why pass@1 is the wrong metric for agent evaluation

The pass@1 metric agent evaluation standard hides retry behavior and variance. This analysis argues for trajectory-aware scoring in production LLM agents.

n4n Team4 min read909 words

Audio narration

Coming soon — every post will get a voice note here.

The pass@1 metric agent evaluation convention measures whether a model produces a correct answer on the first try. That single-shot framing ignores how agents actually operate: they retry, branch, and recover across many model calls. For systems where reliability matters more than raw latency, pass@1 obscures the properties you need to optimize.

What pass@1 assumes about your system

Pass@1 comes from code-generation benchmarks where a model emits one completion and a test suite checks it. The implicit contract: one inference call, one answer, deterministic grading. Agents violate every part of that contract.

An agent is a control loop. It calls a model, observes a tool result, decides a next action, and repeats until termination. The final answer depends on the sequence of decisions, not a single token distribution. Scoring only the first model output tells you nothing about whether the system converges.

The pass@1 metric agent evaluation lens is borrowed from a world where the program is the model output. In an agent, the model output is one node in a graph.

The anatomy of an agent run

Consider a customer-support agent that must refund a transaction. It needs to:

  1. Parse the request.
  2. Call lookup_order with an ID.
  3. Verify eligibility via policy_check.
  4. Execute issue_refund.
  5. Summarize.

A single mistake at step 2 (bad ID extraction) can be corrected at step 3 if the agent notices the empty result and retries. Pass@1 would mark the run failed if the first model call produced a malformed ID, even if the agent self-healed.

def run_agent(query):
    state = {"query": query}
    for step in range(MAX_STEPS):
        action = model.decide(state)
        if action.tool == "lookup_order":
            res = lookup_order(action.args["id"])
            if not res:
                state["error"] = "order not found"
                continue  # agent retries with corrected id
            state["order"] = res
        # ... other tools
    return state.get("refund_confirmation")

The loop is the product. Measuring only the first model.decide call misses the recovery path.

Why single-shot scoring breaks for multi-step agents

Retry and fallback logic

Production agents embed retry. If a provider returns 429, the agent (or its gateway) retries on a different model. n4n.ai offers automatic fallback when a provider is rate-limited or degraded, which means a given logical agent run may span three providers transparently. Pass@1 evaluated against a single provider’s first response is disconnected from observed behavior.

{
  "run_id": "a1",
  "attempts": [
    {"provider": "openai", "status": 429},
    {"provider": "anthropic", "status": 200, "answer": "refund issued"}
  ],
  "final": "success"
}

If you score the first attempt, you record failure. The user got success.

Tool invocation and partial progress

Agents often make progress before failing. A data-analysis agent might correctly load a CSV, compute a pivot, then crash on a chart render. Pass@1 says zero. But the loaded dataframe and pivot are reusable state; a human would call that 80% done.

Partial credit matters when agents are composed. A planner agent that picks the right tools but passes wrong params to one of them is more valuable than one that picks random tools.

Non-determinism and temperature

At temperature > 0, the same prompt yields different first tokens. Pass@1 becomes a Monte Carlo estimate requiring many samples to stabilize. For an agent, you care about the distribution of end states, not the first token’s correctness.

Better metrics for agent evaluation

success@k and budgeted success

Borrow from retrieval: measure success within k attempts or within a token budget. Define success@k as the probability the agent reaches the goal within k model calls. More useful: success@cost — did it finish under a max token spend?

def success_at_k(trajectories, k, goal_reached):
    wins = sum(1 for t in trajectories if goal_reached(t) and len(t.calls) <= k)
    return wins / len(trajectories)

Trajectory cost and token accounting

Agents burn tokens per step. A run that succeeds at pass@1 but costs 50k tokens is worse than one that recovers from a stumble in 10k. Per-token usage metering lets you compute cost-weighted success.

cost = sum(call.prompt_tokens + call.completion_tokens for call in trajectory)
efficient = goal_reached(trajectory) and cost < BUDGET

Partial credit and state reachability

Define a state graph. Award credit for reaching intermediate states. If the goal is unreachable but the agent reached the penultimate state, assign 0.9. This aligns with how you’d debug: you fix the last hop, not the whole plan.

def score_trajectory(traj):
    if traj.final.get("refund_confirmation"):
        return 1.0
    if traj.final.get("order_loaded") and traj.final.get("policy_ok"):
        return 0.7
    if traj.final.get("order_loaded"):
        return 0.4
    return 0.0

A worked example: booking agent

Suppose an agent books a flight. The ideal trajectory:

  • Parse dates
  • Search inventory
  • Select flight
  • Pay
  • Confirm

In testing, model call 1 misformats the date. The agent detects the search error, reformats, and continues. Pass@1 flags this as a fail. Trajectory scoring records: date parsed (0.2), search succeeded after retry (0.5), payment (0.8), confirm (1.0). The run succeeded; the metric should reflect that.

If you only track pass@1, you will either over-prompt (force giant system messages to get first-call perfection) or ship an agent that looks weak in eval but works fine. Neither helps.

Tradeoffs of abandoning pass@1

Trajectory metrics are heavier. You must log every step, replay tool calls, and define goal predicates. That’s real instrumentation work. Pass@1 is cheap: one call, one grade.

Also, pass@1 is comparable across papers. If you publish success@cost, readers need your budget definition. Standardization is weaker.

But the alternative is shipping blind. An agent that scores 0.9 pass@1 but fails 30% of the time after step 3 will surprise users. The eval must match the deployment surface. The pass@1 metric agent evaluation habit persists because it is easy, not because it is right.

Implementing trajectory eval

Capture the full run as a list of structured events. Grade offline.

class Trajectory:
    calls: list[ModelCall]
    tool_results: list[ToolResult]
    final_state: dict

def evaluate(run: Trajectory):
    if run.final_state.get("refund_confirmation"):
        return 1.0
    if run.final_state.get("order_loaded"):
        return 0.5
    return 0.0

Run hundreds of seeded scenarios. Aggregate with variance. Report success@k and median cost. Store the trajectories so you can recompute scores when your goal definition tightens.

A minimal harness:

python eval_agent.py \
  --scenarios ./golden.jsonl \
  --max-steps 12 \
  --budget-tokens 30000 \
  --out report.json

The report should include not just means but p90 cost and failure-mode histogram.

Decisive takeaway

Stop using the pass@1 metric agent evaluation as a primary signal for agent systems. It measures a property your agent does not have: single-shot determinism. Score trajectories—success within budget, partial state credit, and retry resilience. The instrumentation cost pays back the first time a “high pass@1” agent fails in production because its recovery path was never measured.

Tagsai-agentsevaluationmetricsbenchmarking

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All ai agent evaluation & benchmarking posts →