Educational material · technical case study
Evaluating and Improving Agentic Applications
From agent execution
to verified behavior.
Follow the code from a customer request to a regression test. Five modules build one evaluation workflow—each adding a data structure, a check and a way to inspect what happened.
Kaan Kaya’s documented DeepLearning.AI coursework, taught by Laurie Voss and built with Arize AI. The scenario is Northwind Apparel, a fictional retailer whose agent looks up products, checks orders and handles refunds.
Course completed 14 November 2026The system being evaluated
The answer is only
one output.
An agent connects a language model to tools. A request may produce both a response and a change in a database. Evaluation checks the facts, the actions and the resulting state. In these labs, Arize Phoenix is the workspace for inspecting linked traces, test datasets and evaluator scores.
For example: an eligible refund.
Produces a reply and may change state.
What was said, which calls ran, and whether the refund exists.
Linked spans record inputs, outputs, tool arguments and order → test cases → layered scores → paired experiments.
Why separate those checks?
A grounded reply can leave a refund unfinished.
In one saved course practice case, v2 processes a $98 refund; v3 asks for confirmation. Both pass grounding and policy checks, while v3 fails selection and outcome. That is why the pipeline below tests more than the final sentence.
$98 processed
Outcome passConfirmation requested
Outcome failTwo kinds of evidence, clearly separated. The Python snippets and outputs come from a new, runnable deterministic companion with three synthetic DEMO-* cases and a $42 refund. That code is one executable pipeline. The GIFs illustrate the course’s conceptual progression across different saved labs and sessions, including the separate $98 example. They were not generated by the companion, which does not reproduce Phoenix or call a model.
Every stage below is visible. Recordings start as still images; Play loads that GIF, Stop restores its poster, and only one recording plays at a time.
Foundations / instrument the execution
Turn a request into linked spans.
A trace is the execution record. Give the overall request a root span, attach each tool call as a child, and retain inputs, outputs and final state. You can then ask which operation produced—or failed to produce—the answer.
- Inherits
- A synthetic request and a version of its behavior
- Adds
- Parent/child IDs, ordered tool events and retained state
- Produces
- Run → response + claims + spans[] + state
In the course: Phoenix OpenTelemetry instrumentation connects the agent entry point and wrapped tools as parent/child spans.
def record(name: str, inputs: dict, outputs: dict, milliseconds: int) -> None:
tools.append(Span(f"{root_id}/{len(tools) + 1}", root_id, name,
len(tools) + 1, inputs, outputs, milliseconds))
# ... replay the synthetic tool actions ...
root = Span(root_id, None, "support_request", 0,
{"case_id": case.case_id, "request": case.prompt}, {"response": response},
sum(span.synthetic_ms for span in tools))
return Run(case.case_id, version, response, claims, (root, *tools), state)Selected lines from replay(): record() links tool spans to the root request, while Run preserves both the response and the resulting state. The full function is linked below. Timing fields in this fixture are invented, not performance measurements.
Companion output
Deterministic replaycase_id: DEMO-REFUND
version: baseline
spans[0].span_id: baseline/DEMO-REFUND
spans[1].parent_id: baseline/DEMO-REFUND
linked_spans: 4
refund_records: 1
recorded_refund: $42The baseline replay records the expected refund. Its trace and persisted result travel together to the benchmark stage.

What to watch
- Follow the connection check into the first received span: this confirms that tracing is connected.
- Open the tool span and compare its input with its returned product data.
- Follow the parent agent span to see how that tool evidence supports the final response.
The saved tracing labs make the link between agent requests, model calls and tool results visible. A successful span tells you the operation ran; it does not establish that the answer or task was correct.
The GIF is a curated course screenshot sequence. It was not generated by this companion, and tracing ingestion is not a quality score.Benchmarking / turn observations into cases
Make a benchmark from requirements and traces.
A benchmark is a fixed set of cases with explicit expectations. Keep a stable case ID, the requirement it tests and a link back to the observed run. Curate coverage deliberately instead of treating every generated question as a useful test.
- Inherits
- Module 1 runs plus declared case requirements
- Adds
- Identity joins, provenance and coverage validation
- Produces
- case_id → BenchmarkEntry(case, observed_run)
In the course: The Phoenix client exports root agent-run rows; labels and curated questions retain trace provenance.
observations[run.case_id] = run
if set(expectations) != set(observations):
raise ValueError("Observed runs must cover exactly the expected case identities")
covered = {case.requirement for case in expectations.values()}
missing = REQUIRED_COVERAGE - covered
if missing:
raise ValueError(f"Missing requirement coverage: {sorted(missing)}")
return {key: BenchmarkEntry(case, observations[key])
for key, case in expectations.items()}Selected lines from build_benchmark(): join by case identity and check declared coverage. BenchmarkEntry retains the source Run and exposes its root span ID and response. The full function also rejects duplicate IDs and malformed root spans.
Companion output
Deterministic replaycase_id: DEMO-REFUND
requirement: complete_refund
source_span_id: baseline/DEMO-REFUND
joined_trace_cases: 3
unique_case_ids: 3
declared_requirements: 3/3The benchmark retains the observed response and root span ID for each request. These three declared requirements are a teaching fixture, not comprehensive retail coverage.

