LangChain Mastery

Course Content

LangChain Mastery

7 sections · 109 lessons

Write a function to implement a custom memory type in LangChain.


What you need to know

Pick the right extension point

You want to changeExtend (LangChain 1.x)
What the model sees each callwrap_model_call or before_model middleware
What happens after each turn (save facts, export)after_agent middleware
Structured facts remembered in a threadCustom state_schema fields
Facts shared across threadsLangGraph store (put, get, search)
Where checkpoints are storedBaseCheckpointSaver subclass (rare)
Legacy: storage for RunnableWithMessageHistoryBaseChatMessageHistory subclass

Keep storage (where state lives) separate from policy (what is kept and sent). Most "custom memory" requests are policy, and middleware is the place for it.

A custom memory type as middleware

This one keeps a per-thread "case file" of key facts in state, updates it after each turn, and shows it to the model on every call:

Python
from typing_extensions import NotRequiredfrom langchain.agents import AgentState, create_agentfrom langchain.agents.middleware import AgentMiddlewarefrom langchain.messages import SystemMessageclass CaseState(AgentState):    case_file: NotRequired[dict[str, str]]           # checkpointed with the messagesclass CaseFileMemory(AgentMiddleware[CaseState]):    state_schema = CaseState    def after_agent(self, state, runtime):        facts = extract_facts(state["messages"][-4:])          # small model, structured output        return {"case_file": {**state.get("case_file", {}), **facts}}    def wrap_model_call(self, request, handler):        facts = request.state.get("case_file", {})        note = "\n".join(f"- {k}: {v}" for k, v in facts.items()) or "- none yet"        base = request.system_message.content if request.system_message else ""        return handler(request.override(            system_message=SystemMessage(f"{base}\n\nKnown facts:\n{note}")))agent = create_agent(model, tools=TOOLS, middleware=[CaseFileMemory()],                     checkpointer=saver)
  • state_schema on the middleware adds case_file to the agent state, so the checkpointer saves it per thread, like the messages.
  • after_agent runs once per turn, after the final answer, and returns a state update.
  • wrap_model_call adds the facts to the system message for this call only; nothing extra is stored in the message history.

Because the facts live in their own field, trimming or summarising the messages can never lose them.

Legacy: a custom chat message history

In 2024–2025 code, "custom memory" usually meant subclassing BaseChatMessageHistory and implementing three members: the messages property (read), add_messages (append) and clear (delete). It then plugged into RunnableWithMessageHistory. messages_to_dict and messages_from_dict were used to serialise, so tool calls survived the round trip. Know it for reading old code; the wrapper it served is deprecated since langchain-core 1.3.3.

A real-life example

A law firm's contract assistant must keep a record of every client conversation in the firm's own document-management system (DMS), for confidentiality and audit. The 2024 version did this with a BaseChatMessageHistory subclass that wrote each message to the DMS through its API.

When the firm rebuilt the assistant on create_agent, it kept a Postgres checkpointer on its own servers for working state, and added an after_agent middleware that writes each finished turn — question, answer and clause citations — to the matter's DMS record. A matter_id field in the state schema ties every turn and tool call to the right matter. The storage became standard, the firm-specific rule became about 25 lines of middleware, and the old history class was deleted.

Follow-up questions to expect

  • "When would you write a custom checkpointer?" — Only when state must live in a database with no existing saver; you must implement get, list, put, put-writes and delete-thread correctly, sync and async.
  • "What three methods did a custom chat history need?" — messages, add_messages and clear.
  • "Why not subclass BaseMemory?" — It is the deprecated interface of the old Chain classes, which now live in langchain-classic; it fits neither LCEL nor LangGraph.