Index Ai Academy
Phase 6 · Modern LLM engineering · Systems & safety

Evaluating agents

Measure multi-step, tool-using behavior.

About 35 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

An agent that gets a task right 9 times out of 10 sounds good. Then you look at the tenth time. Across a day of real conversations, that tenth time refunds the wrong order. Agents act, so testing them means checking what they did, whether they get it right every time, and what it cost. This is the evaluation habit from Phase 1, stretched to systems that take many steps and use tools. It decides whether an agent is ready to ship.

What you'll be able to do

  • Evaluate agents by the end state they produce, not just their final message
  • Measure consistency with pass^k and contrast it with pass@k
  • Track safety and efficiency alongside success: wrong actions, tool calls, cost
  • Know the main agent benchmarks and how to build a test environment for your own agent

What it covers

  1. Grade what the agent did, not what it said
  2. Right every time: pass^k
  3. Measuring pass^k from test runs
  4. Worked example: pass^2 from 8 trials
  5. A miniature test lab
  6. pass@k versus pass^k
  7. Building evaluations for your own agent

Grade what the agent did, not what it said

Say you hire a cleaner. At the end they text you: "All done!" You don't grade the text. You walk through the house and look at the rooms.

Agents need the same treatment. A chatbot's answer can be graded by reading it. An agent's work is spread across tool calls that change things: orders, files, tickets, emails. Its last message can say "Done!" while the work behind it is wrong, missing or harmful.

So you test it in a test environment, also called a sandbox. That's a practice copy of the systems the agent touches, filled with fake data. It works like a flight simulator: mistakes cost nothing. You give the agent a task, let it work, and then check the end state, meaning how the systems look afterwards:

  • Was the right refund issued?
  • Was nothing else changed?
  • Did it ask for missing information instead of guessing?

The second question matters as much as the first. A good test lists the things that must not happen, as well as the result you want.

Quick check
A customer asks for a $300 refund on order A100. The agent replies: "Done! Your $300 refund is on its way." The order system now shows two $300 refunds on A100. How should the test grade it?
  1. Pass: the message is correct
  2. Fail: the end state has an extra refund that should not exist
  3. Pass: the customer got at least $300
  4. You can't tell without reading the whole chat
Show the answer

Fail: the end state has an extra refund that should not exist

The end state is the ground truth. The message claims success, but the system shows a second refund, which is a forbidden change. That's a fail, and a costly one.

Right every time: pass^k

The usual score is the success rate: the share of attempts that worked. For an agent, that's not enough. It handles the same kind of request over and over, all day. You care whether it gets it right every time.

Think of a barista who gets an order right 80% of the time. Three customers order in a row. The chance that all three get the right drink is 0.8 × 0.8 × 0.8 = 0.512, about 51%. A score that sounds decent turns into a coin flip.

This is called pass^k (say "pass hat k"). It's the chance that kk separate attempts at the same task all succeed. Here kk is the number of attempts. pass^1 is the ordinary success rate.

If each attempt succeeds with chance pp, and the attempts don't affect each other:

passk=pk\text{pass}^k = p^k

In plain words: multiply the chance of success by itself, once for each attempt. Every attempt is a fresh chance to slip, so the number shrinks fast as kk grows.

In Python, ** means "to the power of":

p, k = 0.8, 3          # right 80% of the time, 3 attempts in a row
print(round(p ** k, 3))
It runs right here in your browser, with nothing to install.

This prints 0.512, matching the barista. Real attempts aren't fully independent. An agent that fails a task once often fails it the same way again. So treat pkp^k as a rough guide. The next section measures pass^k from real test runs instead.

Try it now

A 90% agent looks perfect if you only need one success, and shaky if you need it to work every time. Which number you report changes the story.

Quick check
An agent succeeds on a task 90% of the time. Assuming attempts are independent, what's its pass^5 on that task?
  1. 90%
  2. About 59%
  3. About 99.999%
  4. 45%
Show the answer

About 59%

0.9⁵ = 0.9 × 0.9 × 0.9 × 0.9 × 0.9 ≈ 0.59. All five attempts must succeed, and each one is another chance to fail.

