Skip to main content
In this tutorial, we’ll build a customer support bot that helps users navigate a digital music store. Then, we’ll go through the three most effective types of evaluations to run on chat bots:
  • Final response: Evaluate the agent’s final response.
  • Trajectory: Evaluate whether the agent took the expected path (e.g., of tool calls) to arrive at the final answer.
  • Single step: Evaluate any agent step in isolation (e.g., whether it selects the appropriate first tool for a given step).
We’ll build our agent using LangGraph, but the techniques and LangSmith functionality shown here are framework-agnostic.

Setup

Configure the environment

Let’s install the required dependencies:
Let’s set up environment variables for OpenAI and LangSmith:

Download the database

We will create a SQLite database for this tutorial. SQLite is a lightweight database that is easy to set up and use. We will load the chinook database, which is a sample database that represents a digital media store. For more information, see Chinook sample database. For convenience, we have hosted the database in a public GCS bucket:
Here’s a sample of the data in the db:
And here’s the database schema (image from https://github.com/lerocha/chinook-database): Chinook DB

Define the customer support agent

We’ll create a LangGraph agent with limited access to our database. For demo purposes, our agent will support two basic types of requests:
  • Lookup: The customer can look up song titles, artist names, and albums based on other identifying information. For example: “What songs do you have by Jimi Hendrix?”
  • Refund: The customer can request a refund on their past purchases. For example: “My name is Claude Shannon and I’d like a refund on a purchase I made last week, could you help me?”
For simplicity in this demo, we’ll implement refunds by deleting the corresponding database records. We’ll skip implementing user authentication and other production security measures. The agent’s logic will be structured as two separate subgraphs (one for lookups and one for refunds), with a parent graph that routes requests to the appropriate subgraph.

Refund agent

Let’s build the refund processing agent. This agent needs to:
  1. Find the customer’s purchase records in the database
  2. Delete the relevant Invoice and InvoiceLine records to process the refund
We’ll create two SQL helper functions:
  1. A function to execute the refund by deleting records
  2. A function to look up a customer’s purchase history
To make testing easier, we’ll add a “mock” mode to these functions. When mock mode is enabled, the functions will simulate database operations without actually modifying any data.
Now let’s define our graph. We’ll use a simple architecture with three main paths:
  1. Extract customer and purchase information from the conversation
  2. Route the request to one of three paths:
    • Refund path: If we have sufficient purchase details (Invoice ID or Invoice Line IDs) to process a refund
    • Lookup path: If we have enough customer information (name and phone) to search their purchase history
    • Response path: If we need more information, respond to the user requesting the specific details needed
The graph’s state will track:
  • The conversation history (messages between user and agent)
  • All customer and purchase information extracted from the conversation
  • The next message to send to the user (followup text)
We can visualize our refund graph:
Refund graph

Lookup agent

For the lookup (i.e. question-answering) agent, we’ll use a simple ReACT architecture and give the agent tools for looking up track names, artist names, and album names based on various filters. For example, you can look up albums by a particular artist, artists who released songs with a specific name, etc.
QA Graph

Parent agent

Now let’s define a parent agent that combines our two task-specific agents. The only job of the parent agent is to route to one of the sub-agents by classifying the user’s current intent, and to compile the output into a followup message.
We can visualize our compiled parent graph including all of its subgraphs:
graph

Try it out

Let’s give our custom support agent a whirl!

Evaluations

Now that we’ve got a testable version of our agent, let’s run some evaluations. Agent evaluation can focus on at least 3 things:
  • Final response: The inputs are a prompt and an optional list of tools. The output is the final agent response.
  • Trajectory: As before, the inputs are a prompt and an optional list of tools. The output is the list of tool calls
  • Single step: As before, the inputs are a prompt and an optional list of tools. The output is the tool call.
Let’s run each type of evaluation:

Final response evaluator

First, let’s create a dataset that evaluates end-to-end performance of the agent. For simplicity we’ll use the same dataset for final response and trajectory evaluation, so we’ll add both ground-truth responses and trajectories for each example question. We’ll cover the trajectories in the next section.
We’ll create a custom LLM-as-judge evaluator that uses another model to compare our agent’s output on each example to the reference response, and judge if they’re equivalent or not:
Now we can run our evaluation. Our evaluator assumes that our target function returns a ‘response’ key, so lets define a target function that does so. Also remember that in our refund graph we made the refund node configurable, so that if we specified config={"env": "test"}, we would mock out the refunds without actually updating the DB. We’ll use this configurable variable in our target run_graph method when invoking our graph:
You can see what these results look like here: LangSmith link.

Trajectory evaluator

As agents become more complex, they have more potential points of failure. Rather than using simple pass/fail evaluations, it’s often better to use evaluations that can give partial credit when an agent takes some correct steps, even if it doesn’t reach the right final answer. This is where trajectory evaluations come in. A trajectory evaluation:
  1. Compares the actual sequence of steps the agent took against an expected sequence
  2. Calculates a score based on how many of the expected steps were completed correctly
For this example, our end-to-end dataset contains an ordered list of steps that we expect the agent to take. Let’s create an evaluator that checks the agent’s actual trajectory against these expected steps and calculates what percentage were completed:
Now we can run our evaluation. Our evaluator assumes that our target function returns a ‘trajectory’ key, so lets define a target function that does so. We’ll need to usage LangGraph’s streaming capabilities to record the trajectory. Note that we are reusing the same dataset as for our final response evaluation, so we could have run both evaluators together and defined a target function that returns both “response” and “trajectory”. In practice it’s often useful to have separate datasets for each type of evaluation, which is why we show them separately here:
You can see what these results look like here: LangSmith link.

Single step evaluators

While end-to-end tests give you the most signal about your agents performance, for the sake of debugging and iterating on your agent it can be helpful to pinpoint specific steps that are difficult and evaluate them directly. In our case, a crucial part of our agent is that it routes the user’s intention correctly into either the “refund” path or the “question answering” path. Let’s create a dataset and run some evaluations to directly stress test this one component.
You can see what these results look like here: LangSmith link.

Reference code

Here’s a consolidated script with all the above code: