How to Build Custom AI Agents Without Losing Your Mind
Last quarter, I needed a system to automatically triage support tickets based on urgency and topic, then draft a preliminary response. Off-the-shelf solutions like Lindy agent platform or Bardeen.ai were too rigid; they couldn’t handle our specific internal knowledge base or the nuanced classification rules. We needed something custom.
This isn’t about watching Twitter threads. This is about shipping. If you’re actually deploying agents, you know the debugging pain of agents that silently fail, the cost overruns from agents that loop, and the compliance headaches from agents that touch real money or real user data. Building custom AI agents isn’t a magic bullet; it’s an engineering task, and it comes with its own set of challenges.
Why Custom Agents? The Hard Truth
Pre-built agent platforms are fantastic for simple, well-defined tasks. If you need to summarize an email and add it to a CRM, a tool like Bardeen or even a custom Zapier as a glue layer AI action can get you there fast. But real-world complexity demands more. These platforms often fall short when you need:
- Deep integration with proprietary internal APIs or databases.
- Complex, multi-step decision flows with conditional logic.
- Fine-grained control over LLM prompts, tool usage, and state management.
- Robust observability and debugging capabilities for production environments.
- Specific compliance or security requirements for handling sensitive data.
For our ticket triaging agent, I settled on LangGraph. Its state machine approach makes debugging predictable, which is a godsend when you’re dealing with real user data and potential compliance issues. Other frameworks like CrewAI or AutoGen offer different paradigms, but LangGraph’s explicit graph structure appealed to my engineering brain.
The Build: A Step-by-Step Agent Tutorial with LangGraph
Let’s walk through the core components of building a custom agent using LangGraph, focusing on our ticket triaging example.
1. Defining the Workflow and State
First, you map out the agent’s journey. For our ticket triager, it looked something like this:
- Fetch Ticket Data: An initial step to pull the raw ticket content from our support system’s API.
- Classify Ticket: An LLM call to determine urgency (low, medium, high) and topic (billing, technical, feature request).
- Search Knowledge Base: A tool call to our internal vector database, using the classified topic to find relevant articles or FAQs.
- Draft Response: Another LLM call, taking the original ticket, classification, and KB results to draft a preliminary reply.
- Human Review/Escalation: A conditional step. If the urgency is ‘high’ or the LLM flags uncertainty, it routes to a human for review; otherwise, it queues the draft for sending.
LangGraph manages this flow through a shared state. You define a `State` object (often a Pydantic model) that gets passed between nodes. Each node receives the current state, performs an action, and returns updates to the state.
from typing import TypedDict, List, Optional
from langchain_core.messages import BaseMessage
class AgentState(TypedDict):
ticket_id: str
raw_ticket_content: str
classification: Optional[dict]
kb_results: Optional[List[str]]
draft_response: Optional[str]
messages: List[BaseMessage] # For conversational agents
2. Implementing Nodes and Tools
Each step in your workflow becomes a ‘node’ in LangGraph. Nodes are just Python functions that take the current state and return updates. Tools are external functions your LLM can call.
For example, our `classify_ticket` node might look like this:
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
def classify_ticket_node(state: AgentState) -> AgentState:
llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant that classifies support tickets."),
("human", "Classify the following ticket by urgency (low, medium, high) and topic (billing, technical, feature request). Return a JSON object. Ticket: {ticket_content}")
])
chain = prompt | llm | (lambda x: json.loads(x.content))
classification = chain.invoke({"ticket_content": state["raw_ticket_content"]})
return {"classification": classification}
Integrating tools is crucial. I built a tool that queries our internal CRM, and the agent now pulls up customer history automatically before drafting a response. That’s a huge time saver. You wrap your API calls or database queries into LangChain tools, then pass them to your LLM. The LLM decides when and how to call them based on its prompt.
3. Building the Graph and Edges
With nodes and tools defined, you assemble them into a graph. LangGraph uses a `StateGraph` object. You add nodes and define ‘edges’ (transitions) between them. Edges can be conditional, allowing your agent to make decisions.
from langgraph.graph import StateGraph, END
workflow = StateGraph(AgentState)
workflow.add_node("fetch_data", fetch_ticket_data_node)
workflow.add_node("classify", classify_ticket_node)
workflow.add_node("search_kb", search_knowledge_base_node)
workflow.add_node("draft_response", draft_response_node)
workflow.set_entry_point("fetch_data")
workflow.add_edge("fetch_data", "classify")
workflow.add_edge("classify", "search_kb")
workflow.add_edge("search_kb", "draft_response")
# Conditional edge for human review
workflow.add_conditional_edges(
"draft_response",
lambda state: "human_review" if state["classification"].get("urgency") == "high" else END,
{"human_review": "human_review_node"} # Assuming you have a human_review_node
)
app = workflow.compile()
4. Observability is Non-Negotiable
This is where most agent projects silently fail. You must have observability. Agents are non-deterministic; they don’t always follow the same path or produce the same output. Without a way to see what’s happening inside, you’re debugging blind.
I’ve used LangSmith extensively, and its trace view is the only thing that saved me from weeks of head-scratching when an agent started looping or hallucinating tool arguments. It shows you every LLM call, every tool invocation, and the state changes at each step. Langfuse offers similar capabilities, and both are essential for understanding why your agent did what it did.
My concrete gripe: LangSmith’s pricing can feel steep for smaller teams, especially if you’re just experimenting. $50/month for the basic plan is fair if you’re shipping, but the free tier is a joke for anything beyond a toy agent. You’ll hit its limits fast.