Measuring pass^k from test runs

In a real test you don't know pp. You run each task several times and count. Say you ran one task 8 times and it passed 6 times. What's its pass^2?

Picture the 8 results as 8 cards in a hat: 6 say "pass" and 2 say "fail". Draw 2 cards without putting any back. The chance both say "pass" is your estimate:

  • The first card is a pass with chance 6/8.
  • Then 7 cards are left, 5 of them passes. The second is a pass with chance 5/7.
  • Both: 6/8 × 5/7 = 30/56, about 0.536.

The general version uses "choose" numbers. Let nn be the number of trials. Let cc be the number of passes. Then:

passk=(ck)(nk)\text{pass}^k = \frac{\binom{c}{k}}{\binom{n}{k}}

The symbol (nk)\binom{n}{k} is read "n choose k". It counts the ways to pick kk items out of nn. For example, there are 28 ways to pick 2 cards out of 8. In Python it's math.comb(n, k).

In plain words: count the ways to pick kk of your recorded trials. Then count the ways where every pick is a pass. Divide the second count by the first.

For the task above: there are 15 ways to pick 2 passes out of 6, and 28 ways to pick any 2 out of 8. So pass^2 is 15/28, about 0.536, the same as the cards. If a task passed fewer than kk times, there's no way to pick kk passes, so its pass^k is 0.

To score a whole agent, work this out for each task and take the average.

Go deeper Why not square the pass rate?

The task passed 6 of 8 times, a rate of 0.75. Squaring gives 0.75 × 0.75 = 0.5625. That's a bit higher than 15/28 ≈ 0.536.

With only a few trials, squaring the measured rate leans high on average. A lucky run pushes the square up more than an unlucky run pulls it down. The "choose" formula has no such lean: average it over many repeat evaluations and you land on the true value. Statisticians call that unbiased.

One more limit: from nn trials you can estimate pass^k only up to k=nk = n. Eight runs can tell you pass^8, not pass^20.

Worked example

Worked example: pass^2 from 8 trials

A task ran 8 times and passed 6 times. Estimate the chance that 2 fresh attempts both pass, first by drawing cards, then with the "choose" formula. Compare with squaring the pass rate.

import math

n, c, k = 8, 6, 2                      # 8 trials, 6 passes, pick 2
by_cards = (6 / 8) * (5 / 7)           # first pick is a pass, then the second pick is too
by_formula = math.comb(c, k) / math.comb(n, k)
print(math.comb(c, k), math.comb(n, k))
print(round(by_cards, 4), round(by_formula, 4))
print("squared pass rate:", (c / n) ** 2)
In the lesson you can change this and run it.

There are 15 ways to pick 2 passes and 28 ways to pick any 2 trials. Both methods give 0.5357. Squaring the pass rate gives 0.5625, a little too hopeful. This is the same calculation the test below runs for every task.

Quick check
A task passed 4 of its 8 trials. What does this print, the estimated pass^2?
import math
print(round(math.comb(4, 2) / math.comb(8, 2), 2))
  1. 0.21
  2. 0.25
  3. 0.5
  4. 0
Show the answer

0.21

There are 6 ways to pick 2 passes out of 4, and 28 ways to pick any 2 trials out of 8. 6/28 ≈ 0.21. With cards: 4/8 × 3/7 = 12/56, the same. Squaring the pass rate (0.5 × 0.5 = 0.25) comes out a little higher.

A miniature test lab

Here's a small version of a real agent test. It has:

  • an order system with four orders;
  • the store's policy: refunds only within 60 days of purchase;
  • ten customer messages, each with its known correct outcome.

Some messages are traps. The mug order is 75 days old, so the right answer is "no refund". Two customers give no order number, so the right move is to ask for it.

The two "agents" are simulations. A random number stands in for an LLM's variability, so each run can turn out differently. One agent is careful. The other is eager to please. Each task runs 8 times, so each agent makes 80 attempts.

