Memory & Chat History
Ask an LLM “my name is Ajeet”, then “what is my name?”, and it has no idea. Every call starts fresh: LLMs are stateless. “Memory” simply means sending the right old messages again. In this lab you build that by hand, then with LangChain helpers that keep a separate memory for every user, save it to a file and trim it to control cost.
In this session
- Why the LLM forgets
- Manual memory with a list
- InMemoryChatMessageHistory
- RunnableWithMessageHistory: memory per session_id
- FileChatMessageHistory survives restarts
- trim_messages and the 4 memory types
01The big idea: the doctor reads your file every visit
Think of a clinic. The doctor sees 50 patients a day and remembers none of them. Before your turn, the nurse puts your file on the table: past visits, medicines, allergies. LLM memory works the same way: the model forgets, so your app puts the old messages (the file) in front of it on every call.
02Memory types and cost
Every message you resend costs tokens. Long chats need a strategy:
| Type | Idea | Good for | LangChain v1 way |
|---|---|---|---|
| Buffer | keep all messages | short chats | history list / checkpointer |
| Window | keep last k messages | long chats, low cost | trim_messages(strategy="last") |
| Summary | LLM summarises old messages | very long chats | SummarizationMiddleware (Lab 14) |
| Vector / long-term | save facts in a database | personal assistants | LangGraph Store |
Messages sent on each call with buffer memory: the system message, all old turns and the new question (2 × turn number). Cost grows with the chat; trimming keeps it flat.
RunnableWithMessageHistory and InMemoryChatMessageHistory still work but now print a deprecation warning. For new agents the recommended memory is a checkpointer + thread_id (Lab 13 and Lab 17). Learn this lab’s version too: you will see it in most existing code.03Hands-on: run it here
The stand-in LLM can only “remember” what is inside the messages it receives, exactly like a real model. Watch Ajeet’s session remember and Priya’s session start empty.
from standins import RuleModel
from langchain.messages import SystemMessage, HumanMessage, AIMessage, trim_messages
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
class EchoMemory(RuleModel):
"""Stand-in that can only 'remember' what is inside the messages it receives - exactly like a real LLM."""
def _pick(self, messages):
seen = " ".join(str(m.content) for m in messages[:-1])
q = str(messages[-1].content).lower()
if "name" in q or "summarise" in q:
facts = [f for f in ["Ajeet", "Pune", "3-day trip", "Rs 15,000", "mountains"] if f.lower() in seen.lower()]
return "You told me: " + ", ".join(facts) if facts else "Sorry, I don't know that - you haven't told me yet."
return "Noted!"
llm = EchoMemory()
print("no memory :", llm.invoke("What is my name and where do I live?").content)
chat_history = [SystemMessage("You are a friendly assistant. Keep answers short.")]
def chat(user_input):
chat_history.append(HumanMessage(user_input))
response = llm.invoke(chat_history)
chat_history.append(AIMessage(response.content))
return response.content
chat("Hi, my name is Ajeet and I live in Pune.")
print("manual :", chat("What is my name and where do I live?"), f"({len(chat_history)} messages sent)")
prompt = ChatPromptTemplate.from_messages([("system", "You are a helpful travel assistant."), MessagesPlaceholder("history"), ("human", "{input}")])
store = {}
def get_session_history(session_id):
if session_id not in store: store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
chatbot = RunnableWithMessageHistory(prompt | llm, get_session_history, input_messages_key="input", history_messages_key="history")
cfg = {"configurable": {"session_id": "ajeet"}}
chatbot.invoke({"input": "I want a 3-day trip from Pune with Rs 15,000 budget."}, config=cfg)
chatbot.invoke({"input": "I like mountains."}, config=cfg)
print("ajeet :", chatbot.invoke({"input": "Summarise my requirements."}, config=cfg).content)
print("priya :", chatbot.invoke({"input": "Summarise my requirements."}, config={"configurable": {"session_id": "priya"}}).content)
print("store :", {k: len(v.messages) for k, v in store.items()})
trimmed = trim_messages(store["ajeet"].messages, max_tokens=12, strategy="last", token_counter=lambda ms: sum(len(str(m.content).split()) for m in ms), start_on="human")
print("trimmed :", len(store["ajeet"].messages), "->", len(trimmed), [m.content[:30] for m in trimmed])RuleModel or ScriptedModel from standins.py), so every output below is from a real run of this code. In Colab you swap one line: llm = ChatGroq(model="openai/gpt-oss-20b").04Hands-on: Ajeetkumar’s Colab notebook
groq_key with your free key from console.groq.com, switch on Notebook access, then run the cells from the top.05Practice tasks
- Build a “Doctor appointment” bot that remembers name, age and symptoms.
- Keep only the last 4 messages using
trim_messagesinside the chain. - Show that two session ids keep separate memories.
Try these in the same Colab notebook after you finish the lab. Change one thing at a time and look at the output again.
06Summary
- LLMs are stateless: memory = resending old messages.
- Manual: keep a list and append human + AI messages.
RunnableWithMessageHistorykeeps one history persession_id.FileChatMessageHistorysurvives restarts;trim_messagescontrols cost.- For agents in LangChain 1.x use a checkpointer +
thread_id.
07Quick quiz
1. Why does the LLM forget your name between calls?
2. What gives each user their own memory?
3. How to stop costs growing in a long chat?
4. Recommended memory for agents in LangChain 1.x?
5. Which survives a Colab restart?
08References
- LangChain: Short-term memory
- trim_messages
- Ajeetkumar’s notebook: LC_05_Memory_and_Chat_History.ipynb (download)