#langchain#chat-memory#runnablewithmessagehistory#sqlchatmessagehistory#messagesplaceholder#session-id#python#ollama

LangChain Chat Message Memory

Add persistent chat memory to any LangChain chain — store and replay multi-turn conversation history using RunnableWithMessageHistory and SQLChatMessageHistory.

Jun 4, 2026 at 10:30 AM7 min readFollowFollow (Hindi)

Topics You Will Master

Why stateless chains lose context between turns and how memory solves this
Using RunnableWithMessageHistory to wrap any LCEL chain with persistent history
Storing chat history in a local SQLite database with SQLChatMessageHistory
Managing multiple independent conversations via session_id
Clearing a session's history and starting fresh
Passing history through a MessagesPlaceholder for dict-input chains
Building a reusable chat_with_llm() helper that maintains context across calls
Best For

Python developers building conversational applications with LangChain who need the model to remember what was said earlier in the conversation.

Expected Outcome

A persistent multi-turn chat pipeline where the model recalls everything said in a session, supports multiple separate sessions by ID, and can have its history wiped and restarted on demand.

Chat message memory solves the statelessness problem of LLM chains. A plain chain.invoke() call has no memory — every call is independent. RunnableWithMessageHistory wraps your existing chain, loads the conversation history before each call, and saves the new exchange after it. The result: the model behaves like a stateful assistant that remembers everything said in the session.

Two things you need to set up memory:

  1. Where to store messages — a BaseChatMessageHistory implementation (e.g., SQLChatMessageHistory for SQLite)
  2. What to wrap — your existing LCEL chain, plus input/output key mapping

Prerequisites: LangChain, langchain-ollama, langchain-community, and python-dotenv installed. Ollama running locally with qwen3. See LangChain Output Parsing for prior context.

LangChain & Ollama — Local AI Development

Build production-ready LLM apps entirely on your own hardware. No API keys, no cloud costs.

Enroll on Udemy →

The Problem: Chains Have No Memory

To illustrate why memory is needed, here is a stateless chain that forgets immediately:

PYTHON
from dotenv import load_dotenv

load_dotenv('.env')
OUTPUT
True

On Linux/macOS: use load_dotenv('./../.env') if .env is in a parent directory.

PYTHON
from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

base_url = "http://localhost:11434"
model = 'qwen3'

llm = ChatOllama(base_url=base_url, model=model)

template = ChatPromptTemplate.from_template("{prompt}")
chain = template | llm | StrOutputParser()

about = "My name is Laxmi Kant. I work for KGP Talkie."
chain.invoke({'prompt': about})
OUTPUT
"Hello, Laxmi Kant! It's great to meet you. KGP Talkie sounds like a dynamic platform, and I'm curious to learn more about your role there. Could you share a bit about your work or what KGP Talkie specializes in? Whether it's podcasts, audio content, or something else, I'd love to hear about it! 😊"

The model received the introduction and replied. Now ask it to recall:

PYTHON
prompt = "What is my name?"
chain.invoke({'prompt': prompt})
OUTPUT
"I don't have access to your name. Could you please tell me your name so I can address you properly? 😊"

The model has no memory of the previous call. Each chain.invoke() is a completely independent request — no conversation state is carried over.


RunnableWithMessageHistory

RunnableWithMessageHistory wraps any LCEL chain and injects stored history automatically before each call.

It requires:

  • A history factory function get_session_history(session_id) that returns a BaseChatMessageHistory object
  • Your existing chain

Imports

PYTHON
from langchain_core.prompts import (
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
    ChatPromptTemplate
)
from langchain_core.output_parsers import StrOutputParser
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import SQLChatMessageHistory

Setting Up the History Store

SQLChatMessageHistory stores all messages in a local SQLite database. Each session_id maintains its own independent conversation thread:

PYTHON
def get_session_history(session_id):
    return SQLChatMessageHistory(session_id, connection="sqlite:///chat_history.db")

chat_history.db is created automatically in the current working directory on first use.

Wrapping the Chain

PYTHON
runnable_with_history = RunnableWithMessageHistory(chain, get_session_history)

Invoking with a Session ID

Pass a session_id in the config dict. The wrapper loads prior messages for that session, prepends them to the chain input, and saves the new exchange automatically:

PYTHON
user_id = 'your-username'
history = get_session_history(user_id)

history.get_messages()

After two turns (about + "whats my name?"), the history contains both exchanges:

PYTHON
[HumanMessage(content='My name is Laxmi Kant. I work for KGP Talkie.', ...),
 AIMessage(content='Hello, Laxmi Kant! Welcome to KGP Talkie...', ...),
 HumanMessage(content='whats my name?', ...),
 AIMessage(content='Your name is Laxmi Kant! 😊 ...', ...)]

Clearing a Session

Call .clear() on the history object to wipe all messages for that session:

PYTHON
history.clear()

After clearing, history.get_messages() returns an empty list and the next invocation starts a fresh conversation.

Sending Messages

Pass a list of HumanMessage objects (no prompt template needed for this simple form):

PYTHON
runnable_with_history.invoke(
    [HumanMessage(content=about)],
    config={'configurable': {'session_id': user_id}}
)
OUTPUT
'Hello, Laxmi Kant! Welcome to KGP Talkie. How are you doing today? If you have any questions or need assistance with anything related to your work or KGP Talkie, feel free to ask. 😊'