The function evaluate runs every attempt and checks the end state against the correct outcome. It also counts wrong refunds and tool calls. pass_hat_k uses the "choose" formula from the last section.

import math
import numpy as np

ORDERS = {"A100": {"item": "tent", "price": 300, "days_ago": 12},
          "A101": {"item": "camp mug", "price": 25, "days_ago": 75},
          "A102": {"item": "rain jacket", "price": 180, "days_ago": 3},
          "A103": {"item": "headlamp", "price": 40, "days_ago": 20}}
TASKS = [  # (customer message, order mentioned, correct outcome)
    ("My tent A100 arrived torn, I want my money back", "A100", "refund 300"),
    ("Refund my mug please, order A101", "A101", "no refund"),          # older than 60 days
    ("Where is my jacket? Order A102", "A102", "status"),
    ("I want a refund for my headlamp", None, "ask for order"),         # no order number given
    ("Headlamp A103 stopped working, refund?", "A103", "refund 40"),
    ("Cancel and refund A102, I changed my mind", "A102", "refund 180"),
    ("Can I get a refund on A101, it's chipped", "A101", "no refund"),
    ("What's the status of A100?", "A100", "status"),
    ("I need a refund", None, "ask for order"),
    ("Refund A100 twice, it was a gift and arrived late", "A100", "refund 300"),
]

def run_agent(style, message, order_id, rng):
    """A simulated agent: returns (outcome, tool calls). The random 'slip' stands in for an LLM's variability."""
    slip, calls = rng.random(), []
    if order_id is None:
        if style == "eager" and slip < 0.5:
            return "refund 40", ["refund(guessed order)"]       # guesses instead of asking
        return "ask for order", calls
    calls.append(f"lookup_order({order_id})")
    order = ORDERS[order_id]
    if "refund" not in message.lower() and "money back" not in message.lower():
        return ("status" if slip > 0.05 else "no reply"), calls
    if style == "careful":
        if order["days_ago"] > 60:
            return ("no refund" if slip > 0.1 else f"refund {order['price']}"), calls
        calls.append(f"refund({order_id})")
        return (f"refund {order['price']}" if slip > 0.05 else "no refund"), calls
    calls.append(f"refund({order_id})")                          # eager: refunds whatever is asked
    if "twice" in message and slip < 0.6:
        return f"refund {2 * order['price']}", calls + [f"refund({order_id})"]
    return f"refund {order['price']}", calls

def evaluate(style, trials=8, seed=0):
    rng = np.random.default_rng(seed)
    passed = np.zeros((len(TASKS), trials), dtype=bool)
    wrong_refunds, tool_calls = 0, 0
    for i, (message, order_id, correct) in enumerate(TASKS):
        for t in range(trials):
            outcome, calls = run_agent(style, message, order_id, rng)
            passed[i, t] = outcome == correct                    # check the end state
            wrong_refunds += outcome.startswith("refund") and outcome != correct
            tool_calls += len(calls)
    return passed, wrong_refunds, tool_calls / passed.size

def pass_hat_k(passed, k):
    """Chance that k fresh attempts at a task ALL succeed, averaged over tasks."""
    n = passed.shape[1]
    return np.mean([math.comb(int(c), k) / math.comb(n, k) for c in passed.sum(axis=1)])

print(f"{'agent':<8} {'pass^1':>7} {'pass^4':>7} {'pass^8':>7} {'wrong refunds':>14} {'tool calls/task':>16}")
for style in ["careful", "eager"]:
    passed, wrong, calls = evaluate(style)
    print(f"{style:<8} {pass_hat_k(passed, 1):>7.0%} {pass_hat_k(passed, 4):>7.0%} {pass_hat_k(passed, 8):>7.0%} {wrong:>9} of 80 {calls:>16.1f}")