What to watch
- Read input, output and context.span_id together: the label must refer to the exact response being reviewed.
- Watch the expanded question bank preserve the same trace-linked schema as coverage grows.
- Inspect the final four failures: category, severity, source and span ID explain why each case was retained.
The saved practice capture records 43 traces and a curated four-case failure set. Its labeling notes explicitly preserve disagreements and correct response-to-trace provenance.
This recording shows Data Explorer tables derived from Phoenix traces, not the Phoenix UI. A curated failures-only set cannot estimate production error rates; the separate graded notebook retains a RateLimitError.Evaluator reliability / check the measurement
Score evidence, then validate the judge.
Use code when the rule is precise. Use a model-based rubric when judgment is needed, then compare the judge’s decisions with reviewed reference labels. Keep case identity and held-out labels fixed so a changed reference cannot masquerade as improvement.
- Inherits
- Trace-linked cases, evidence and reviewed labels
- Adds
- A fact check and reference-aligned judge validation
- Produces
- Grounding decision + judge agreement report
In the course: Phoenix classification evaluators apply model rubrics; scikit-learn reports agreement across a 33/11/12 development/validation/test split.
def grounding_score(run: Run) -> bool:
"""Exact structured-fact check; not a general natural-language judge."""
evidence = {}
for span in run.tools:
evidence.update(span.outputs)
return all(key in evidence and evidence[key] == value
for key, value in run.claims.items())The companion compares structured claims with tool outputs. Its separate judge_agreement() function aligns four fixed synthetic label fixtures and computes accuracy and Cohen’s kappa; it does not call an LLM or validate natural-language answers.
Companion output
Deterministic replaygrounded: True
fixed_label_matches: 3/4
judge_accuracy: 0.750
judge_kappa: 0.500The 3/4 agreement is a separate four-label fixture for teaching validation. It is not the outcome score for the three replayed requests, and it is not the course’s held-out result.

What to watch
- Read the jeans refusal: the response says the catalog does not offer jeans, rather than recommending an unavailable product.
- Compare the passing correctness/policy labels with the failing literal product check, which flags the word ‘jeans’ even in a refusal.
- Open the annotation explanations: this heuristic false positive is a reason to validate the evaluator, not to trust its label automatically.
The product-word heuristic flags a refusal as a failure. Separately, the held-out policy-judge report matches 9 of 12 reference labels for both versions: 0.750 accuracy and kappa 0.400. A perfect intermediate score follows reference-label changes.
This GIF shows annotation inspection, not that validation report. The report comes from the saved notebook; small samples and mutable reference labels limit its interpretation.Actions and outcomes / test the work behind the reply
Verify the trajectory and the resulting state.
A trajectory is the ordered sequence of tool actions. Check the selected tools, arguments, identity verification before a refund and the refund record itself. Here the candidate version replays the same request but asks for confirmation instead of recording the refund.
- Inherits
- Benchmark expectations + a candidate replay of the same case
- Adds
- Selection, arguments, ordering, policy and state checks
- Produces
- case_id → a six-dimension score vector
In the course: Ordered tool spans become trajectories; expected-tool/argument rules and refunds-table checks produce layered annotations.
identities = [span.sequence for span in tools
if span.name == "verify_identity" and span.outputs.get("verified") is True]
refunds = [span for span in tools if span.name == "issue_refund"]
ordering = all(any(verified < refund.sequence for verified in identities) for refund in refunds)
matching_refunds = [row for row in run.state["refunds"]
if row["order_id"] == case.order_id and row["refunded_amount"] == case.amount]
if case.requirement == "complete_refund":
outcome = bool(matching_refunds)Selected lines from evaluate_run(): ordering inspects earlier identity events; outcome inspects the retained refund state. Empty refund calls can pass the ordering rule, so a separate selection/outcome failure is essential.
Companion output
Deterministic replayDEMO-REFUND / candidate
grounding: True selection: False
invocation: True ordering: True
outcome: False policy: TrueThe candidate asks for confirmation without recording a refund. Its structured claims remain supported, but the required action and outcome fail. This is the new companion’s $42 example.

