Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Simple ReAct Agent using LangChain and OpenRouter

Authoerd by Dr.Tiziana Ligorio for AI Agents - CSCI 395.32 taught at Hunter College of The City University of New York
This demo has two parts.
In Part 1

We build a simple ReAct agent using the LangChain framework to illustrate how much a framework can abstract and automate compared to our previous demo, where we implemented the agent loop from scratch.

Note that, because a lot of the agent logic is built within the framework, as a designer, you have much less control over context engineering. We will discuss context engineering in depth in future lectures.

As before, the agent follows the ReAct pattern by iteratively alternating between reasoning and acting to accomplish a task. However, in this version most of the control flow, such as managing the reasoning loop, invoking tools, and handling intermediate state, is handled by the framework (and thus standardized) rather than implemented directly by the agent developer.
Here are the LangChain docs for your reference.

For this tutorial, we will demonstrate access to search tools (web search and Wikipedia), which it can use to gather information and answer user queries. We will then show how to include a tool for fetching via MCP, and a custom tool for filtering.

In Part 2

We demonstrate how to trace and run evals using LangSmith. This part will be covered after our lecture on evals.
Here are the LangSmith docs for your reference.

ReAct Agent with LanghChain

Installs and Imports

%%capture
!pip install langchain langchain-openai langchain-community langsmith google-search-results wikipedia

%%capture hides the output

# LangChain: the core framework
# Defines agents, executors, prompts, and how LLMs + tools are orchestrated
from langchain.agents import create_agent
from langchain_core.tools import tool

# LangChain Community:
# Contains third-party tools like Wikipedia, search APIs, web loaders, etc.
from langchain_community.agent_toolkits.load_tools import load_tools

# LangSmith: observability + prompt hub
# Used here to fetch a pre-built ReAct prompt (and possibly for tracing/debugging)
from langsmith import Client

# OpenAI integration for LangChain
# Provides ChatOpenAI, a LangChain-compatible LLM wrapper
from langchain_openai import ChatOpenAI

# Standard library
import os
import requests
import re

OpenRouter is a unified API that provides access to various LLMs through a single interface. It offers a generous free tier and affordable token usage for minimal cost, making it ideal for learning and experimentation.

If you already pay for other LLM providers or prefer to use a different service, you are welcome to adapt the code accordingly.

Setup your API Keys

Step 1 — Get an OpenRouter API key

For this demo we will use an LLM via OpenRouter, which requires an API key.

  1. Go to https://openrouter.ai

  2. Sign in (or create an account if you don’t have one)

  3. Once logged in, navigate to https://openrouter.ai/settings/keys

  4. Click Create Key

  5. Give the key a name, e.g. colab-langchain_ReAct_agent

  6. Copy the key immediately (you won’t be able to see it again)

Important: Treat this key like a password. Do not share it, paste it into notebooks, or commit it to GitHub.

Step 2 — Add a secret in Colab (UI)

  1. On the left sidebar, click 🔑 Secrets

  2. Add a new secret:

  • Name: OPENROUTER_API_KEY

  • Value: your actual API key

  1. Toggle the switch to the left to give notebook access (you should see a checkmark)

If running locally — Add a secret in .env

  1. Create a .env file in the project root:

touch .env

  1. Add the following (replace with your own key):

OPENROUTER_API_KEY=your_openrouter_key_here.

Important: Never paste API keys into code cells.

Step 3 — Get a SerpAPI key