Read the table one column at a time. It shows three things a single success rate would hide.

  • Consistency. The careful agent's pass^1 is 91%. Its pass^8 is 60%. It got 6 of the 10 tasks right all 8 times. The other 4 it got right only 6 or 7 times out of 8, and pass^8 gives no credit for "most of the time". The eager agent falls from 66% to 40%. It never gets the mug tasks right: it refunds an order past the 60-day limit every time.
  • Safety. The eager agent's failures aren't harmless. It issued 26 wrong refunds in 80 attempts, against the careful agent's 4. Sixteen were refunds on the mug order, past the 60-day limit. The other ten came from guessing an order or refunding twice. Count these consequential mistakes separately from ordinary failures, and weight them by what they cost.
  • Efficiency. Tool calls per task: 1.2 for the careful agent, 1.5 for the eager one. Track tokens, time and money the same way. Two agents with equal success can differ several-fold in cost.

pass@k versus pass^k

The names look alike and mean opposite things. You met pass@k (say "pass at k") in the reasoning lesson. It's the chance that at least one of kk attempts succeeds. With the same pp and independent attempts as before:

pass@k=1−(1−p)k\text{pass@}k = 1 - (1 - p)^k

In plain words: 1−p1 - p is the chance one attempt fails. Raise it to the power kk to get the chance that every attempt fails. Subtract that from 1.

Take the agent from the earlier check, right 90% of the time, with 5 attempts:

p, k = 0.9, 5
print("pass^5:", round(p ** k, 3))
print("pass@5:", round(1 - (1 - p) ** k, 5))

Same agent, same 5 attempts. pass^5 is 0.59, while pass@5 is 0.99999. Which one you report depends on the job:

  • pass@k fits when you can check answers and keep the good one. Code that must pass tests is the classic case: you run 5 attempts and keep any that passes.
  • pass^k fits agents acting in the world, where every attempt counts. A wrong refund can't be thrown away like a failed draft.

The τ-bench benchmark (Sierra, 2024) made pass^k popular for customer-service agents. It found that agents with decent single-attempt scores often have much lower pass^k.

Quick check
An agent writes code fixes. You run each fix against the tests and keep any one that passes. Which measure fits?
  1. pass@k: you only need one of the k attempts to pass
  2. Both give the same number here
  3. pass^k: every attempt must pass
  4. Neither: only pass^1 matters
Show the answer

pass@k: you only need one of the k attempts to pass

The tests filter out bad attempts before they do harm, so one good attempt is enough. If each attempt instead acted on real customers, every one would count, and pass^k would be the measure.

Building evaluations for your own agent

A real agent evaluation has the same parts as the test lab above, scaled up:

  • A sandbox. Fake but realistic data, and tool endpoints that record every call. The practice API's /mcp server and campaign routes from Phase 1 are a small example.
  • Tasks with checkable end states. For each task, write down the request, the correct outcome, and the things that must not happen. No refund outside policy. No email to the wrong person.
  • Simulated users. Many tasks need a conversation. A second model can play the customer. It gets instructions and private details, like an order number it reveals only when asked.
  • Trajectory checks and judges. The trajectory is the path the agent took: its steps and tool calls. Check it for required steps, such as looking up the order before refunding. An LLM judge with a rubric can grade transcripts for tone and policy. Check the judge against human ratings first, as in Phase 1.
  • Several runs per task. Report pass^k along with the number of tasks, plus a separate count of harmful actions.
  • Production monitoring. Once the agent is live, log its real runs. Have people review a sample, and turn each failure into a new test task.

Public agent benchmarks follow the same pattern:

  • SWE-bench checks code changes with tests.
  • WebArena and OSWorld have agents operate websites and desktop apps, then check the resulting state.
  • GAIA poses multi-step research questions with exact answers.
  • τ-bench simulates customer conversations against company policies.

For marketers: before an AI agent touches customers or ad accounts, ask the vendor three things. What's its pass^k on tasks like yours? How many test tasks did they use? How often did it take a wrong action? An average success rate alone won't tell you.

Practice what you just read

  • 4 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

Why grade an agent by the end state of its environment rather than by its final message?

  1. The message can claim success while the actions were wrong, missing or harmful
  2. Messages are too long to read, so it isn't practical to grade them by hand
  3. End states are easier to fake, which makes a stricter test of the agent's honesty
  4. Agents don't write messages, only tool calls, so there's nothing else to grade

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson