A langgraph support bot memory implementation needs more than a prompt with chat history—it needs durable state across turns. This tutorial builds a multi-turn customer support agent in LangGraph that persists conversation state per thread, so the bot recalls prior messages without you manually re-sending the transcript on every call.
Prerequisites
- Python 3.10 or newer
langgraphandlangchain-openaiinstalled- An OpenAI-compatible API key (or any endpoint you can call)
pip install langgraph langchain-openai
Export your credential:
export OPENAI_API_KEY="sk-..."
If you later point at a gateway, you’ll swap the base URL and key, not the graph code.
The core idea behind langgraph support bot memory
LangGraph separates execution from state. The graph node runs the model; a checkpointer stores the message list keyed by thread_id. On each invocation you pass the same thread_id, and LangGraph loads prior messages automatically. You never concatenate history by hand or stuff it into the system prompt.
This matters for support bots because users ask follow-ups (“How long do I have?”) that are incoherent without the prior turn. Checkpointers make that context a deployment detail, not a prompt-engineering hack.
Define the state and node
LangGraph ships MessagesState, a typed dict with a messages field. We’ll inject a system prompt at call time so it isn’t duplicated in the stored transcript.
from langgraph.graph import StateGraph, MessagesState, START, END
from langchain_openai import ChatOpenAI
SYSTEM = "You are a concise customer support agent for Acme Corp. Use prior context in the thread."
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
def chatbot(state: MessagesState):
msgs = state["messages"]
if not msgs or msgs[0].type != "system":
msgs = [("system", SYSTEM)] + list(msgs)
response = model.invoke(msgs)
return {"messages": [response]}
The node returns only the new AI message; LangGraph appends it to the existing list.
Compile with a checkpointer
Memory requires a checkpointer. MemorySaver keeps state in process memory; swap it for SqliteSaver or PostgresSaver in production.
from langgraph.checkpoint.memory import MemorySaver
builder = StateGraph(MessagesState)
builder.add_node("chatbot", chatbot)
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)
memory = MemorySaver()
app = builder.compile(checkpointer=memory)
Run a multi-turn conversation
Create a thread config. Each user message is invoked with the same config.
config = {"configurable": {"thread_id": "user-42"}}
turn1 = app.invoke(
{"messages": [("user", "What is your return policy?")]},
config
)
print(turn1["messages"][-1].content)
Expected output (abridged):
Our return policy allows returns within 30 days of purchase with a receipt.
Now ask a follow-up that relies on context:
turn2 = app.invoke(
{"messages": [("user", "Do I need the original packaging?")]},
config
)
print(turn2["messages"][-1].content)
Because the checkpointer loaded the prior user question and the bot’s answer, the model responds coherently:
If the item is unopened, original packaging helps but is not required within 30 days.
Inspect stored memory
Debugging a langgraph support bot memory issue starts with dumping the state.
state = app.get_state(config)
for m in state.values["messages"]:
print(m.type, ":", m.content[:60])
Output:
system : You are a concise customer support agent for Acme Corp...
user : What is your return policy?
ai : Our return policy allows returns within 30 days...
user : Do I need the original packaging?
ai : If the item is unopened, original packaging helps...
Adding a retrieval tool without losing memory
Support bots need facts. Wrap a function as a tool; LangGraph stores tool calls and observations in the same message list, so memory stays consistent.
from langchain_core.tools import tool
from langgraph.prebuilt import ToolNode, tools_condition
@tool
def lookup_policy(query: str) -> str:
"""Lookup Acme policy docs."""
return "Returns accepted within 30 days, original packaging preferred."
tool_node = ToolNode([lookup_policy])
def chatbot(state: MessagesState):
msgs = state["messages"]
if not msgs or msgs[0].type != "system":
msgs = [("system", SYSTEM)] + list(msgs)
model_with_tools = model.bind_tools([lookup_policy])
return {"messages": [model_with_tools.invoke(msgs)]}
builder = StateGraph(MessagesState)
builder.add_node("chatbot", chatbot)
builder.add_node("tools", tool_node)
builder.add_edge(START, "chatbot")
builder.add_conditional_edges("chatbot", tools_condition)
builder.add_edge("tools", "chatbot")
builder.add_edge("chatbot", END)
app = builder.compile(checkpointer=memory)
Run with the same config. The tool result is appended to the thread state, visible in get_state. This proves the langgraph support bot memory approach scales to agentic loops.
Expected interaction when asking “What’s the return window?”:
AI: I'll check the policy. (tool_call)
Tools: Returns accepted within 30 days, original packaging preferred.
AI: You have 30 days to return items.
Clearing or forking threads
To reset a conversation, change thread_id or delete the checkpoint. With MemorySaver you can simply use a new ID. For SQLite:
from langgraph.checkpoint.sqlite import SqliteSaver
saver = SqliteSaver.from_conn_string("support.db")
config = {"configurable": {"thread_id": "user-43"}}
Forking lets you branch a conversation: copy state to a new thread and continue without mutating the original.
Pointing at an inference gateway
If you don’t want to juggle provider keys, point ChatOpenAI at an OpenAI-compatible gateway. n4n.ai exposes one endpoint that fronts 240+ models and automatically falls back when a provider is rate-limited, while honoring your routing directives per call.
model = ChatOpenAI(
base_url="https://api.n4n.ai/v1",
api_key="your-n4n-key",
model="anthropic/claude-3.5-sonnet"
)
The rest of the graph code stays identical. Per-token metering shows up in your gateway dashboard instead of scattered provider bills.
Production considerations
- Truncation:
MemorySavergrows unbounded. Use a custom reducer or prune messages older than N turns before they hit the model. - Concurrency:
SqliteSaveris fine for single-instance; usePostgresSaverwith row locking for multi-worker deployments. - Secrets: System prompts and API keys belong in env vars, not in code.
- Streaming:
app.stream()yields token chunks; wire it to your chat UI with the same config object.
The langgraph support bot memory pattern is fundamentally about letting the framework own state persistence. Once that clicks, multi-turn support stops being a prompt-engineering chore and becomes a configuration choice.