SerpAPI is used to access search tools (e.g. Google search) from agents. It requires a separate account and API key.

  1. Go to https://serpapi.com

  2. Click Sign up

  3. Create an account or login if you have one (SerpAPI offers a free tier that is sufficient for this course.)

  4. Confirm your email and phone number if prompted

  5. Once logged in, go to your dashboard (https://serpapi.com/dashboard)

  6. Locate and copy your API Key

Step 4 — Add a secret in Colab (UI)

  1. On the left sidebar, click 🔑 Secrets

  2. Add a new secret:

  • Name: SERPAPI_API_KEY

  • Value: your actual API key

  1. Toggle the switch to the left to give notebook access (you should see a checkmark)

If running locally — Add a secret in .env

  1. Add the following to .env (replace with your own key):

SERPAPI_API_KEY=your_serpapi_key_here.

Important: Never paste API keys into code cells.

Load the API Keys

In Colab (For Part 1 Only):

Uncomment and run the cell below if you’re using Google Colab.

# # Load API keys from Colab Secrets into environment variables if running in colab
# from google.colab import userdata

# keys = ["OPENROUTER_API_KEY", "SERPAPI_API_KEY"]

# for key in keys:
#     value = userdata.get(key)
#     assert value is not None, f"{key} not found in Colab Secrets or access is disabled"
#     os.environ[key] = value

# print("API keys successfully loaded")

Locally:

# Load API keys from .env if running locally - see local install instructions in the repo README
from dotenv import load_dotenv
load_dotenv()
True
# sanity check
print("OPENROUTER_API_KEY present:", bool(os.getenv("OPENROUTER_API_KEY")))
print("SERPAPI_API_KEY present:", bool(os.getenv("SERPAPI_API_KEY")))
OPENROUTER_API_KEY present: True
SERPAPI_API_KEY present: True

These will be used in Part 2. If you are running Part 1 only, comment these out.

# Configure LangSmith tracing here, before any LangChain objects are created.
# Setting these after the kernel has already run agent calls can cause
# a "ValueError: I/O operation on closed file" due to async thread conflicts
# between LangSmith's tracing backend and Jupyter's output stream.
if not os.getenv("LANGSMITH_API_KEY"):
    print("Warning: LANGSMITH_API_KEY not set; tracing disabled")
else:
    os.environ["LANGSMITH_TRACING"] = "true"
    os.environ["LANGSMITH_API_KEY"] = os.getenv("LANGSMITH_API_KEY")
    os.environ["LANGSMITH_PROJECT"] = "react-agent-demo"
    os.environ["LANGSMITH_ENDPOINT"] = "https://api.smith.langchain.com"

Part 1: ReAct Agent with LangChain

Load the tools our Agent will have access to

 # Load LangChain's built-in wrappers for SerpAPI (web search) and Wikipedia as agent tools  
tools = load_tools(["serpapi", "wikipedia"])

Load the LLM and setup the Agent

In this demo we use the gpt-4o-mini model via OpenRouter. This is a lightweight and cost-efficient model that works well for agentic workflows involving tool use. You may change the model by selecting one available on the OpenRouter model list. Setting the temperature to zero makes the agent’s behavior deterministic, which is we choose for demos and debugging: given the same input, the agent will tend to make the same decisions and tool calls.

model = ChatOpenAI(
    model="openai/gpt-4o-mini",
    temperature=0,
    max_tokens=600,
    openai_api_base="https://openrouter.ai/api/v1",
    openai_api_key=os.getenv("OPENROUTER_API_KEY")
)
agent = create_agent(
    model=model,
    tools=tools,
    system_prompt=(
        "You are a research assistant.\n"
        "For research questions, do not rely on a single source.\n"
        "Use multiple search or information-gathering tool calls to cover "
        "different time periods, perspectives, or subtopics before answering.\n"
        "Only provide a final answer after you have gathered information "
        "from multiple sources."
        )
    )

That is all we need to do to setup the Agent! LangChain will handle the logic. Note how short our system prompt is now. The short system prompt is enough because the framework now implements the ReAct loop.

In our previous demo (ReAct from scratch), the LLM drove the ReAct loop and the system prompt had to explicity specify:

AspectResponsibilities
Control loopThought → Action → Observation
Repeat up to max_iterations
Action protocolJSON format
One action at a time
Tool routingTool names
Tool schemas
TerminationWhen to stop
Exact Final Answer format
Error preventionRounding rules
Allowed operations
Strict formatting constraints
Prompt roleProgram
State machine
Protocol definition

With a framework instead:

  • The logic is no longer in the prompt

  • The logic LLM → decide → tool → observe → repeat → stop is implemented in code

  • The framwork handles:

    • Iteration (including how many times it can loop)

    • Tool invocation (what tools exist and how to call them)

    • Passing observations back to the model

    • Termination conditions

    • Output structure

When we use an agent framework, the reasoning loop moves from the prompt into code, so the prompt can be short and declarative.

As a developer, it is however important to understand exactly how it works before relying on the abstractions afforded by the frameworks.

Run the Agent

answer = agent.invoke({
    "messages": [
        {"role": "user", "content": "Who is Tiziana Ligorio?"}
    ]
})
answer
{'messages': [HumanMessage(content='Who is Tiziana Ligorio?', additional_kwargs={}, response_metadata={}, id='421578ed-330b-4df0-9777-a561c5248112'), AIMessage(content='', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 53, 'prompt_tokens': 188, 'total_tokens': 241, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}, 'cost': 6e-05, 'is_byok': False, 'cost_details': {'upstream_inference_cost': 6e-05, 'upstream_inference_prompt_cost': 2.82e-05, 'upstream_inference_completions_cost': 3.18e-05}}, 'model_provider': 'openai', 'model_name': 'openai/gpt-4o-mini', 'system_fingerprint': 'fp_373a14eb6f', 'id': 'gen-1772118972-DRfTYuSf15wmNJw0stTF', 'finish_reason': 'tool_calls', 'logprobs': None}, id='lc_run--019c9a85-95c6-7512-91bb-f5a61797906f-0', tool_calls=[{'name': 'wikipedia', 'args': {'query': 'Tiziana Ligorio'}, 'id': 'call_Mji6xBDcspC4IZPt5LHTDtSr', 'type': 'tool_call'}, {'name': 'Search', 'args': {'__arg1': 'Tiziana Ligorio news'}, 'id': 'call_fpY4bEbjpoLPtistBs3BkfzB', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 188, 'output_tokens': 53, 'total_tokens': 241, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}), ToolMessage(content="Page: Warren Neidich\nSummary: Warren Neidich ( NYE-dik) is an American artist who lives in Berlin and Los Angeles. He was a professor at Kunsthochschule Weißensee School of Art, Berlin and visiting scholar at Otis College of Art and Design, Los Angeles.\nNeidich is founding director of the Saas-Fee Summer Institute of Art (SFSIA). He has collaborated with artists, curators and critics including: Barry Schwabsky (co-director of SFSIA), Armen Avanessian, Nicolas Bourriaud, Tiziana Terranova, Franco Berardi, Hans-Ulrich Obrist, Isaac Julien, Hito Steyerl, Chris Kraus (American writer), and many others.\nHis work has been exhibited at numerous institutions including: MoMA PS1, Whitney Museum of American Art, LACMA – Los Angeles County Museum of Art, California Museum of Photography, ICA – Institute of Contemporary Arts, London, Museum Ludwig, Cologne, and Walker Art Center, Minneapolis, Minnesota.\nIn relation to his exhibitions and extended theories he has edited and published over 10 books, including Neuromacht, Merve Verlag (German), 2017, the Psychopathologies of Cognitive Capitalism: Part One (2013), Two (2014), and Three (2017), Archive Books (English), the Noologist's Handbook and Other Art Experiments, Anagram, 2013, From Noopower to Neuropower: How Mind Becomes Matter, 2010 and, Cognitive Architecture. From Biopolitics to Noopolitics. Architecture & Mind in the Age of Communication and Information, 2010.\nHe was collaborator, along with Elena Bajo and others, on Exhibition 211 in New York, 2009.", name='wikipedia', id='aff527df-ce16-4ace-a3e4-e4120de1d3dd', tool_call_id='call_Mji6xBDcspC4IZPt5LHTDtSr'), ToolMessage(content='[\'Tiziana Ligorio is a doctoral lecturer in the Department of Computer Science. See Contact Details. Educational Background. PhD, The Graduate Center of The City ...\', \'This new paradigm will render our current approach with LLMs obsolete. I did my best to represent the view that LLMs will function as the foundation on which ...\', \'Tiziana Ligorio, a Hunter computer science doctoral lecturer who together with Epstein teaches a deep machine learning class at the City ...\', \'Tiziana Ligorio. Computer Science PhD. I am a Doctoral Lecturer of Computer Science at Hunter College, The City University of New York. Email me · GitHub ...\', \'Recognizes undergraduate students in North American universities who show outstanding research potential in an area of computing research.\', \'Tiziana Ligorio. Doctoral Lecturer. Research Areas: Machine Learning, Spoken ... NEWS. Hunter College Schools. School of Arts & Sciences · School of Education ...\', \'I recently received a notice from Whirlpool that my water filter was due to be changed. Along with this notice was a coupon with a discounted ...\', "Location: Brooklyn · 500+ connections on LinkedIn. View Tiziana Ligorio, PhD\'s profile on LinkedIn, a professional community of 1 billion members.", \'Tiziana Ligorio and Marco Boggiosella attend NEW MUSEUM opening for AFTER NATURE at New Museum on the Bowery on July 16, 2008 in New York City.\']', name='Search', id='1da812a7-98bb-403e-8d9e-4dee4015f81b', tool_call_id='call_fpY4bEbjpoLPtistBs3BkfzB'), AIMessage(content='Tiziana Ligorio is a doctoral lecturer in the Department of Computer Science at Hunter College, part of The City University of New York. She specializes in areas such as machine learning and spoken language processing. Her academic background includes a PhD from The Graduate Center of the City University of New York.\n\nIn addition to her teaching role, she is involved in research and has contributed to the field of computing, particularly in the context of deep machine learning. There are mentions of her collaborating with other educators in the field, indicating an active engagement in academic discourse and development.\n\nThere is limited information available about her outside of academic contexts, and no recent news articles specifically highlight her work or contributions.', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 138, 'prompt_tokens': 955, 'total_tokens': 1093, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}, 'cost': 0.00022605, 'is_byok': False, 'cost_details': {'upstream_inference_cost': 0.00022605, 'upstream_inference_prompt_cost': 0.00014325, 'upstream_inference_completions_cost': 8.28e-05}}, 'model_provider': 'openai', 'model_name': 'openai/gpt-4o-mini', 'system_fingerprint': 'fp_373a14eb6f', 'id': 'gen-1772118979-HA2o9FFmy9AlvfRrZG4h', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019c9a85-b41c-7200-8e54-afea38983fbf-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 955, 'output_tokens': 138, 'total_tokens': 1093, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]}

This is too much to look at! Let’s break it down slowly.

When we call agent.invoke, the returned object contains the full internal execution state of the agent, including:

  • messages

  • tool calls

  • tool outputs

  • metadata

  • the final response.

We use build_agent_trace(result) to reorganize this raw output into a readable, step-by-step trace that makes it easier to inspect what the agent did, which tools it called, and how it arrived at its final answer.

Let’s inspect the components of the answer first to understand how to extract data from it to build the trace.

The answer has a list of messages, and each message has a type (the node that issued it)

for msg in answer["messages"]:
    print(msg.type)
human
ai
tool
tool
ai

AI messages may issue a tool call

for msg in answer["messages"]:
    mtype = msg.type
    if mtype == "ai":
            # Action(s): tool calls
            if getattr(msg, "tool_calls", None):
                for call in msg.tool_calls:
                    print(call)
                    # trace.append({"type": "action", "tool": call["name"], "args": call["args"]})
{'name': 'wikipedia', 'args': {'query': 'Tiziana Ligorio'}, 'id': 'call_Mji6xBDcspC4IZPt5LHTDtSr', 'type': 'tool_call'}
{'name': 'Search', 'args': {'__arg1': 'Tiziana Ligorio news'}, 'id': 'call_fpY4bEbjpoLPtistBs3BkfzB', 'type': 'tool_call'}

Tool messages contain observations: the data returned by the tool. Rather than storing the full tool output (msg.content) in the trace, which can be large and clutter the display, we record only the length of the result. This keeps the trace compact and easier to inspect while still conveying how much information the tool returned.

for msg in answer["messages"]:
    mtype = msg.type
    if mtype == "tool":
        print(msg)
        # trace.append({"type": "observation", "tool": msg.name, "chars": n})
content="Page: Warren Neidich\nSummary: Warren Neidich ( NYE-dik) is an American artist who lives in Berlin and Los Angeles. He was a professor at Kunsthochschule Weißensee School of Art, Berlin and visiting scholar at Otis College of Art and Design, Los Angeles.\nNeidich is founding director of the Saas-Fee Summer Institute of Art (SFSIA). He has collaborated with artists, curators and critics including: Barry Schwabsky (co-director of SFSIA), Armen Avanessian, Nicolas Bourriaud, Tiziana Terranova, Franco Berardi, Hans-Ulrich Obrist, Isaac Julien, Hito Steyerl, Chris Kraus (American writer), and many others.\nHis work has been exhibited at numerous institutions including: MoMA PS1, Whitney Museum of American Art, LACMA – Los Angeles County Museum of Art, California Museum of Photography, ICA – Institute of Contemporary Arts, London, Museum Ludwig, Cologne, and Walker Art Center, Minneapolis, Minnesota.\nIn relation to his exhibitions and extended theories he has edited and published over 10 books, including Neuromacht, Merve Verlag (German), 2017, the Psychopathologies of Cognitive Capitalism: Part One (2013), Two (2014), and Three (2017), Archive Books (English), the Noologist's Handbook and Other Art Experiments, Anagram, 2013, From Noopower to Neuropower: How Mind Becomes Matter, 2010 and, Cognitive Architecture. From Biopolitics to Noopolitics. Architecture & Mind in the Age of Communication and Information, 2010.\nHe was collaborator, along with Elena Bajo and others, on Exhibition 211 in New York, 2009." name='wikipedia' id='aff527df-ce16-4ace-a3e4-e4120de1d3dd' tool_call_id='call_Mji6xBDcspC4IZPt5LHTDtSr'
content='[\'Tiziana Ligorio is a doctoral lecturer in the Department of Computer Science. See Contact Details. Educational Background. PhD, The Graduate Center of The City ...\', \'This new paradigm will render our current approach with LLMs obsolete. I did my best to represent the view that LLMs will function as the foundation on which ...\', \'Tiziana Ligorio, a Hunter computer science doctoral lecturer who together with Epstein teaches a deep machine learning class at the City ...\', \'Tiziana Ligorio. Computer Science PhD. I am a Doctoral Lecturer of Computer Science at Hunter College, The City University of New York. Email me · GitHub ...\', \'Recognizes undergraduate students in North American universities who show outstanding research potential in an area of computing research.\', \'Tiziana Ligorio. Doctoral Lecturer. Research Areas: Machine Learning, Spoken ... NEWS. Hunter College Schools. School of Arts & Sciences · School of Education ...\', \'I recently received a notice from Whirlpool that my water filter was due to be changed. Along with this notice was a coupon with a discounted ...\', "Location: Brooklyn · 500+ connections on LinkedIn. View Tiziana Ligorio, PhD\'s profile on LinkedIn, a professional community of 1 billion members.", \'Tiziana Ligorio and Marco Boggiosella attend NEW MUSEUM opening for AFTER NATURE at New Museum on the Bowery on July 16, 2008 in New York City.\']' name='Search' id='1da812a7-98bb-403e-8d9e-4dee4015f81b' tool_call_id='call_fpY4bEbjpoLPtistBs3BkfzB'

