Tutorials7 min read

Debugging Production Agents: How to Build AI Agents That Don't Break the Bank

Dan Hartman headshotDan Hartman— Editor··Updated ·7 min read

Learn how to build AI agents that are reliable and cost-effective. This agent tutorial covers LangGraph for structured development and LangSmith for essential debugging in production.

Last quarter, I shipped an agent designed to triage inbound support tickets. It was supposed to read a customer’s email, classify the issue, and route it to the right team. Simple enough, right? For the first few days, it worked. Then, a customer wrote in about a billing discrepancy. The agent, instead of escalating, got stuck in a loop, trying to ‘resolve’ the issue by repeatedly asking for account details it already had. It burned through $300 in tokens in an hour, then finally spat out a generic ‘we’re looking into it’ email. The customer was furious. I was furious. This wasn’t just a silent failure; it was an expensive, reputation-damaging one. That’s the reality of how to build AI agents for production: the happy path is easy, the failure modes are brutal.

The promise of agents is seductive. You hear about them ‘reasoning’ and ‘planning,’ but in practice, they’re brittle state machines with a language model at the core. They fail for predictable reasons: bad tool definitions, unclear instructions, unexpected user input, or just plain old LLM hallucinations. Debugging these issues isn’t like debugging traditional code. You can’t just set a breakpoint and inspect variables. The ‘state’ is often implicit in the conversation history, and the ‘logic’ is a black box. This opacity makes iterating on agent behavior a frustrating exercise in guesswork.

Frameworks vs. Platforms: Picking Your Battles

When you’re trying to figure out how to build agents that actually work, you quickly run into two camps: frameworks and platforms. Frameworks like LangGraph, CrewAI, or AutoGen give you the primitives to compose LLM calls, tool use, and state transitions. They’re code-first, offering maximum control over the agent’s internal workings. Platforms like Lindy agent platform or Bardeen.ai offer a more visual, low-code approach, often with pre-built integrations and a simpler setup. They’re great for quick prototypes or simpler tasks, but I’ve found they hit a ceiling fast when you need custom logic, complex error handling, or specific governance requirements. For anything serious, anything that touches real money or real user data, you’re probably going to be writing code.

My experience tells me that for production-grade agents, you need the explicit control a framework provides. You need to define exactly what happens at each step, what tools are available, and how the agent reacts to different outcomes. This isn’t about making things harder; it’s about making them predictable. Predictability is your best friend when an agent starts misbehaving at 3 AM.

How to Build AI Agents with LangGraph: A Structured Approach

After that support agent debacle, I decided to rebuild using LangGraph. It’s an extension of LangChain, and it forces you to think about your agent as a state machine, which is exactly what you need for reliability. You define nodes (LLM calls, tool calls, human interventions) and edges (transitions between nodes based on output). This explicit graph structure makes it much easier to visualize and, crucially, debug the agent’s flow. It’s a massive improvement over trying to guess why a chain of prompts went sideways.

Here’s a simplified example of how you might structure a basic agent that can call a tool:

from langgraph.graph import StateGraph, END
from langchain_core.messages import HumanMessage, ToolMessage
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

# Define a simple tool
@tool
def search_web(query: str) -> str:
    """Searches the web for a given query and returns results."""
    # In a real app, this would hit a search API like Google Search or Brave Search
    if "weather" in query.lower():
        return "It's sunny and 75 degrees in San Francisco right now."
    if "capital of france" in query.lower():
        return "The capital of France is Paris."
    return f"Search results for '{query}': [Some relevant info from a mock search]"

# Define the agent state
class AgentState:
    messages: list

# Define the nodes
def call_llm(state: AgentState):
    llm = ChatOpenAI(model="gpt-4o", temperature=0)
    response = llm.invoke(state.messages)
    return {"messages": [response]}

def call_tool(state: AgentState):
    last_message = state.messages[-1]
    tool_calls = last_message.tool_calls
    tool_outputs = []
    for tool_call in tool_calls:
        tool_name = tool_call["name"]
        tool_args = tool_call["args"]
        if tool_name == "search_web":
            output = search_web.invoke(tool_args["query"])
            tool_outputs.append(ToolMessage(content=output, tool_call_id=tool_call["id"])) # Pass tool_call_id
        else:
            tool_outputs.append(ToolMessage(content=f"Unknown tool: {tool_name}", tool_call_id=tool_call["id"])) # Pass tool_call_id
    return {"messages": tool_outputs}

