By the end of this tutorial, you will have built a fully functional AI customer service agent capable of handling complex multi-turn conversations while maintaining context across the entire dialogue. Whether you are a Python developer just starting your journey with large language models or an enterprise architect looking to implement intelligent automation, this guide will walk you through every concept and code block with clear explanations.
What is LangGraph and Why Does It Matter for Customer Service?
LangGraph is a powerful library built on top of LangChain that enables developers to create structured agent workflows with cycles and state management. Unlike simple sequential chains, LangGraph allows your agent to make decisions, loop back to gather more information, and maintain conversation context across multiple turns. This is essential for customer service scenarios where users might provide incomplete information that needs to be gathered step by step.
I first encountered LangGraph when building a support system for a client who needed their AI to ask clarifying questions before creating support tickets. The traditional approach of pre-defined conversation flows felt too rigid, and I needed something that could adapt dynamically while maintaining state. LangGraph solved this elegantly, and I will share everything I learned along the way.
Understanding Multi-turn State Management
Before we write any code, let us understand what we mean by "state" in the context of customer service conversations. Imagine a user saying "I want to return my order" but not specifying which order. A good customer service agent needs to remember this intent while asking for the missing details, then store the complete information for final processing.
In LangGraph, state is managed through a shared dictionary that gets passed between nodes in your conversation graph. Each node can read from and write to this state, allowing information to accumulate as the conversation progresses. This is fundamentally different from stateless API calls where each message is processed in isolation.
Setting Up Your Environment
You will need Python 3.9 or later installed on your system. Start by creating a new project directory and setting up a virtual environment. For this tutorial, we will use HolySheep AI as our LLM provider because they offer exceptional pricing—DeepSeek V3.2 at just $0.42 per million tokens compared to GPT-4.1 at $8—and their API provides sub-50ms latency that makes real-time conversations feel natural and responsive.
# Create and activate virtual environment
python -m venv customer-service-env
source customer-service-env/bin/activate # On Windows: customer-service-env\Scripts\activate
Install required packages
pip install langgraph langchain-core langchain-holysheep python-dotenv
Create a .env file in your project root to store your API key securely:
HOLYSHEEP_API_KEY=YOUR_HOLYSHEEP_API_KEY
Replace YOUR_HOLYSHEEP_API_KEY with the key you received after signing up. HolySheep AI supports WeChat and Alipay for payments in addition to international payment methods, making it accessible regardless of your location.
Defining Your Conversation State Schema
The first major decision in building your agent is defining what information your state needs to track. For a customer service agent, you typically want to capture the user's inquiry type, their personal information, any order numbers or references, and the resolution status. Let us define this schema using Python's TypedDict or Pydantic.
from typing import TypedDict, Annotated, Sequence
from langgraph.graph import StateGraph, END
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
import operator
class ConversationState(TypedDict):
"""Defines the structure of our customer service conversation state."""
messages: Annotated[Sequence[BaseMessage], operator.add]
customer_name: str | None
customer_email: str | None
order_number: str | None
inquiry_type: str | None
is_resolved: bool
pending_questions: list[str]
def create_initial_state() -> ConversationState:
"""Factory function to create a fresh state for new conversations."""
return {
"messages": [],
"customer_name": None,
"customer_email": None,
"order_number": None,
"inquiry_type": None,
"is_resolved": False,
"pending_questions": []
}
Building the LLM Integration with HolySheep AI
Now we need to connect our agent to a language model. We will use HolySheep AI's API which provides access to multiple models including DeepSeek V3.2 at $0.42 per million output tokens—a fraction of the cost compared to Claude Sonnet 4.5 at $15 or GPT-4.1 at $8 per million tokens. Their free credits on registration allow you to test everything before committing.
import os
from dotenv import load_dotenv
from langchain_holysheep import HolySheepChatLLM
from langchain_core.messages import HumanMessage, AIMessage
load_dotenv()
Initialize the LLM with HolySheep AI
llm = HolySheepChatLLM(
model="deepseek-v3.2",
holysheep_api_key=os.getenv("HOLYSHEEP_API_KEY"),
base_url="https://api.holysheep.ai/v1",
temperature=0.7,
max_tokens=500
)
def call_llm(state: ConversationState) -> dict:
"""Send conversation history to LLM and get response."""
messages = state["messages"]
# Craft a system prompt for customer service persona
system_message = """You are a helpful customer service representative.
Be friendly, professional, and concise. Ask clarifying questions when needed
to gather complete information. Keep responses under 3 sentences."""
response = llm.invoke([
{"role": "system", "content": system_message},
* [{"role": "user" if isinstance(m, HumanMessage) else "assistant",
"content": m.content} for m in messages]
])
return {"messages": [AIMessage(content=response.content)]}
Creating the Decision Router Node
The router is the brain of your agent—it analyzes the current state and decides what action to take next. This could be answering directly, asking for more information, or escalating to a human agent. The router outputs a simple string that LangGraph uses to determine which edge to follow.
def router_node(state: ConversationState) -> str:
"""
Analyzes conversation state and decides the next action.
Returns: 'collect_info' | 'answer' | 'resolve' | 'escalate'
"""
messages = state["messages"]
pending = state["pending_questions"]
customer_email = state["customer_email"]
order_number = state["order_number"]
# If we have pending questions, ask them first
if pending:
return "collect_info"
# If this is the first message, classify the inquiry
if len(messages) <= 1:
return "classify"
# If we have all required info, try to resolve
if customer_email and order_number:
return "resolve"
# Default to collecting info
return "collect_info"
def classification_node(state: ConversationState) -> dict:
"""Classify the inquiry type based on initial message."""
last_message = state["messages"][-1].content.lower()
inquiry_type = "general"
pending = []
if "return" in last_message or "refund" in last_message:
inquiry_type = "return_request"
pending = ["order_number", "reason_for_return"]
elif "shipping" in last_message or "delivery" in last_message:
inquiry_type = "shipping_inquiry"
pending = ["order_number"]
elif "change" in last_message or "modify" in last_message:
inquiry_type = "order_modification"
pending = ["order_number", "requested_change"]
return {
"inquiry_type": inquiry_type,
"pending_questions": pending
}
Implementing the Information Collection Node
This node asks the user for missing information one piece at a time. Iterative information gathering creates a smooth conversational flow rather than overwhelming the user with multiple questions simultaneously.
def collect_info_node(state: ConversationState) -> dict:
"""Generate response asking for next piece of missing information."""
pending = state["pending_questions"]
inquiry_type = state["inquiry_type"]
# Determine what to ask next
if not pending:
# No specific questions, ask for email as fallback
question = "Could you please provide your email address?"
elif pending[0] == "order_number":
question = f"For your {inquiry_type.replace('_', ' ')}, could you please provide your order number?"
elif pending[0] == "reason_for_return":
question = "What is the reason for your return request?"
elif pending[0] == "requested_change":
question = "What changes would you like to make to your order?"
else:
question = "Could you provide more details about your inquiry?"
return {"messages": [AIMessage(content=question)]}
def process_user_input(state: ConversationState, user_input: str) -> dict:
"""Process user input and update state with new information."""
updates = {"messages": [HumanMessage(content=user_input)]}
# Parse input to extract information
if "@" in user_input and "." in user_input:
updates["customer_email"] = user_input
updates["pending_questions"] = state["pending_questions"][1:]
if any(char.isdigit() for char in user_input) and len(user_input) >= 6:
updates["order_number"] = user_input
updates["pending_questions"] = state["pending_questions"][1:]
if "order_number" in state["pending_questions"] and not state.get("order_number"):
# If we detected an order number
import re
match = re.search(r'\d{6,}', user_input)
if match:
updates["order_number"] = match.group()
current_pending = state["pending_questions"].copy()
current_pending.remove("order_number")
updates["pending_questions"] = current_pending
return updates
Assembling the LangGraph Workflow
Now we bring everything together by defining the graph structure. We create nodes for each major function, connect them with conditional edges that follow our decision logic, and compile the graph into an executable format.
from langgraph.graph import StateGraph
Initialize the graph with our state schema
workflow = StateGraph(ConversationState)
Add all nodes to the graph
workflow.add_node("llm_response", call_llm)
workflow.add_node("router", router_node)
workflow.add_node("classifier", classification_node)
workflow.add_node("collect_info", collect_info_node)
Define entry point
workflow.set_entry_point("router")
Define conditional edges from router
workflow.add_conditional_edges(
"router",
lambda state: state.get("inquiry_type") if state.get("inquiry_type") and len(state["messages"]) <= 2 else router_node(state),
{
"classify": "classifier",
"collect_info": "collect_info",
"answer": "llm_response",
"resolve": "llm_response",
"escalate": "llm_response"
}
)
Connect classifier back to router for re-evaluation
workflow.add_edge("classifier", "router")
workflow.add_edge("collect_info", END)
workflow.add_edge("llm_response", END)
Compile the graph
customer_service_agent = workflow.compile()
print("Agent compiled successfully!")
print("Available models and pricing (2026):")
print("- DeepSeek V3.2: $0.42/MTok (output) — Best value")
print("- Gemini 2.5 Flash: $2.50/MTok — Balanced performance")
print("- Claude Sonnet 4.5: $15/MTok — Premium tier")
print("- GPT-4.1: $8/MTok — High capability")
Running Your Customer Service Agent
Let us test our agent with a realistic customer interaction. The beauty of this approach is that the state persists throughout the conversation, allowing the agent to maintain context and build upon previous exchanges.
# Create a fresh conversation state
initial_state = create_initial_state()
print("=== Starting Customer Service Conversation ===\n")
Simulate a customer interaction
conversation_turns = [
"Hi, I want to return my recent purchase",
"The order number is 847293",
"My email is [email protected]"
]
for user_message in conversation_turns:
print(f"Customer: {user_message}")
# Add user message to state
current_state = customer_service_agent.invoke({
**initial_state,
"messages": initial_state["messages"] + [HumanMessage(content=user_message)]
})
# Display agent response
if current_state["messages"]:
last_message = current_state["messages"][-1]
if isinstance(last_message, AIMessage):
print(f"Agent: {last_message.content}\n")
# Update initial state for next iteration
initial_state = current_state
Display final state summary
print("=== Conversation Summary ===")
print(f"Inquiry Type: {initial_state.get('inquiry_type', 'N/A')}")
print(f"Customer Email: {initial_state.get('customer_email', 'N/A')}")
print(f"Order Number: {initial_state.get('order_number', 'N/A')}")
print(f"Resolved: {initial_state.get('is_resolved', False)}")
Adding Persistence for Production Use
In production, you will want your agent to remember conversations across user sessions. LangGraph supports checkpointing through various storage backends. For a simple implementation, we can use in-memory checkpointing or SQLite for persistence.
from langgraph.checkpoint.memory import MemorySaver
Create a checkpointer for conversation persistence
checkpointer = MemorySaver()
Recompile graph with persistence
production_agent = workflow.compile(checkpointer=checkpointer)
def run_persistent_conversation(thread_id: str):
"""Run a conversation with persistence across multiple sessions."""
config = {"configurable": {"thread_id": thread_id}}
print(f"Starting conversation thread: {thread_id}")
# First session
print("\n--- Session 1 ---")
state = production_agent.invoke(
{"messages": [HumanMessage(content="I need help with my order")]},
config=config
)
print(f"Agent: {state['messages'][-1].content}")
# Simulate session ending...
# Second session (same thread_id maintains state)
print("\n--- Session 2 (Same Thread) ---")
state = production_agent.invoke(
{"messages": [HumanMessage(content="The order number is 123456")]},
config=config
)
print(f"Agent: {state['messages'][-1].content}")
# New conversation thread
print("\n--- Session 3 (New Thread) ---")
new_config = {"configurable": {"thread_id": "different-thread"}}
state = production_agent.invoke(
{"messages": [HumanMessage(content="Hello, I need help")]},
config=new_config
)
print(f"Agent: {state['messages'][-1].content}")
run_persistent_conversation("user-123-session-1")
Advanced Feature: Human-in-the-Loop Escalation
Sometimes automated responses are not enough. LangGraph allows you to interrupt the flow and hand off to a human agent. This is crucial for customer service where complex issues require empathy and nuanced understanding that AI cannot always provide.
from langgraph.graph import Command
def smart_router(state: ConversationState) -> Command:
"""
Enhanced router that can trigger human intervention.
Returns Command to continue or interrupt for human review.
"""
# Check for escalation triggers
last_message = state["messages"][-1].content.lower()
escalation_keywords = ["manager", "supervisor", "speak to human", "not happy",
"lawsuit", "attorney", "extremely dissatisfied"]
for keyword in escalation_keywords:
if keyword in last_message:
return Command(
goto="human_escalation",
update={"messages": [AIMessage(content="I am connecting you with a human agent. Please hold.")]}
)
# Check if issue appears unsolvable after 10+ exchanges
if len(state["messages"]) > 20 and not state["is_resolved"]:
return Command(
goto="human_escalation",
update={"messages": [AIMessage(content="Let me connect you with a specialist who can better assist you.")]}
)
return Command(goto="continue_flow")
Add the escalation node to the graph
workflow.add_node("human_escalation", lambda state: {"escalated": True})
workflow.add_edge("human_escalation", END)
print("Human-in-the-loop escalation configured successfully!")
Monitoring and Analytics Integration
Understanding how your agent performs is essential for continuous improvement. You can integrate logging and analytics to track conversation patterns, resolution rates, and customer satisfaction indicators.
import json
from datetime import datetime
from typing import Optional
class ConversationAnalytics:
"""Track and log conversation metrics."""
def __init__(self):
self.sessions = []
def log_interaction(self, thread_id: str, user_message: str,
agent_response: str, state: dict):
"""Record a single interaction for analysis."""
interaction = {
"timestamp": datetime.now().isoformat(),
"thread_id": thread_id,
"user_message_length": len(user_message),
"agent_response_length": len(agent_response),
"state_snapshot": {
"inquiry_type": state.get("inquiry_type"),
"has_email": bool(state.get("customer_email")),
"has_order": bool(state.get("order_number")),
"message_count": len(state.get("messages", [])),
"is_resolved": state.get("is_resolved", False)
}
}
self.sessions.append(interaction)
def get_summary(self) -> dict:
"""Generate analytics summary."""
if not self.sessions:
return {"error": "No data available"}
total_messages = sum(s["state_snapshot"]["message_count"]
for s in self.sessions)
resolved = sum(1 for s in self.sessions
if s["state_snapshot"]["is_resolved"])
return {
"total_conversations": len(self.sessions),
"total_messages": total_messages,
"resolution_rate": resolved / len(self.sessions) * 100,
"avg_messages_per_conversation": total_messages / len(self.sessions)
}
Initialize analytics
analytics = ConversationAnalytics()
def monitored_llm_response(state: ConversationState, thread_id: str = "default") -> dict:
"""LLM wrapper with analytics logging."""
response = call_llm(state)
# Log the interaction
if state["messages"]:
analytics.log_interaction(
thread_id=thread_id,
user_message=state["messages"][-1].content,
agent_response=response["messages"][-1].content,
state=state
)
return response
Example: Get analytics after running conversations
print("Analytics Summary:", analytics.get_summary())
Best Practices for Production Deployment
When moving from development to production, several considerations ensure your customer service agent performs reliably at scale. Rate limiting protects your infrastructure from abuse, timeout handling prevents hanging conversations, and proper error recovery ensures graceful degradation when issues occur.
HolySheep AI's infrastructure provides 99.9% uptime guarantees and their sub-50ms latency means your agents respond instantly to users. Their ¥1=$1 pricing model translates to approximately 85% savings compared to ¥7.3 rates on other platforms, making large-scale deployments economically viable even for startups.
Common Errors and Fixes
Throughout my experience building LangGraph agents, I have encountered several common pitfalls that can frustrate beginners. Understanding these errors and their solutions will save you hours of debugging time.
Error 1: State Keys Not Defined in TypedDict
Error Message: KeyError: 'some_key' is not defined in state schema
Cause: You are trying to access or update a state key that was not included in your ConversationState TypedDict definition.
# WRONG - Missing keys cause errors
class ConversationState(TypedDict):
messages: Sequence[BaseMessage]
# Missing: customer_email
CORRECT - Define all keys upfront
class ConversationState(TypedDict):
messages: Sequence[BaseMessage]
customer_email: str | None
order_number: str | None
# ... all other keys
FIX: Always initialize all state keys
initial_state = {
"messages": [],
"customer_email": None, # Must be present even if None
"order_number": None,
# ... etc
}
Error 2: Infinite Loops in Conditional Routing
Error Message: RecursionError: maximum recursion depth exceeded
Cause: Your conditional edges create a cycle without proper termination conditions, causing the graph to loop infinitely.
# WRONG - No exit condition creates infinite loop
def bad_router(state):
if not state["is_resolved"]:
return "router" # Routes back to self forever
CORRECT - Ensure every path eventually reaches END
def good_router(state):
if not state["customer_email"]:
return "collect_email"
elif not state["is_resolved"]:
return "process_resolution"
else:
return "end_conversation" # Must have terminal state
Add explicit end node and edge
workflow.add_node("end_conversation", lambda state: {"is_resolved": True})
workflow.add_edge("end_conversation", END)
Error 3: API Authentication Failures with HolySheep
Error Message: AuthenticationError: Invalid API key or unauthorized access
Cause: The API key is not being loaded correctly, or the base_url is pointing to the wrong endpoint.
# WRONG - Hardcoded key or wrong base_url
llm = HolySheepChatLLM(
model="deepseek-v3.2",
holysheep_api_key="sk-wrong-key", # Don't hardcode
base_url="https://api.openai.com/v1" # Wrong endpoint
)
CORRECT - Load from environment with proper base_url
import os
from dotenv import load_dotenv
load_dotenv() # Must be called before accessing os.getenv
llm = HolySheepChatLLM(
model="deepseek-v3.2",
holysheep_api_key=os.getenv("HOLYSHEEP_API_KEY"), # Correct env var
base_url="https://api.holysheep.ai/v1", # HolySheep endpoint
temperature=0.7
)
VERIFY: Print loaded configuration
print(f"API Key loaded: {'Yes' if os.getenv('HOLYSHEEP_API_KEY') else 'No'}")
print(f"Base URL: https://api.holysheep.ai/v1")
Error 4: Message Type Confusion in LangGraph
Error Message: AttributeError: 'str' object has no attribute 'content'
Cause: Mixing plain strings with LangChain message objects in your conversation history.
# WRONG - Mixing strings and message objects
messages = [
"Hello there", # Plain string
HumanMessage(content="I need help"), # Message object
"What's my order status?" # Another string
]
CORRECT - Always use proper message types
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
messages = [
SystemMessage(content="You are a helpful assistant."),
HumanMessage(content="Hello there"), # User input
AIMessage(content="How can I help you today?"), # Agent response
HumanMessage(content="What's my order status?") # More user input
]
Helper function to convert mixed lists
def normalize_messages(raw_messages: list) -> list:
"""Ensure all items are proper LangChain message types."""
normalized = []
for msg in raw_messages:
if isinstance(msg, str):
normalized.append(HumanMessage(content=msg))
elif hasattr(msg, 'content'):
normalized.append(msg)
return normalized
Error 5: Checkpointer Thread ID Mismatch
Error Message: ValueError: No state found for thread_id
Cause: Attempting to resume a conversation with a thread_id that was never created, or using different thread_ids for the same conversation.
# WRONG - Different thread_id each time loses state
for message in conversation:
config = {"configurable": {"thread_id": str(uuid.uuid())}} # New ID each time!
agent.invoke({"messages": [HumanMessage(content=message)]}, config=config)
CORRECT - Consistent thread_id throughout conversation
def run_conversation(messages: list, thread_id: str = "user-session-001"):
config = {"configurable": {"thread_id": thread_id}}
for msg in messages:
state = agent.invoke(
{"messages": [HumanMessage(content=msg)]},
config=config
)
return state
WRONG - Typo in thread_id
config1 = {"configurable": {"thread_id": "conversation-1"}}
config2 = {"configurable": {"thread_id": "conversation-2"}} # Different!
CORRECT - Use same thread_id variable
thread_id = "conversation-1"
config1 = {"configurable": {"thread_id": thread_id}}
config2 = {"configurable": {"thread_id": thread_id}} # Same ID!
Performance Optimization Tips
When deploying at scale, consider implementing response streaming for better user experience, caching frequent responses to reduce API costs, and batching similar requests when analyzing conversation patterns. HolySheep AI's DeepSeek V3.2 at $0.42 per million tokens makes aggressive optimization less critical, but good practices still matter for latency-sensitive applications.
For conversation summarization when threads get long, implement periodic state compression where older messages get condensed into a summary to prevent token bloat. This becomes especially important if you charge per token or have context window limitations.
Conclusion and Next Steps
You now have a complete understanding of building multi-turn customer service agents with LangGraph. The key concepts covered—state management with TypedDict, conditional routing between nodes, persistent checkpoints, and human-in-the-loop escalation—form the foundation for any production-grade conversational AI system.
The agent architecture demonstrated here is modular and extensible. You can add new node types for specialized tasks like order tracking integration, refund processing, or sentiment analysis without restructuring the entire graph.
👉 Sign up for HolySheep AI — free credits on registration
With HolySheep AI's competitive pricing (DeepSeek V3.2 at just $0.42/MTok output compared to $8 for GPT-4.1), sub-50ms latency, and WeChat/Alipay payment support, you have everything needed to build and deploy production customer service solutions affordably.