Now that we have broken down the structure of the answer, let’s parse it into a step-by-step trace that makes it easier to inspect the agent’s actions.

def build_agent_trace(result):
    trace = []
    for msg in result["messages"]:
        if msg.type == "human":
            trace.append({"type": "user", "content": msg.content})

        elif msg.type == "ai" and getattr(msg, "tool_calls", None):
            for call in msg.tool_calls:
                trace.append({"type": "action", "tool": call["name"], "args": call["args"]})

        elif msg.type == "tool":
            n = len(msg.content) if isinstance(msg.content, str) else None
            trace.append({"type": "observation", "tool": msg.name, "chars": n})

    final = result["messages"][-1].content
    return {"trace": trace, "final_answer": final}
trace = build_agent_trace(answer)
trace
{'trace': [{'type': 'user', 'content': 'Who is Tiziana Ligorio?'}, {'type': 'action', 'tool': 'wikipedia', 'args': {'query': 'Tiziana Ligorio'}}, {'type': 'action', 'tool': 'Search', 'args': {'__arg1': 'Tiziana Ligorio news'}}, {'type': 'observation', 'tool': 'wikipedia', 'chars': 1521}, {'type': 'observation', 'tool': 'Search', 'chars': 1340}], 'final_answer': 'Tiziana Ligorio is a Doctoral Lecturer in the Department of Computer Science at Hunter College, part of The City University of New York. She has a PhD from The Graduate Center of the City University of New York and specializes in areas such as machine learning and spoken language processing. \n\nIn addition to her teaching role, she is involved in research and has contributed to discussions on the implications of large language models (LLMs) in computing. Her work is recognized in the academic community, particularly for her contributions to machine learning education.\n\nThere is limited information available about her outside of academic contexts, and she does not appear to be a widely known public figure outside of her professional domain.'}

Much easier to trace now!

From this trace alone, it’s not obvious that the Wikipedia lookup was irrelevant (there is no Wikipedia page for me), which is why deeper inspection is sometimes necessary when results look questionable. Recognizing and investigating such mismatches is an important skill when debugging agent behavior.

Now let’s try to get our agent to dig a little deeper.

inputs = {"messages": [{"role": "user", "content": "Who is Tiziana Ligorio? Dig out different areas she has worked on from her early PhD years until now."}]}

answer = agent.invoke(inputs)
trace = build_agent_trace(answer)
trace
{'trace': [{'type': 'user', 'content': 'Who is Tiziana Ligorio? Dig out different areas she has worked on from her early PhD years until now.'}, {'type': 'action', 'tool': 'wikipedia', 'args': {'query': 'Tiziana Ligorio'}}, {'type': 'action', 'tool': 'Search', 'args': {'__arg1': 'Tiziana Ligorio research areas and contributions'}}, {'type': 'observation', 'tool': 'wikipedia', 'chars': 1521}, {'type': 'observation', 'tool': 'Search', 'chars': 1532}], 'final_answer': "Tiziana Ligorio is a researcher and educator in the field of computer science, particularly known for her work on machine learning and spoken dialogue systems. Here’s a summary of her career and contributions from her early PhD years to the present:\n\n1. **Early Research and PhD**:\n - Tiziana Ligorio's doctoral research focused on feature selection for spoken dialogue systems. This area involves the interaction between humans and machines, particularly how machines can understand and process spoken language.\n\n2. **Research Areas**:\n - **Machine Learning**: She has contributed to the field of machine learning, particularly in the context of naturalistic dialogue management and noisy speech recognition.\n - **Spoken Dialogue Systems**: Her work includes developing systems that can manage conversations in a naturalistic manner, which is crucial for improving human-computer interaction.\n - **Constraint Satisfaction**: This area involves solving problems where a set of constraints must be satisfied, which is relevant in various computational contexts.\n\n3. **Current Position**:\n - Tiziana Ligorio is a Doctoral Lecturer at Hunter College, part of The City University of New York. She teaches courses related to deep machine learning and has been involved in hands-on training for building and training neural networks.\n\n4. **Recent Research**:\n - Her recent studies have included topics such as encrypted Domain Name System (DNS) privacy, user interfaces, and the performance limitations of home WiFi networks. This indicates a broadening of her research interests beyond dialogue systems to include aspects of network security and user experience.\n\n5. **Publications and Citations**:\n - Ligorio has authored several research works, with a notable number of citations, indicating her contributions are recognized within the academic community.\n\nOverall, Tiziana Ligorio has evolved from focusing on spoken dialogue systems during her PhD to engaging in a wider range of topics in computer science, including machine learning applications and network privacy."}

Using MCP Server

MCP (Model Context Protocol) is an open standard developed by Anthropic that allows AI models to connect to external tools and data sources through a unified, portable protocol. Unlike LangChain’s built-in tool wrappers, MCP servers are standalone processes that any MCP-compatible client can use, not just LangChain.
So far, our agent has relied on LangChain’s built-in wrappers for SerpAPI and Wikipedia, which return short search snippets.
For demonstration purposes, we use an MCP server to fetch the full content of a web page, which LangChain has no built-in tool for.

The MCP connection code below is asynchronous. This is not specific to the mcp-server-fetch server; it is a general requirement of MCP:

  • Connecting to an MCP server involves I/O-bound work (spawning a subprocess and communicating over pipes for local servers, or sending HTTP requests for remote ones). asyncio allows the program to wait for that I/O without blocking everything else.

  • client.get_tools() from langchain_mcp_adapters is an async function, as it sends a request to the server asking what tools are available, then waits for the response.

