Skip to content

4. Evaluate your agent

Time: 40 minutesEdit: code/04-evaluate/starter/run_evals.py

Your agent works when you try it. But does it work every time, for every kind of question? After you change the prompt or swap the model, is it better or just different? Trying a few questions by hand won’t tell you. Evals will.

An eval is a test for an AI system: an input, and a way to decide whether the output is good enough.

Normal tests expect an exact output. AI output changes from run to run, and many different answers can be correct. So we combine different kinds of checks:

Check Example Cost Good for
Tool checks Did it call search_faq? Free, instant Behaviour you can see in the trace
Text checks Does the reply contain 12:30? Free, instant Facts that must appear (or must not)
LLM judge Does the reply meet this rubric? One model call Meaning, tone, completeness

Use cheap, exact checks wherever you can. Use the judge for what only a reader could decide.

Open code/04-evaluate/dataset.jsonl. Each line is one case:

{"id": "faq-lunch",
"input": "When is lunch, and is there vegan food?",
"expect_tools": ["search_faq"],
"must_contain": ["12:30"],
"rubric": "Says lunch is 12:30 to 13:30 and that vegan options are available."}

A case only uses the checks it lists. Some cases have only a rubric, and some have no rubric at all.

Open code/04-evaluate/starter/run_evals.py. The runner is already there: it loads your agent, runs each case in a fresh conversation, applies the checks and prints a report. You write three checks.

TODO 1: check_tools. Every expected tool must have been called. An entry like "a|b" means either one is fine.

def check_tools(case, result):
called = {c["name"] for c in result["tool_calls"]}
missing = [t for t in case["expect_tools"] if not set(t.split("|")) & called]
return (not missing, f"missing {missing}" if missing else f"called {sorted(called)}")

TODO 2: check_contains. Every required phrase must appear in the reply, ignoring case.

def check_contains(case, result):
reply = result["reply"].lower()
missing = [p for p in case["must_contain"] if p.lower() not in reply]
return (not missing, f"missing {missing}" if missing else "ok")

TODO 3: judge. Ask a model to grade the reply against the rubric. The prompt template, JUDGE_PROMPT, is at the top of the file.

def judge(case, result):
prompt = JUDGE_PROMPT.format(question=case["input"], answer=result["reply"], rubric=case["rubric"])
verdict = llm.chat([{"role": "user", "content": prompt}], model=config.JUDGE_MODEL, temperature=0)
text = verdict["content"].strip()
first_line = text.splitlines()[0].upper() if text else ""
return ("PASS" in first_line and "FAIL" not in first_line, text.replace("\n", " ")[:160])

Run the evals against your Module 3 agent:

Terminal window
python 04-evaluate/starter/run_evals.py
[PASS] beginner-day1 (3.1s)
[PASS] session-detail (2.4s)
[PASS] full-session (2.9s)
[FAIL] faq-wifi (2.2s)
expect_tools: missing ['search_faq']
must_contain: missing ['badge']
reply: "I can't provide the wifi password. You can find it in the event FAQ or ask the conference staff directly."
...
7/8 passed (88%)
Saved details to results/eval-20261104-142210.json

That failure is real: it’s what happened when we tested with qwen3:4b-instruct. The model decided a password was too sensitive to look up, even though the FAQ has the answer. Your results will differ. That’s the point. Open the JSON file in results/ to see every reply and tool call.

  1. Run it more than once. AI is not deterministic.
    Terminal window
    python 04-evaluate/starter/run_evals.py --runs 3
    A case that passes 2 out of 3 times is a flaky feature, not a passing one.
  2. Pick one failing case and fix it. Change the system prompt in 03-agent-loop/starter/agent.py, or improve a tool description in agentlab/tools.py. Run the evals again. Did your fix break anything else?
  3. Add two cases of your own. One for something the agent should do, and one for something it should refuse or say it doesn’t know.
  4. Compare models. If you have another model pulled, change LLM_MODEL in .env and run the evals again. Now you can pick a model with data, not vibes.

An LLM judge is also an AI system, and it makes mistakes too. A small judge model may be too strict or too lenient. Some habits help:

  • Write rubrics that are specific and checkable (“says lunch is 12:30 to 13:30”), not vague (“is helpful”).
  • Read some of the judge’s verdicts yourself. If you disagree often, fix the rubric.
  • Use a stronger model as the judge if you can. Set JUDGE_MODEL in .env. It can even be a different provider from the agent.
  • Run python 04-evaluate/starter/run_evals.py --only faq-wifi --runs 5.
  • Add --min-pass-rate 0.8 to make the command fail when quality drops. That’s how you’d use evals in CI.

Checkpoint. You can measure your agent instead of guessing. You have deterministic checks, an LLM judge, repeated runs and a pass rate you can track over time. In Part 3 you’ll use the same runner to prove your security fixes work.

Stuck? Use python 04-evaluate/solution/run_evals.py