# Define the graph
workflow = StateGraph(AgentState)
workflow.add_node("llm", call_llm)
workflow.add_node("tool_node", call_tool)

workflow.set_entry_point("llm")

# Define conditional edges
def should_continue(state: AgentState):
    last_message = state.messages[-1]
    if last_message.tool_calls:
        return "tool_node"
    return "end"

workflow.add_conditional_edges(
    "llm",
    should_continue,
    {"tool_node": "tool_node", "end": END}
)
workflow.add_edge("tool_node", "llm") # After tool, go back to LLM for synthesis

app = workflow.compile()

# Example usage
# print(app.invoke({"messages": [HumanMessage(content="What's the weather like in San Francisco?")]}))
# print(app.invoke({"messages": [HumanMessage(content="What is the capital of France?")]}))
# print(app.invoke({"messages": [HumanMessage(content="Tell me a joke.")]}))

This explicit structure, where you define each step and its transitions, is a lifesaver. It makes the agent’s behavior predictable, which is a huge win for debugging. You can trace the exact path it took, see which node it entered, and what output it generated. This level of control is what separates a prototype from a production system.

What Breaks at Scale, and How to See It

Even with LangGraph, things still break. Agents can still hallucinate tool arguments, or a tool might return an unexpected error. The real challenge isn’t just building the agent; it’s observing it in production. This is where tools like LangSmith or Langfuse become non-negotiable. They give you visibility into every LLM call, every tool invocation, and the full trace of your agent’s execution. Without them, you’re flying blind.

I’ve seen teams try to parse raw LLM logs, and honestly, it’s a nightmare. LangSmith’s trace view, showing the exact sequence of events and inputs/outputs for each step, is the only way I’d deploy an agent that touches real users. It lets you replay an agent’s entire thought process, pinpointing exactly where it went off the rails. This is critical for understanding why an agent looped, or why it gave a nonsensical answer. The cost of these monitoring tools isn’t trivial. LangSmith’s pricing starts at $50/month for basic usage, scaling up with traces. For a small team, that $50/month is fair, considering the headaches it saves. But if you’re running thousands of agent interactions daily, those costs can add up quickly. You need to factor that into your budget from day one.

My biggest gripe with many of these frameworks, including LangGraph, is the documentation. It’s often fragmented, assumes a lot of prior knowledge, and examples don’t always cover the edge cases you hit in production — and good luck finding comprehensive examples for this. You spend too much time digging through GitHub issues or Discord channels to figure out how to handle a specific tool error or a complex conditional transition. It’s a constant battle, and it slows down development significantly.

Governance and Compliance: The Unsexy But Essential Part

Beyond technical failures, there’s the compliance angle. If your agent handles any user data, especially PII or financial information, you need audit trails. Who accessed what? What decisions did the agent make? Why? LangSmith helps here too, by logging every interaction. But you also need to think about data retention policies and how you’re securing those logs. It’s not just about preventing loops; it’s about preventing data breaches and regulatory fines. This is often overlooked in the rush to ‘ship an agent,’ but it’s a hard requirement for any serious deployment. You can’t just hope for the best; you need a plan for the worst.

Honestly, if you’re building anything beyond a toy agent, you need to treat it like a distributed system. Expect failures. Plan for them. Don’t assume the LLM will always do the ‘right’ thing. It won’t.

For quick iteration and testing, especially when you’re just figuring out how to build AI agents, I’ve found Replit Agent to be surprisingly useful. You can spin up an environment, drop in your LangGraph code, and test it against different inputs without much setup. It’s a decent sandbox for development, and they’ve got some interesting agent features themselves.

If you want the deep cut on this, AI meeting tools coverage.

So, how do you build AI agents that don’t silently fail or burn through your budget? You start with a structured framework like LangGraph. You embrace explicit state management. You invest in robust observability tools like LangSmith from the beginning. And you accept that agents are not magic; they’re complex software that requires diligent engineering. The free tier of LangChain and LangGraph is enough to get started, but you’ll need to pay for monitoring and higher-tier LLMs once you move past prototypes. Don’t skimp on those. The cost of a production failure far outweighs the subscription fees.

— The Colophon

One AI tool. Tested. Reviewed.
In your inbox every Sunday.

~3 minute read. Real outcomes from operators, not marketers.