Evaluating AI Agents in CI: Scoring Full Trajectories, Not Just the Final Answer
In short
Final-answer assertions miss the agent regressions that actually cost money: wrong tool, extra steps, silent cost creep, and policy violations. Put evals in CI with deterministic tool mocks and score the whole trajectory across six dimensions, reserving the LLM judge for rubric-bound subjective checks. See how I build and harden AI agents.

On this page
- Why isn't a final-answer assertion enough for an agent?
- How do I make agent tests deterministic in CI?
- What should I score across the trajectory?
- When should I use an LLM judge, and how do I keep it honest?
- What does the eval harness look like in CI?
- Which metrics catch the regressions teams actually ship?
- What is the smallest version I can ship this week?
Most agent test suites only assert the final answer, and that is exactly why they miss the regressions that hurt you in production. If your agent reaches the right answer by calling the wrong tool, paying triple the tokens, looping four extra steps, or quietly violating a policy, a final-answer assertion stays green while your bill and your incident count climb. The fix is trajectory scoring: in CI you replay the agent against deterministic tool mocks and score the whole run, including tool choice, argument validity, step count, cost, latency, and policy compliance, and you reserve the LLM judge for the few subjective dimensions where you can give it a rubric and audit its verdicts.
I have been shipping agents into production long enough to have been burned by the green-build-bad-agent problem, and with the AI Engineer World's Fair landing 29 June to 2 July 2026 in San Francisco, "evals in CI" is the loudest message in the agents-in-production conversation right now. Here is the harness I actually run.
Why isn't a final-answer assertion enough for an agent?
A final-answer assertion checks the destination and ignores the route, but for an agent the route is most of the risk. The same correct output can come from a clean two-step trajectory or from a six-step mess that called a write tool it should never have touched.
In my experience the failures that cost real money never show up in the final string:
- The agent calls
search_orderswhen it should callget_order_by_id, doubling latency on every request. - It passes a malformed date to a tool, the tool errors, the agent recovers, and the answer is still right but the run took three extra LLM calls.
- A prompt-injected document convinces it to call
send_email, and your output assertion never inspected the tool log. - A model upgrade keeps accuracy flat but quietly raises average step count from 3 to 5, and you only notice when the monthly bill arrives.
An agent run is a trajectory: a sequence of (thought, tool call, arguments, observation) tuples ending in an answer. If your tests only look at the last element, you are blind to the middle, and the middle is where agents go wrong. This is closely related to the prompt-injection failure mode I cover in defending against the lethal trifecta ↗: you cannot defend against a tool call you never assert on.
How do I make agent tests deterministic in CI?
Mock every tool with recorded, deterministic responses so the only variable left is the agent's reasoning. Real network calls in CI make tests flaky, slow, and expensive, and they make failures impossible to attribute to the agent versus the API.
The pattern I use is a fixture registry keyed by tool name and a hash of the arguments. The agent thinks it is calling live tools; the harness intercepts each call and returns a canned observation.
## tool_mocks.py
import json, hashlib
class MockToolRegistry:
def __init__(self, fixtures: dict):
# fixtures: {tool_name: {arg_hash: observation}}
self.fixtures = fixtures
self.calls = [] # the trajectory log
def _hash(self, args: dict) -> str:
return hashlib.sha256(
json.dumps(args, sort_keys=True).encode()
).hexdigest()[:12]
def call(self, name: str, args: dict):
self.calls.append({"tool": name, "args": args})
table = self.fixtures.get(name, {})
key = self._hash(args)
if key not in table:
# unexpected argument shape is itself a signal
return {"error": f"no fixture for {name}({args})"}
return table[name_key := key] and table[key]
Two rules I never break. First, an unmatched argument hash is not a test setup bug to paper over; it is a finding, because it means the agent called a tool with arguments you never anticipated. Second, the LLM itself stays real unless you are testing pure orchestration, because mocking the model removes the thing you are actually evaluating. Pin the model id and set temperature to 0 so the reasoning is as reproducible as a sampled model allows.
What should I score across the trajectory?
Score six dimensions, not one. Each catches a class of regression that the final answer hides, and together they give you a single pass or fail with an itemized reason.
| Metric | What it catches | How to score | Gate type |
| Final answer correctness | Wrong output | Exact match, regex, or LLM judge | Hard fail |
| Tool choice | Right goal, wrong tool | Set match vs expected tool sequence | Hard fail |
| Argument validity | Malformed tool inputs | JSON-schema validate every call | Hard fail |
| Step count | Looping, inefficiency | Count <= budget per task | Soft threshold |
| Cost and latency | Silent bill and speed creep | Sum tokens x price, wall-clock | Soft threshold |
| Policy compliance | Forbidden tools, data leaks | Deny-list assertions on the call log | Hard fail |
| Regression you shipped | Final-answer test | Trajectory test | |
| Right answer, wrong tool | Pass | Fail (tool choice) | |
| Right answer, +2 steps | Pass | Fail (step budget) | |
| Right answer, 2x tokens | Pass | Fail (cost) | |
| Right answer, called send_email | Pass | Fail (policy) | |
| Malformed tool args, recovered | Pass | Fail (arg validity) |
Two operational notes from running this. Watch trends, not just thresholds: a metric creeping toward its ceiling over five PRs is a regression in progress, so chart per-PR cost and step count even when the gate is green. And feed production back in: when an agent misbehaves in the wild, capture that trajectory, anonymize it, and add it to cases/ so the same failure can never merge again. This is the same discipline that keeps long-running agents stable, which I get into in the context rot compaction playbook ↗.
What is the smallest version I can ship this week?
Start with three cases and three metrics: final-answer correctness, argument validity, and a forbidden-tool policy check, all over deterministic mocks. That alone catches the majority of the regressions that final-answer assertions miss, and you can add step-count and cost budgets once you have a baseline to set ceilings against.
If you want help wiring trajectory evals into your pipeline or auditing an agent that passes its tests but misbehaves in production, building and hardening AI agents and automation ↗ is most of what I do, and you can reach me on my contact page ↗. Build the harness before the World's Fair crowd convinces your stakeholders that everyone else already has one, because the teams shipping agents reliably in 2026 are the ones scoring the whole trajectory, not just the last token.
FAQ
Why isn't a final-answer assertion enough to test an AI agent?
Because the same correct answer can come from a clean trajectory or a six-step mess that called forbidden tools and burned triple the tokens, and a final-answer check only inspects the destination, never the route.
How do I make agent evals deterministic in CI?
Mock every tool with recorded, argument-hashed responses so the agent's reasoning is the only variable, pin the model id, and set temperature to 0 for the most reproducible runs possible.
What should I score across an agent trajectory?
Score final-answer correctness, tool choice, argument validity, step count, cost and latency, and policy compliance, gating the first three plus policy as hard fails and treating step count and cost as soft budget thresholds.
When should I use an LLM judge in agent evals?
Use an LLM judge only for subjective dimensions like tone or faithfulness that code cannot check, always bound it with a discrete rubric, log a reason field, and calibrate it against a human-labeled golden set before it gates merges.
Which trajectory metrics catch regressions that final-answer tests miss?
Tool choice, step count, and cost catch the silent drift a model upgrade introduces, like keeping answers correct while quietly adding a step per task or doubling token spend.
Working on something like this?
I build web apps, AI features, and mobile products for clients. If this article matches a problem you have, tell me about it.
Start a conversationMalik Hamza Shabbir · Full-Stack & AI Engineer
I build full-stack and AI products solo: a reputation SaaS in production, RAG pipelines, and React Native apps. I write from what I ship, not from documentation summaries.
Related articles
Who's Liable When the AI Agent You Built for a Client Goes Wrong? A Contract Checklist
When an autonomous agent you built takes a wrong action in production, the law often attributes that action to whoever deployed it, and the contract decides who actually pays. Here are the six clauses I put in every AI agent engagement.
Migrating to TypeScript 7 (tsgo) Without Breaking Your Build: The Plugin, Transformer & Emit Gotchas
tsgo is fast, but it has no JS plugin or transformer support and no stable compiler API until 7.1. Here is the dual-compiler tsconfig and CI setup I use to type-check with tsgo while keeping tsc for emit, plus a checklist of the tooling that breaks quietly.
Defending Against the Lethal Trifecta: An Agent Architecture That Survives Prompt Injection
Prompt injection is OWASP's number one LLM risk and no input filter fixes it. The real defense is architectural: detect when an agent holds all three lethal trifecta conditions at once, then break one of them. Here is how I do it with capability scoping, egress allow-lists, and human approval on privileged calls.