The same async pattern would apply regardless of which MCP server you connect to.
In Jupyter, top-level await is supported natively, so no extra setup is needed (i.e. you don’t need to include it inside an async def function as you would in a python program or script).

Side Note - Where to look for MCP servers

The authoritative source are the official MCP servers repo: https://github.com/modelcontextprotocol/servers and the mcp registry: https://registry.modelcontextprotocol.io/

They list officially maintained servers with the exact command and args to use for each one.

Local vs Remote MCP Servers

MCP servers come in two flavors, and MultiServerMCPClient supports both — you just change the transport type:

Local (stdio) — the server runs as a subprocess on your machine. uvx or npx download and launch it automatically:

CommandPackage registryExample
uvxPyPI"mcp-server-fetch"
npxnpm"@modelcontextprotocol/server-github"
direct pathlocal"/path/to/server.py"
client = MultiServerMCPClient({
    "fetch": {
        "command": "uvx",
        "args": ["mcp-server-fetch"],
        "transport": "stdio"        # <-- local subprocess over stdin/stdout
    }
})

Remote (HTTP) — the server runs elsewhere and exposes an HTTP endpoint. You connect with a URL and authenticate with a token:

client = MultiServerMCPClient({
    "github": {
        "url": "https://api.githubcopilot.com/mcp/",
        "transport": "streamable_http",  # <-- remote server over HTTP
        "headers": {"Authorization": "Bearer <your_github_token>"}
    }
})

In both cases, after creating the client you call await client.get_tools() to retrieve the tools.

# Connect to mcp-server-fetch: a local MCP server that retrieves the full content of a URL.
# uvx downloads and runs the server automatically as a subprocess (no separate install needed).
# MultiServerMCPClient handles the connection lifecycle.
# To use a different MCP server, change the command/args (for local) or provide a url (for remote).

from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient({
    "fetch": {
        "command": "uvx",
        "args": ["mcp-server-fetch"],
        "transport": "stdio"
    }
})
fetch_tool = await client.get_tools()

for t in fetch_tool:
    t.handle_tool_error = True
# before adding the MCP fetch tool
[t.name for t in tools]
['Search', 'wikipedia']
for ft in fetch_tool:
    tools.append(ft)
# after adding the MCP fetch tool
[t.name for t in tools]
['Search', 'wikipedia', 'fetch']
agent = create_agent(
    model=model,
    tools=tools,
    system_prompt=(
        "You are a research assistant.\n"
        "For research questions, do not rely on a single source.\n"
        "Use multiple search or information-gathering tool calls to cover "
        "different time periods, perspectives, or subtopics before answering.\n"
        "When a search result returns a promising URL, use the fetch tool to "
        "retrieve the full page content for deeper information.\n"
        "If a fetch returns a 404 error, that URL does not exist — do NOT retry it. "
        "Move on to the next most promising URL from the search results, "
        "or proceed without fetching if no other URLs are available.\n"
        "Only provide a final answer after you have gathered information "
        "from multiple sources."
    )
)

Now that the agent has an async tool (the MCP fetch tool communicates over I/O under the hood), we use await agent.ainvoke() instead of agent.invoke(). While invoke() may still work in some cases, ainvoke() is the correct choice here because it allows the event loop to properly handle the async tool calls without blocking, which matters when a tool is waiting on network or subprocess I/O, as MCP tools are.

In a regular Python script, await can only appear inside an async def function. Writing it at the top level would raise a SyntaxError because the top-level scope is not an async context. Jupyter notebooks are different. IPython runs each cell inside an event loop and wraps cell execution in an async context automatically, so top-level await in a cell is valid. This is why answer = await agent.ainvoke(...) works directly in a cell, while the same line in a .py script would need to live inside an async def function.


answer = await agent.ainvoke({
    "messages": [
        {"role": "user", "content": "Who is Tiziana Ligorio? What has she been working on recently?"}
    ]
})
trace = build_agent_trace(answer)
trace
{'trace': [{'type': 'user', 'content': 'Who is Tiziana Ligorio? What has she been working on recently?'}, {'type': 'action', 'tool': 'wikipedia', 'args': {'query': 'Tiziana Ligorio'}}, {'type': 'action', 'tool': 'Search', 'args': {'__arg1': 'Tiziana Ligorio recent work 2023'}}, {'type': 'observation', 'tool': 'wikipedia', 'chars': 1521}, {'type': 'observation', 'tool': 'Search', 'chars': 1536}, {'type': 'action', 'tool': 'fetch', 'args': {'url': 'https://www.hunter.cuny.edu/cs/faculty/tiziana-ligorio'}}, {'type': 'observation', 'tool': 'fetch', 'chars': 88}, {'type': 'action', 'tool': 'fetch', 'args': {'url': 'https://www.researchgate.net/profile/Tiziana-Ligorio'}}, {'type': 'observation', 'tool': 'fetch', 'chars': 86}, {'type': 'action', 'tool': 'Search', 'args': {'__arg1': 'Tiziana Ligorio Hunter College profile'}}, {'type': 'observation', 'tool': 'Search', 'chars': 1521}], 'final_answer': 'Tiziana Ligorio is a Doctoral Lecturer in the Department of Computer Science at Hunter College, part of The City University of New York. She holds a PhD in Computer Science and specializes in areas such as machine learning, spoken dialogue systems, and constraint satisfaction.\n\nRecently, she has been involved in several educational initiatives and research projects. For instance, she is working on a program aimed at improving persistence among Hispanic and Black students in the computer science major. Additionally, she teaches courses such as Software Design and Analysis II, focusing on algorithms and data structures.\n\nHer research contributions include works on naturalistic dialogue management for noisy speech recognition, and she has published multiple papers in her field, garnering citations for her work. She is also engaged in preparing students for technical challenges, including online assessment workshops supported by NSF.\n\nOverall, Tiziana Ligorio is actively contributing to both teaching and research in computer science, with a focus on enhancing educational outcomes and advancing dialogue systems in technology.'}

Adding Custom Tool

For demonstration purposes, we add a custom tool to our search agent: a tool that scans search results for year mentions and groups the evidence by time period, helping the agent reason chronologically about the information it finds.

