Home / Articles / Giving a LangChain Chatbot Conversation History With Messages

This article is published in English.

Giving a LangChain Chatbot Conversation History With Messages

Build a simple chatbot by passing earlier messages back to the model with MessagesPlaceholder, and see why history is not the same thing as built-in model memory.

1268 words

Up to now every model call has stood alone. Ask a question, get an answer, and the next call starts from nothing. To get something that behaves like a real chatbot, you need to deal with that first.

Here are two calls made back to back:

model.invoke("What is Python?")
model.invoke("What is it used for?")

The second one has no idea you were talking about Python. The model only sees what's in the messages you send it, so the earlier exchange has to be included.

The basic idea

A conversation is a list of turns. The first one:

Human → What is Python?
AI    → Python is a programming language.

And the follow-up:

Human → What is it used for?
AI    → Python is used for web development, AI, automation...

For the model to make sense of that second question, you send the earlier turns along with it, in order:

SystemMessage
HumanMessage
AIMessage
HumanMessage

That's why AIMessage matters so much. It's how the model's own earlier replies go back into the prompt.

A manual conversation first

It's worth doing this by hand before adding any helpers. The example below uses Ollama, so it runs against a local model and needs no API quota.

from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

model = ChatOllama(model="llama3.2")

prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        "You are a helpful teacher."
    ),
    MessagesPlaceholder(variable_name="history"),
    (
        "human",
        "{question}"
    )
])

chain = prompt | model

history = []


def chat(question):
    response = chain.invoke({
        "history": history,
        "question": question
    })

    history.append(HumanMessage(content=question))
    history.append(AIMessage(content=response.content))

    return response.content


print(chat("What is Python?"))
print(chat("What is it used for?"))
print(chat("Can you give me three examples?"))

Look closely at this line:

messages.append(response)

It drops the model's answer back into the conversation. After that, the list holds the system message, the first question, the first answer and the follow-up question:

SystemMessage
HumanMessage("What is Python?")
AIMessage("Python is...")
HumanMessage("What is it used for?")

So when you call this, the model sees the whole thread:

model.invoke(messages)

Why it works

Imagine you sent only this:

HumanMessage(content="What is it used for?")

Nothing tells the model what "it" refers to. With the history attached, it reads this instead:

Human: What is Python?
AI: Python is a programming language.
Human: What is it used for?

Now it can resolve the pronoun:

"it" = Python

History is simply context handed to the model.

LangChain doesn't give the model memory

This point is easy to miss. Using LangChain doesn't make a model remember anything. The model generally receives only the messages you pass for that one interaction. In rough terms:

Your application
      ↓
Conversation history
      ↓
Messages
      ↓
Model

Your application owns the conversation state. Keep that in mind, because it becomes a big deal once you reach state management and LangGraph.

Adding a prompt template

History combines nicely with the prompt templates from earlier lessons. Start with the imports:

from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

Then define the prompt:

prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        "You are a helpful Python teacher."
    ),
    MessagesPlaceholder(variable_name="history"),
    (
        "human",
        "{question}"
    )
])

The line that does the work is this one:

MessagesPlaceholder(variable_name="history")

Read it as "insert the earlier messages here."

Building the chain

Pipe the prompt into the model:

chain = prompt | model

Start with an empty history list:

history = []

Make the first call and print the reply:

response = chain.invoke({
    "history": history,
    "question": "What is Python?"
})print(response.content)

Then store both sides of the exchange, first the user's message:

history.append(
    HumanMessage(content="What is Python?")
)

and then the model's response:

history.append(response)

Now ask the follow-up:

response = chain.invoke({
    "history": history,
    "question": "What is it used for?"
})
print(response.content)

What the model actually receives looks roughly like this, beginning with the system instruction:

System:
You are a helpful Python teacher.

followed by the conversation so far:

Human:
What is Python?AI:
Python is a programming language.Human:
What is it used for?

Wrapping it as a chatbot

All of that fits inside one function:

from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

model = ChatOllama(model="llama3.2")

prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        "You are a helpful teacher."
    ),
    MessagesPlaceholder(variable_name="history"),
    (
        "human",
        "{question}"
    )
])

chain = prompt | model

history = []


def chat(question):
    response = chain.invoke({
        "history": history,
        "question": question
    })

    history.append(HumanMessage(content=question))
    history.append(AIMessage(content=response.content))

    return response.content


print(chat("What is Python?"))
print(chat("What is it used for?"))
print(chat("Can you give me three examples?"))

You now have a bare-bones conversational app.

A detail about ordering

The user's message goes into history after the model call, not before:

response = chain.invoke(...)
history.append(HumanMessage(content=question))
history.append(AIMessage(content=response.content))

The reason is that the prompt already takes the current question as its own input:

"question": question

so adding it to history as well would duplicate it. You could do it the other way and put everything in the history, then pass only the history to the model. Both designs work. Just make sure the current message doesn't show up twice.

History versus memory

Older LangChain tutorials talk a lot about "memory." Don't assume these two are always the same thing:

history = memory

At the level covered here, history just means the messages from earlier turns:

History = messages from previous turns

More capable state handling comes later, where an application keeps and updates conversation state across steps. For now, remember this flow:

Conversation history
        ↓
Previous messages
        ↓
Model gets context

The resulting architecture

The chatbot you've built works like this: the application stores history, feeds it into the prompt, and the model answers from it.

Why this matters in real apps

The same idea sits behind many products:

Customer support chatbot
AI tutor
Personal assistant
HR assistant
Coding assistant
Sales assistant

Take a simple exchange where the user introduces themselves:

User: My name is Wasim.

If the next question asks for their name, the model can answer correctly, because the earlier turns were included as context:

AI: Nice to meet you, Wasim.User: What is my name?AI: Your name is Wasim.

Your mental model so far

You've covered these pieces, from a bare model call up to a chain with history:

1. Model
       ↓
   invoke()2. Messages
       ↓
   System / Human / AI
3. Prompt Templates
       ↓
   {variables}
4. Structured Output
       ↓
   predictable data
5. Chains
       ↓
   prompt | model | parser
6. Conversation History
       ↓
   previous messages → context

What's next

The next topic is tools and tool calling. That's where an LLM starts doing things outside the model itself, along this path:

User
 ↓
LLM
 ↓
"I need a calculation"
 ↓
Python tool
 ↓
Result
 ↓
LLM
 ↓
Final answer

It's the groundwork for understanding agents.