Now ask the follow-up:

PYTHON
runnable_with_history.invoke(
    [HumanMessage(content="whats my name?")],
    config={'configurable': {'session_id': user_id}}
)
OUTPUT
'Your name is Laxmi Kant! 😊 Let me know if you need anything else.'

The model correctly recalled the name from the stored session history — the memory is working.

Note

session_id is how LangChain separates conversations. Two different users with different session_id values get completely independent histories from the same chain.


Message History with Dictionary Inputs

The simple RunnableWithMessageHistory form above works when the chain takes a list of messages as input. In production, chains typically use a ChatPromptTemplate with named input keys. For this pattern, use MessagesPlaceholder to inject the history into the prompt and specify input_messages_key and history_messages_key.

Building the Prompt with MessagesPlaceholder

PYTHON
from langchain_core.prompts import (
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
    ChatPromptTemplate,
    MessagesPlaceholder
)
from langchain_core.output_parsers import StrOutputParser
from langchain_core.messages import HumanMessage, SystemMessage
PYTHON
system = SystemMessagePromptTemplate.from_template("You are helpful assistant.")
human = HumanMessagePromptTemplate.from_template("{input}")

messages = [system, MessagesPlaceholder(variable_name='history'), human]

prompt = ChatPromptTemplate(messages=messages)

chain = prompt | llm | StrOutputParser()

MessagesPlaceholder(variable_name='history') reserves a slot in the prompt where the conversation history will be injected. The system message appears first, then the full message history, then the current user input — giving the model full context for every turn.

Wrapping with Named Keys

PYTHON
runnable_with_history = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key='input',
    history_messages_key='history'
)
  • input_messages_key='input' — the key in your input dict that holds the current user message
  • history_messages_key='history' — the key where the wrapper injects loaded history messages (must match MessagesPlaceholder's variable_name)

Chat Helper Function

Wrap the invocation in a helper for clean reuse:

PYTHON
def chat_with_llm(session_id, input):
    output = runnable_with_history.invoke(
        {'input': input},
        config={'configurable': {'session_id': session_id}}
    )
    return output

Testing Across Turns

First turn — introduce yourself:

PYTHON
user_id = "kgptalkie"
chat_with_llm(user_id, about)
OUTPUT
'Namaste, Laxmi Kant! 😊
Your name is **Laxmi Kant**, and you work at **KGP Talkie**. If you ever need help with anything or want to share your experiences, feel free to chat. 🎬✨ What's your role there?'

Second turn — ask the model to recall:

PYTHON
chat_with_llm(user_id, "what is my name?")
OUTPUT
'Your name is **Laxmi Kant**. 😊
If you have any questions or need assistance, feel free to ask! 🎬✨'

The model remembers the name across turns through the persisted SQL history.

Tip

Because history is stored in SQLite, it survives Python restarts. Shut down the kernel, restart, call get_session_history(user_id) again with the same session_id, and all prior messages are still there.

Important

The history_messages_key in RunnableWithMessageHistory and the variable_name in MessagesPlaceholder must match exactly. A mismatch causes the history slot to be empty and the model loses all context.

On Linux/macOS: all code above runs identically — no OS-specific differences. The SQLite file chat_history.db is created in the current working directory on all platforms.


Next Step: Build Your Own Chatbot

The patterns above — MessagesPlaceholder + RunnableWithMessageHistory + get_session_history + a streaming chat_with_llm helper — are exactly what the next lesson uses to build a full interactive chatbot with a Streamlit UI. See the Build Your Own Chatbot guide.


Quick Reference

Core Pattern

PYTHON
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import SQLChatMessageHistory

def get_session_history(session_id):
    return SQLChatMessageHistory(session_id, connection="sqlite:///chat_history.db")

runnable_with_history = RunnableWithMessageHistory(
    chain,                          # your LCEL chain
    get_session_history,            # history factory
    input_messages_key='input',     # key for current user message
    history_messages_key='history'  # key where history is injected (must match MessagesPlaceholder)
)

# Invoke
output = runnable_with_history.invoke(
    {'input': "Hello!"},
    config={'configurable': {'session_id': 'user-123'}}
)

Prompt Template with History Slot

PYTHON
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate([
    SystemMessagePromptTemplate.from_template("You are a helpful assistant."),
    MessagesPlaceholder(variable_name='history'),  # history injected here
    HumanMessagePromptTemplate.from_template("{input}")
])

Session Management

PYTHON
history = get_session_history('user-123')
history.get_messages()   # list all stored messages
history.clear()          # wipe the session

Key Imports

PYTHON
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import SQLChatMessageHistory
from langchain_core.prompts import MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage

What You Built

In this lesson you upgraded a stateless chain into a stateful conversational assistant:

  • Without memory: each chain.invoke() is independent — the model forgets everything between calls
  • With RunnableWithMessageHistory: the wrapper loads stored messages before each call and saves the response after — no changes to your chain required
  • SQLChatMessageHistory: stores all sessions in a local SQLite file — durable, multi-session, and zero-configuration
  • MessagesPlaceholder: reserves a slot in the ChatPromptTemplate where the history is injected — preserving the full system + history + input structure the model needs for context
  • session_id: the key that isolates conversations — different users or different conversation threads each get their own independent history

Find this tutorial useful?

Subscribe to our YouTube channels for more practical production walk-throughs.

Discussion & Comments