We use the @tool decorator to register this function as a tool that the agent can call, allowing LangChain to expose its inputs and outputs in a structured way. What required explicit JSON schemas and parsing logic in our previous demo is handled automatically by LangChain through a single tool decorator.
Note the importance of the docstring here, which effectively tells the Agent what this tool is and how/when to use it.

from typing import List, Dict, Any
from collections import defaultdict

@tool
def extract_year_timeline(results: List[str]) -> Dict[str, Any]:
    """
    Extract year mentions from search result snippets and group the snippets by year.

    Use this after a search tool call to quickly identify which time periods appear
    in the evidence (e.g., early years vs recent years) and to plan targeted follow-up searches.
    """
    year_to_snippets = defaultdict(list)
    year_pattern = re.compile(r"\b(19\d{2}|20\d{2})\b")

    for r in results:
        years = year_pattern.findall(r)
        for y in years:
            # keep a few distinct snippets per year
            if r not in year_to_snippets[y]:
                year_to_snippets[y].append(r)

    years_sorted = sorted(year_to_snippets.keys())
    return {
        "years": years_sorted,
        "by_year": dict(year_to_snippets),
        "count_years": len(years_sorted),
    }

Add the custom tool to the Agent and re-run the query

# before adding the custom tool
[t.name for t in tools]
['Search', 'wikipedia', 'fetch']
tools.append(extract_year_timeline)
# after adding the custom tool
[t.name for t in tools]
['Search', 'wikipedia', 'fetch', 'extract_year_timeline']

Let’s modify the system prompt to nudge the agent to use the custom tool as well.

agent = create_agent(
    model=model,
    tools=tools,
    system_prompt = (
    "You are a research assistant.\n"
    "For research questions, do not rely on a single source.\n"
    "Use multiple search or information-gathering tool calls to cover "
    "different time periods, perspectives, or subtopics before answering.\n"
    "After a web search, if the results mention years or dates, use the "
    "extract_year_timeline tool to identify relevant time periods and guide "
    "any follow-up searches.\n"
    "When a search result returns a promising URL, use the fetch tool to "
    "retrieve the full page content for deeper information.\n"
    "If a fetch returns a 404 error, that URL does not exist — do NOT retry it. "
    "Move on to the next most promising URL from the search results, "
    "or proceed without fetching if no other URLs are available.\n"
    "Only provide a final answer after you have gathered information "
    "from multiple sources."
    )
)
inputs = {"messages": [{"role": "user", "content": "Who is Tiziana Ligorio? Focus on her computer science academic career and research, from her early PhD years until now. Discuss how her interests might have shifted based on what she has been teaching in last few years."}]}

answer = await agent.ainvoke(inputs)


trace_obj = build_agent_trace(answer)
trace_obj 
{'trace': [{'type': 'user', 'content': 'Who is Tiziana Ligorio? Focus on her computer science academic career and research, from her early PhD years until now. Discuss how her interests might have shifted based on what she has been teaching in last few years.'}, {'type': 'action', 'tool': 'wikipedia', 'args': {'query': 'Tiziana Ligorio'}}, {'type': 'observation', 'tool': 'wikipedia', 'chars': 1521}, {'type': 'action', 'tool': 'Search', 'args': {'__arg1': 'Tiziana Ligorio computer science academic career research'}}, {'type': 'observation', 'tool': 'Search', 'chars': 1520}, {'type': 'action', 'tool': 'extract_year_timeline', 'args': {'results': ['Tiziana Ligorio is a doctoral lecturer in the Department of Computer Science.', 'Research Areas. Machine Learning; Spoken Dialogue Systems; Constraint ...', "Tiziana Ligorio's 13 research works with 50 citations, including: Naturalistic Dialogue Management for Noisy Speech Recognition.", 'Teaching history. SPRING 2026. CSCI 395.32. – AI Agents – Instructor & Course Coordinator. CSCI 127. – Introduction to Computer Science (Large Lecture)', 'A couple reflections on the quantum computing breakthrough we just announced... Most of us grew up learning there are three main types of matter that matter: ...', 'The primary focus of this course is the design and analysis of algorithms and abstract data types. To this end it introduces elementary data structures.', 'Teaching Faculty. Photo of Tiziana Ligorio. Tiziana Ligorio. Doctoral Lecturer. Research Areas: Machine Learning, Spoken Dialogue Systems, Constraint ...', 'Tiziana Ligorio, a Hunter computer science doctoral lecturer who together with Epstein teaches a deep machine learning class at the City ...']}}, {'type': 'observation', 'tool': 'extract_year_timeline', 'chars': 215}, {'type': 'action', 'tool': 'Search', 'args': {'__arg1': 'Tiziana Ligorio research publications machine learning dialogue systems'}}, {'type': 'observation', 'tool': 'Search', 'chars': 1596}, {'type': 'action', 'tool': 'extract_year_timeline', 'args': {'results': ['My doctoral research was on feature selection for spoken dialogue systems.', "Tiziana Ligorio's 13 research works with 50 citations, including: Naturalistic Dialogue Management for Noisy Speech Recognition.", 'Machine learning has been applied to such corpora with various goals, for example, to learn dialogue acts from human-human corpora [8], to learn error-handling ...', 'This paper presents a novel approach to learning for spoken dialogue systems. It describes embedded wizardry, a meth- odology for learning from skilled people, ...', 'Feature selection for error detection and recovery in spoken dialogue systems · Susan L. Epstein, T. Brown, Tiziana Ligorio · Published 2011 · Computer Science.', 'Tiziana Ligorio, Susan L. Epstein and Rebecca J. Passonneau. 2010. Wizards ... This paper concerns the discourse understanding process in spoken dialogue systems.']}}, {'type': 'observation', 'tool': 'extract_year_timeline', 'chars': 408}, {'type': 'action', 'tool': 'Search', 'args': {'__arg1': 'Tiziana Ligorio teaching courses computer science 2023 2024'}}, {'type': 'observation', 'tool': 'Search', 'chars': 1366}, {'type': 'action', 'tool': 'extract_year_timeline', 'args': {'results': ['Teaching history. SPRING 2026. CSCI 395.32. – AI Agents – Instructor & Course Coordinator. CSCI 127. – Introduction to Computer Science (Large Lecture)', 'Read all 98 reviews of Tiziana Ligorio. Department. CSCI. Recent Semesters Teaching. Spring 2026, Spring 2025, Spring 2024, Fall 2023, Spring 2023. Typical ...', 'Tiziana Ligorio. Computer Science PhD. TENTATIVE SCHEDULE for CSCI 235 Spring 2024: This schedule is ...', 'Tiziana Ligorio, a Hunter computer science doctoral lecturer who together with Epstein teaches a deep machine learning class at the City ...']}}, {'type': 'observation', 'tool': 'extract_year_timeline', 'chars': 1029}, {'type': 'action', 'tool': 'Search', 'args': {'__arg1': 'Tiziana Ligorio research interests teaching focus 2023 2024'}}, {'type': 'observation', 'tool': 'Search', 'chars': 1344}, {'type': 'action', 'tool': 'extract_year_timeline', 'args': {'results': ['Tiziana Ligorio. Doctoral Lecturer. Research Areas: Machine Learning, Spoken ... Professor Emeritus. Research Areas: Graph Theory, Algorithms, Combinatorics ...', 'Tiziana Ligorio, a Hunter computer science doctoral lecturer who together with Epstein teaches a deep machine learning class at the City ...']}}, {'type': 'observation', 'tool': 'extract_year_timeline', 'chars': 46}], 'final_answer': 'Tiziana Ligorio is a prominent figure in the field of computer science, particularly known for her work in machine learning and spoken dialogue systems. Her academic journey began with her doctoral research, which focused on feature selection for spoken dialogue systems. This area of study is crucial as it addresses the challenges of natural language processing and the interaction between humans and machines.\n\n### Early Academic Career\nDuring her PhD years, Ligorio\'s research primarily revolved around improving the efficiency and effectiveness of spoken dialogue systems. Notable publications from this period include her work on "Naturalistic Dialogue Management for Noisy Speech Recognition" and studies on error detection and recovery in dialogue systems, co-authored with Susan L. Epstein. These contributions laid the groundwork for her future research and teaching.\n\n### Research Focus\nOver the years, Ligorio\'s research interests have evolved but have remained anchored in machine learning and dialogue systems. Her work has included methodologies for learning from skilled individuals in dialogue contexts, which is essential for developing more intuitive and responsive AI systems. By 2011, she had published several papers that explored various aspects of dialogue management and error handling, indicating a strong focus on practical applications of her research.\n\n### Teaching and Recent Interests\nIn recent years, particularly from 2023 to 2026, Ligorio has been involved in teaching courses such as "AI Agents" and "Introduction to Computer Science." This shift in her teaching focus suggests a broader engagement with artificial intelligence and foundational computer science principles. The inclusion of AI in her curriculum indicates a response to the growing importance of AI technologies in various fields, reflecting current trends in computer science education.\n\nHer teaching also emphasizes algorithm design and analysis, which are critical skills for students entering the tech industry. This focus on foundational concepts, combined with her expertise in machine learning, positions her as a key educator in preparing students for the challenges of modern computing.\n\n### Conclusion\nTiziana Ligorio\'s academic career showcases a trajectory from specialized research in spoken dialogue systems to a broader engagement with artificial intelligence and foundational computer science education. Her evolving interests reflect the dynamic nature of the field and the importance of adapting educational content to meet contemporary technological advancements. As she continues to teach and conduct research, her contributions will likely influence both her students and the wider field of computer science.'}