What to watch
- The selected run has selection/policy failures beside passing invocation, identity and outcome checks. Read the layers together.
- Follow the actual tool trajectory: no refund was called. Here, an outcome pass means no prohibited refund occurred—not that a refund was completed.
- The identity-order rule also passes when there is no refund call. Its pass is not proof that identity was verified; inspect the explanation and action trace.
In the saved 12-case graded test, selection fails 6 and invocation fails 8; outcome and policy each fail 1. The layers expose different failure modes on the same cases.
The failures overlap and cannot be added as distinct failed requests. The course’s expected-tool rules are specific to its tests; these counts are not production reliability estimates.Experiments / compare and preserve working behavior
Pair the same cases and expose regressions.
Run the same benchmark against each version. Match results by case identity rather than row position, inspect new failures, and keep known-good cases beside targeted failures. A promising score is only useful if the comparison is valid.
- Inherits
- Baseline and candidate score maps from Module 4
- Adds
- Identity-safe pairing and an explicit regression list
- Produces
- Paired scores + layer regressions + gate result
In the course: Phoenix asynchronous experiments compare versioned prompts; saved production-style failures are added as future regression cases.
paired = {case_id: {"baseline": baseline[case_id], "candidate": candidate[case_id]}
for case_id in baseline}
layer_regressions = {
case_id: [layer for layer in PROTECTED_LAYERS
if pair["baseline"][layer] and not pair["candidate"][layer]]
for case_id, pair in paired.items()
}
layer_regressions = {key: layers for key, layers in layer_regressions.items() if layers}
return {"cases": len(paired), "paired": paired, "regressions": list(layer_regressions),
"layer_regressions": layer_regressions, "gate_passed": not layer_regressions,
"baseline_outcomes": sum(row["outcome"] for row in baseline.values()),
"candidate_outcomes": sum(row["outcome"] for row in candidate.values())}Selected lines from compare_versions(): pair stable keys and flag any true→false change across all six protected layers. The full function rejects missing cases or malformed score vectors. Deliberately reversed candidate output order does not change the result.
Companion output
Deterministic replaypaired_cases: 3
baseline_outcomes: 3/3
candidate_outcomes: 2/3
regressions: ["DEMO-REFUND"]
failed_layers: ["selection", "outcome"]
gate_passed: FalseThe fixture identifies the omitted refund even though grounding passes; a policy-only regression would also fail the gate. A passing gate means no prior pass became a failure—not that all cases passed or that a release is approved.

What to watch
- Start with the known-good experiment overview: look for the dimension that regresses.
- Compare the same eligible-refund case in v2 and v3: v2 processes $98; v3 requests confirmation.
- Read the scores beside the behavior: grounding and policy pass in both, while selection and outcome fail in v3.
This saved practice snapshot contains 3 failure cases and 3 known-good cases. Separately, identity-aligning the graded notebook’s five-case failure set gives the same grounding score for v1/v2/v3: 3/5, with the same failures.
The practice and graded experiments are different datasets and rubrics. These captures do not establish a general prompt improvement or a causal latency/cost saving.Preserve the failing identity, expected action and observed trace as a regression case. The next version starts with evidence and a test, rather than another unsupported prompt change.
All recordings show still images.
Run the cumulative example
Read it here.
Inspect it in the repository.
The companion runs locally with Python’s standard library. It replays fixtures so every stage can be inspected without credentials, model costs or a hosted course runtime.
python -m technical_companionEducational material / course attribution
Course foundation.
Inspectable technical work.
This showcase documents coursework from Evaluating and Improving Agentic Applications by DeepLearning.AI, taught by Laurie Voss and built with Arize AI. Northwind, the supplied agent and the course exercises belong to their creators.
The public repository contains original analysis, reviewed recording excerpts and independently authored companion code prepared with AI assistance. Original course notebooks and solutions remain local. Their cell order is preserved; missing helpers and data snapshots prevent a complete local reproduction of the hosted labs.