Every agent we've built so far forgets everything the moment invoke() returns. Ask it a follow-up question and it has no idea what you were just talking about. In this article we fix that with memory, and then add middleware - LangChain's mechanism for enforcing rules around what an agent is allowed to do.
Recap: The Series So Far
- Part 1 - What LangChain is & your first script
- Part 2 - Prompts, models, and LCEL chains
- Part 3 - Tools and your first agent
- Part 4 - RAG: reading your own documents
- Part 5 (this article) - Memory and middleware
- Part 6 - Debugging and observing agents with LangSmith
Why Agents Forget by Default
Each call to agent.invoke(...) runs the agent loop from a blank slate - there's no built-in place for previous messages to live between calls. To fix that, LangChain agents support a checkpointer: a component that saves the conversation's state after every step, keyed by a thread_id you choose.
Step 1: Add a Checkpointer
from dotenv import load_dotenv
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
load_dotenv()
checkpointer = InMemorySaver()
agent = create_agent(
"gpt-5",
checkpointer=checkpointer,
system_prompt="You are a helpful, concise assistant.",
)
config = {"configurable": {"thread_id": "conversation-1"}}
agent.invoke(
{"messages": [{"role": "user", "content": "Hi! My name is Ramesh."}]},
config=config,
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "What's my name?"}]},
config=config,
)
print(result["messages"][-1].content)
The second call correctly answers "Ramesh" - even though it's a separate invoke() call - because both calls share the same thread_id. Change the thread_id and you get a completely fresh conversation, which is exactly how you'd isolate memory per user or per chat session in a real app.
InMemorySaver keeps everything in your process's memory, so it disappears when your script exits. For anything you want to survive a restart, swap it for PostgresSaver or another persistent checkpointer - same pattern we've used throughout this series: change one line, keep the rest of your code.
Step 2: Keep Conversations From Growing Forever
Long conversations eventually blow past a model's context window, and cost money on every turn besides. LangChain's @before_model hook lets you trim the message list right before it's sent to the model:
from typing import Any
from langchain.messages import RemoveMessage
from langgraph.graph.message import REMOVE_ALL_MESSAGES
from langchain.agents import AgentState
from langchain.agents.middleware import before_model
from langgraph.runtime import Runtime
@before_model
def trim_messages(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
"""Keep only the most recent messages to stay within context limits."""
messages = state["messages"]
if len(messages) <= 6:
return None # nothing to trim yet
first_msg = messages[0]
recent_messages = messages[-5:]
return {
"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), first_msg, *recent_messages]
}
agent = create_agent(
"gpt-5",
checkpointer=checkpointer,
middleware=[trim_messages],
)
This is middleware: a function that intercepts the agent loop at a defined point - here, right before every model call - and gets a chance to modify what happens next.
Step 3: Require Human Approval for Sensitive Tools
The most important middleware pattern for production agents is human-in-the-loop. If a tool can send an email, charge a card, or delete data, you generally don't want the model taking that action unsupervised.
from langchain.agents.middleware import HumanInTheLoopMiddleware
agent = create_agent(
"gpt-5",
tools=[send_email, get_weather],
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"send_email": {"allowed_decisions": ["approve", "reject"]},
}
)
],
checkpointer=checkpointer,
)
With this in place, whenever the agent decides to call send_email, execution pauses and hands control back to your application instead of firing the tool immediately. Your app can then show the proposed email to a human, and only actually send it once approved - the agent literally cannot bypass this gate, because it's enforced outside the model's control, not requested of it.
Notice get_weather isn't listed in interrupt_on - only tools you name there get the approval gate, so low-risk tools keep running automatically.
Putting It Together
A production-grade support agent might combine everything from this series so far:
agent = create_agent(
"gpt-5",
tools=[search_company_docs, send_email], # Part 3 & 4
system_prompt="You are a support assistant...", # Part 2
checkpointer=checkpointer, # memory, this part
middleware=[trim_messages, HumanInTheLoopMiddleware(
interrupt_on={"send_email": {"allowed_decisions": ["approve", "reject"]}}
)], # guardrails, this part
)
That's a genuinely production-shaped agent in about ten lines - remembers the conversation, grounds answers in your documents, and can't send an email without a human saying yes.
What's Next
In the final article of this series, Part 6, we have covered how to actually see what your agent is doing - tracing every model call and tool call with LangSmith, and using that visibility to debug the inevitable cases where an agent picks the wrong tool or gets stuck in a loop.
This is Part 5 of a 6-part LangChain beginner series.

Top comments (0)