Free lesson · GenAI Safety & Evaluation Engineering
Score agent tool selection with DeepEval 3.0 and Vertex AI Agent Evaluation
You will evaluate whether an agent selects the correct tools for each step using two modern frameworks. DeepEval 3.0 approach: use the @observe decorator to automatically map entire execution traces of AI agents — DeepEval maps tool invocations, memory access, retrieval steps, and generation calls into an evaluation graph. Apply component-level metrics to any step: tools, memories, retrievers, generators. Vertex AI Agent Evaluation approach: use the native trajectory metrics — trajectory_exact_match (strict ordered match), trajectory_in_order_match (subset in order), trajectory_any_order_match (subset any order), trajectory_precision (fraction of agent actions that are correct), and trajectory_recall (fraction of expected actions taken). Build a ToolSelectionEvaluator that takes an agent trajectory and expected_tools list. Test with an MCP-based agent that has access to hosted LLM APIs (OpenAI, Gemini), a database tool, and a search tool. Evaluate on 30 tasks with known optimal tool sequences. Compare: DeepEval @observe (code-native, pytest-integrated) vs Vertex AI (cloud-native, GCP-integrated) — which provides more actionable insights for debugging agent failures?
Course: GenAI Evaluation, Safety & Governance · Chapter 5 · Agent Trajectory Evaluation
Free to read — no subscription required.
Introduction
When you evaluate a multi-step agent only on its final answer, you cannot tell whether it picked the right tool, recovered from a failed reasoning step, or got lucky on a path that won't generalize — a bad tool choice, a skipped retrieval, and a flawed planning step all collapse into one opaque score. In this lesson you'll use DeepEval's @observe decorator to instrument each function in a multi-step agent so every LLM call, tool invocation, and synthesis step emits a structured span. By the end you'll be able to attach the captured trace to an LLMTestCase and run trajectory-aware metrics (ToolCorrectnessMetric, AgentGoalAccuracyMetric) that score individual segments of the run, turning agent evaluation into a per-step diagnostic instead of a single pass/fail verdict.
Key Terminology
@observedecorator: DeepEval function wrapper (type="llm",type="tool",type="retriever", etc.) that emits a structured span for each call, recording input, output, latency, and parent–child relationships in the active trace.- Trace span: A single recorded execution unit inside a trace tree, produced by an
@observe-wrapped function and carrying metadata that component-level metrics consume. TraceManagercontext: Context manager that opens a root trace, captures every nested@observespan emitted inside itswithblock, and returns the assembled hierarchical trace object.ToolCorrectnessMetric: Trajectory metric that compares tools the agent actually invoked (from the trace) againstexpected_toolson the test case, reporting precision and recall.AgentGoalAccuracyMetric: Reference-free judge metric that scores whether the agent's final output satisfies the user's stated goal, suitable for open-ended tasks with multiple correct answers.
Concepts
@observe-based trajectory evaluation rests on three ideas working together. First, decoration is the instrumentation contract: wrapping a function with @observe(type=..., name=...) is what makes it visible to DeepEval's trace pipeline — untagged functions stay invisible, so coverage is a deliberate authoring choice rather than automatic. Second, the trace tree is the evaluation substrate: spans nest under whichever decorated function is currently on the call stack, so wrapping run_agent callees inside a TraceManager block produces a hierarchical record of planning → tool use → synthesis that mirrors actual execution order. Third, metrics operate on segments, not just outputs: ToolCorrectnessMetric reads the tool spans out of the trace and checks them against expected_tools; AgentGoalAccuracyMetric reads the goal and the final answer and asks an LLM judge whether the trajectory satisfied intent. Because both metrics live on the same LLMTestCase, a single evaluate(...) call returns separate scores and reasons for tool selection and goal achievement — letting you tell apart "agent picked the wrong tool" from "agent picked the right tool but synthesized a bad answer" (see Code Walkthrough).
Code Walkthrough
Building on the three ideas just covered, the snippet below demonstrates them together: @observe decorators tag each agent step as an llm or tool span, the TraceManager block collects nested spans into one hierarchical trace, and evaluate runs the two component-level metrics against that trace.
Code snippetpython
1from deepeval import evaluate 2from deepeval.tracing import observe, TraceManager 3from deepeval.metrics import ToolCorrectnessMetric, AgentGoalAccuracyMetric 4from deepeval.test_case import LLMTestCase 5 6@observe(type="llm", name="planner") 7def plan_next_action(query: str) -> str: 8 response = llm_client.chat(messages=[{"role": "user", "content": query}]) 9 return response.content 10 11@observe(type="tool", name="search_api") 12def search_documents(query: str) -> list[dict]: 13 return vector_store.similarity_search(query, k=5) 14 15@observe(type="llm", name="synthesizer") 16def synthesize_answer(context: list[dict], query: str) -> str: 17 prompt = f"Context: {context}\nQuestion: {query}" 18 return llm_client.chat(messages=[{"role": "user", "content": prompt}]).content 19 20def run_agent(user_query: str): 21 with TraceManager() as trace: 22 action = plan_next_action(user_query) 23 docs = search_documents(action) 24 answer = synthesize_answer(docs, user_query) 25 return answer, trace 26 27answer, trace = run_agent("What is our refund policy?") 28test_case = LLMTestCase( 29 input="What is our refund policy?", 30 actual_output=answer, 31 expected_tools=["search_api"], 32 trace=trace, 33) 34results = evaluate( 35 test_cases=[test_case], 36 metrics=[ToolCorrectnessMetric(threshold=0.8), AgentGoalAccuracyMetric()], 37)
The three @observe-decorated functions emit one span each per call; nesting is automatic because every call happens inside the TraceManager context, so the resulting trace mirrors planner → tool → synthesizer order. LLMTestCase ties that trace to expected_tools=["search_api"], and evaluate returns one score per metric with a reason field explaining the judgment. When ToolCorrectnessMetric reports low precision the agent called a tool not in expected_tools; low recall means it skipped a required tool. AgentGoalAccuracyMetric returns a reference-free judgment on whether the final answer satisfies the input. You'll know it works when the result contains both metrics scored with non-empty reason strings.
Do's and Don'ts
Do's
- ✓Do decorate every agent function with
@observe(type=..., name=...)— settype="llm"forplan_next_action/synthesize_answerandtype="tool"forsearch_documentssoToolCorrectnessMetriccan correctly separate tool spans from LLM spans; using the wrongtypecauses the metric to misclassify calls and report misleading precision/recall scores. - ✓Do wrap the full agent run in a
TraceManagercontext and pass the returned trace object toLLMTestCase(trace=trace)— spans emitted by@observeare only collected into a hierarchical trace while aTraceManageris active, so callingplan_next_action,search_documents, orsynthesize_answeroutside that block silently discards the spans and leavesevaluatewith nothing to score. - ✓Do populate
expected_toolswith the exact strings passed to@observe(name=...)—ToolCorrectnessMetricmatches tool span names againstexpected_toolsby string equality, so listing"search_api"while the decorator readsname="search_documents"counts as both a missed required tool and an unexpected invocation, zeroing precision and recall simultaneously.
Don'ts
- ✗Don't evaluate a multi-step agent using only output-level metrics like exact-match against the final answer — doing so collapses planner failures, wrong tool choices, and skipped retrievals into a single opaque score, making the per-step span data emitted by
@observeinvisible to any metric and defeating the purpose of trajectory instrumentation. - ✗Don't rename a tool's
@observe(name=...)string without updatingexpected_toolsin everyLLMTestCasethat references it —ToolCorrectnessMetrictreats the old name as a missing expected tool and the new name as an unexpected invocation, so a cosmetic rename silently produces a zero precision-recall score with no error message to alert you. - ✗Don't apply
@observeto helper functions that aggregate or orchestrate the decorated steps (likerun_agentitself) without also keeping theTraceManagerinside it — nesting span emission outside an activeTraceManagercontext drops those spans, and the trace attached toLLMTestCasewill be incomplete, causingAgentGoalAccuracyMetricto judge the run on a truncated trajectory missing the planner or synthesizer steps.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Safety & Evaluation Engineering subscription.
From · cancel anytime · Already a subscriber? Sign in →
More free lessons in GenAI Evaluation, Safety & Governance
- Ch 1Build a stratified evaluation dataset
- Ch 1Detect dataset contamination and leakage
- Ch 3Implement RAGAS metrics for RAG evaluation
- Ch 3Build DeepEval test suites for RAG
- Ch 5Score agent tool selection with DeepEval 3.0 and Vertex AI Agent EvaluationYou are here
- Ch 5Build agent benchmarks with task suites
- Ch 7Design A/B experiments for prompt variants