--------------------------------------------------------------------

Part 2: Observability and Evaluation

We will continue exploring the rest of this notebook after our lecture on evals.

LangSmith is LangChain’s observability and evaluation platform for agent-based systems. It records everything that happens during an agent run (LLM calls, tool calls, intermediate states, timing, and token usage) so you can inspect how an agent behaved, not just what it returned. It can be used for debugging agents, comparing different prompts or tools, evaluating behavior across datasets, and understanding failure modes in complex, multi-step workflows.

LangSmith works by attaching tracing callbacks to the runtime and capturing execution events as they occur. Because Google Colab replaces Python’s standard output system, this tracing mechanism is unstable in Colab notebooks and can cause runtime errors even when the agent itself is correct. For this reason, LangSmith tracing is not supported reliably in Colab. To use LangSmith, you should run the local version of this notebook from the course GitHub repository (for example using a local virtual environment and Jupyter or VS Code), where tracing works correctly and you can inspect full agent traces in the LangSmith UI.

Structured Tracing with LangSmith

In Part 1, we built a simple build_agent_trace() function to inspect agent behavior.

In this section, we’ll enable LangSmith tracing and re-run the agent to inspect with LangSmith.

Get a LangSmith API Key

  1. Go to https://smith.langchain.com

  2. Sign in with your GitHub, Google, or email account (or create a new account)

  3. Once logged in, click on your profile icon in the bottom left corner

  4. Select Settings

  5. Navigate to API Keys

  6. Click + API Key

  7. Give the key a name, e.g. langchain-react_agent_demo

  8. Copy the key immediately (you won’t be able to see it again)

Important: Treat this key like a password. Do not share it, paste it into notebooks, or commit it to GitHub.

Add the LangSmith API Key

In Colab:

  1. On the left sidebar, click 🔑 Secrets

  2. Add a new secret:

  • Name: LANGSMITH_API_KEY

  • Value: your actual API key

  1. Toggle the switch to the left to give notebook access (you should see a checkmark)

If running locally — Add to .env

Add the following to your .env file (replace with your own key):

LANGSMITH_API_KEY=your_langsmith_key_here

Important: Never paste API keys into code cells.

Enable LangSmith Tracing

Setting these environment variables tells LangChain to automatically send trace data to LangSmith. The LANGCHAIN_PROJECT groups your runs together for easier organization.

# LangSmith tracing environment variables were configured at startup (at the top of this notebook),
# right after load_dotenv(), to avoid kernel I/O conflicts mid-session.

Re-run the Agent with Tracing

Now when we invoke the agent, LangSmith will automatically capture the full execution trace. After running this cell, go to https://smith.langchain.com and select the react-agent-demo project to see the trace.

