Building a simple, conversational chatbot that remembers previous interactions involves applying memory components in a practical exercise. This hands-on example demonstrates how to integrate a memory object into a chain to maintain state across multiple turns of a conversation.
First, let's prepare our environment. We need to install the necessary libraries and configure our API keys. This example will use the OpenAI API, but the principles apply to any LLM supported by LangChain.
# Ensure you have the required libraries installed
# pip install langchain langchain-openai langchain-community python-dotenv
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# Load environment variables from .env file
load_dotenv()
# It's good practice to set your API key as an environment variable
# os.environ["OPENAI_API_KEY"] = "your_api_key_here"
Before adding memory, let's quickly illustrate the problem we are solving. Without a memory component, each call to the LLM is independent. If you were to ask a follow-up question, the model would have no context of your previous statement.
Consider this interaction:
The model fails on the follow-up because the third turn is processed in complete isolation from the first.
To solve this, we will use LangChain's RunnableWithMessageHistory. This utility wraps a standard chain (Model + Prompt) and automatically manages reading from and writing to a history store based on a session ID.
We start by defining the model and the prompt template. The prompt must include a placeholder for the message history so the model can see previous turns.
# Initialize the LLM
llm = ChatOpenAI(model_name="gpt-3.5-turbo", temperature=0.7)
# Define the prompt template
# We include a system message to set the behavior, a placeholder for history,
# and a template for the new user input.
prompt = ChatPromptTemplate.from_messages([
("system", "You are a friendly assistant who remembers details from the conversation."),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}")
])
# Create the basic chain
chain = prompt | llm
The variable_name="chat_history" tells the system where to inject the past messages.
Next, we need a way to store the conversation data. For this example, we will use a simple dictionary to hold InMemoryChatMessageHistory objects, keyed by a session ID. In a production app, you might use a database like Redis here.
# Dictionary to store conversation history for different sessions
store = {}
def get_session_history(session_id: str) -> InMemoryChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
Now we wrap our basic chain using RunnableWithMessageHistory. This object connects our chain, the history factory function, and the specific keys used in the prompt.
# Create the conversational runnable
conversation = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history"
)
We specify input_messages_key="input" to match the {input} variable in our prompt, and history_messages_key="chat_history" to match the MessagesPlaceholder.
Now, let's test our chatbot. We use the .invoke() method, passing in the input and a config dictionary containing the session_id. This ID allows the system to retrieve the correct history.
First Interaction: Let's provide some initial information.
# First user input with a specific session ID
config = {"configurable": {"session_id": "user_123"}}
response = conversation.invoke(
{"input": "Hi, my name is Clara. I'm building a chatbot."},
config=config
)
print(response.content)
The chatbot will provide a friendly greeting. The InMemoryChatMessageHistory has now stored both our input and the model's response under the key user_123.
Second Interaction: Now for the real test. Let's ask a follow-up question that relies on the context from the first message.
# Second user input using the SAME session ID
response = conversation.invoke(
{"input": "What is my name?"},
config=config
)
print(response.content)
The expected output will be something like:
Your name is Clara.
Success. The model correctly identified the name because the runnable retrieved the history for user_123, inserted it into the prompt, and sent the complete context to the LLM.
The process for each interaction follows a clear, cyclical pattern, which ensures that context is preserved and built upon.
This diagram illustrates the lifecycle of a single conversational turn. The runnable reads from the history store, formats the prompt, gets a response from the LLM, and then writes the new interaction back to the store, preparing it for the next turn.
This practical exercise demonstrates the fundamental mechanism for building stateful applications. While keeping all history in memory is simple, it is powerful for short sessions. For applications requiring very long conversations, you can implement strategies to summarize old messages or trim the history, which can be handled by modifying the logic within the history retrieval or chain steps.
Cleaner syntax. Built-in debugging. Production-ready from day one.
Built for the AI systems behind ApX Machine Learning
Was this section helpful?
© 2026 ApX Machine LearningEngineered with