agent = create_agent(
    model=model,
    tools=tools,
    system_prompt = (
    "You are a research assistant.\n"
    "For research questions, do not rely on a single source.\n"
    "Use multiple search or information-gathering tool calls to cover "
    "different time periods, perspectives, or subtopics before answering.\n"
    "Only provide a final answer after you have gathered information "
    "from multiple sources."
    )
)
# Run the agent - this will be traced in LangSmith
answer = await agent.ainvoke({
    "messages": [
        {"role": "user", "content": "Who is Tiziana Ligorio?"}
    ]
})

# Display the final answer
print(answer["messages"][-1].content)
Tiziana Ligorio is a professional in the field of computer science, currently serving as a doctoral lecturer at Hunter College, part of The City University of New York. She holds a PhD from The Graduate Center of the City University of New York. Her academic work includes teaching courses related to computer science and deep machine learning.

In addition to her academic role, Ligorio has been involved in various events and initiatives. For instance, she attended the opening of the "After Nature" exhibition at the New Museum in New York City in July 2008. She has also participated as a mentor in the CUNY Recovery Corps and was a keynote speaker at the Arrow NYC event in 2019.

Her research interests include topics such as naturalistic dialogue management and noisy speech recognition, contributing to the field with several publications and citations.

Overall, Tiziana Ligorio is recognized for her contributions to computer science education and research, particularly in the context of machine learning and dialogue systems.
import os
print(os.getenv("LANGSMITH_ENDPOINT"))
https://api.smith.langchain.com

Simple Evaluation

Beyond tracing individual runs, LangSmith allows you to evaluate your agent systematically against a dataset. For demonstration purposes, we’ll create a small test set and run a simple evaluator that checks whether the agent’s response contains expected facts.

This pattern is useful for:

  • Regression testing (i.e. re-running known test cases to ensure that previous correct behaviors have not degraded due to the change) after prompt or model changes

  • Comparing different models or configurations

  • Extensive testing before deploying agents to production

from langsmith import Client
from langsmith.evaluation import evaluate

client = Client()

# evaluate() requires a LangSmith dataset — it cannot accept plain Python dicts.
# We create a named dataset via the API and upload our test cases to it.
dataset_name = "react-agent-test-set"

# Delete and recreate so re-running the notebook doesn't accumulate duplicate examples
if client.has_dataset(dataset_name=dataset_name):
    client.delete_dataset(dataset_name=dataset_name)

dataset = client.create_dataset(dataset_name=dataset_name)
client.create_examples(
    inputs=[
        {"input": "Who is Tiziana Ligorio?"},
        {"input": "What is reinforcement learning?"},
    ],
    outputs=[
        {"expected_facts": ["Hunter College", "computer science", "PhD"]},
        {"expected_facts": ["reward", "agent", "learning"]},
    ],
    dataset_id=dataset.id,
)
{'example_ids': ['15f2265d-7550-4184-8c8e-a84b62b98821', '9e232c9c-6789-4f2b-89f0-fe5952823595'], 'count': 2}
# Simple evaluator: checks what fraction of expected facts appear in the answer
def fact_coverage_evaluator(run, example):
    """
    Evaluates whether the agent's response contains the expected facts.
    Returns a score between 0 and 1 representing the fraction of facts found.
    """
    final_message = run.outputs["messages"][-1]
    answer = final_message.content.lower() if hasattr(final_message, "content") else str(final_message).lower()

    # expected_facts are stored in example.outputs (not example.inputs)
    expected = example.outputs["expected_facts"]
    found = sum(1 for fact in expected if fact.lower() in answer)

    return {
        "key": "fact_coverage",
        "score": found / len(expected),
        "comment": f"Found {found}/{len(expected)} expected facts",
    }
# Wrapper so evaluate() can call our agent
def run_agent(inputs):
    return agent.invoke({
        "messages": [{"role": "user", "content": inputs["input"]}]
    })

Run the Evaluation

The evaluate function will:

  1. Run the agent on each example in our test set

  2. Apply our custom evaluator to each result

  3. Record everything in LangSmith for analysis

After running, visit LangSmith to see the evaluation results, including scores and traces for each test case.

# Run the evaluation
results = evaluate(
    run_agent,
    data=dataset_name,
    evaluators=[fact_coverage_evaluator],
    experiment_prefix="react-agent-eval",
)

print("Evaluation complete! Check LangSmith for detailed results.")
View the evaluation results for experiment: 'react-agent-eval-50a4cda7' at:
https://smith.langchain.com/o/4faa9f56-b77c-482c-bd99-d16f4a864b7e/datasets/17a5b126-5fc7-4f9d-9339-9f2a535a45dd/compare?selectedSessions=15e21a44-2a6c-4e96-b872-252b9636a3ab


Loading...
Evaluation complete! Check LangSmith for detailed results.

Summary

In Part 1, we built a ReAct agent using the LangChain framework:

  1. Framework setup: By calling create_agent with a model, a list of tools, and a short system prompt, we got a fully functional ReAct agent — the reasoning loop, tool routing, and termination logic are all handled by the framework

  2. Built-in tools: We loaded LangChain’s built-in wrappers for web search (SerpAPI) and Wikipedia with a single load_tools call, and connected an MCP server to add a fetch tool for retrieving full page content.

  3. Custom tools: We defined a custom extract_year_timeline tool using the tool decorator. LangChain automatically exposes its inputs and outputs to the agent based on the type hints and docstring.

  4. Inspecting agent behavior: We built a build_agent_trace() helper to parse the agent’s raw message output into a readable step-by-step trace of user input, actions, and observations.

These techniques illustrate:

  • Quickly prototyping agents without implementing the reasoning loop from scratch (less coding but also less control)

  • Extending agents with tools: built-in, MCP-based, or fully custom

  • Inspecting agent decisions to debug unexpected behavior before adding full observability

In Part 2, we explored structured observability with LangSmith:

  1. Tracing: By setting a few environment variables, we enabled automatic capture of all LLM calls, tool invocations, latencies, and token usage, without modifying our agent code.

  2. Evaluation: We created a simple test dataset with expected facts and wrote a custom evaluator to measure how well our agent’s responses covered those facts.

These techniques are useful for:

  • Use tracing to extensively debug unexpected agent behavior

  • Build regression test suites to catch issues before deployment

  • Compare different models, prompts, or tool configurations systematically