What does MessagesPlaceholder do in a LangChain ChatPromptTemplate?
answer
- a slot for many messages, not one
- list of message objects at format time
- roles and tool calls survive intact
- optional means the key may be missing
- position decides what the model sees last
basics
~20 sMessagesPlaceholder reserves a slot inside a ChatPromptTemplate that is filled at format time with a whole list of message objects — typically prior turns — instead of a formatted string, preserving each message's role and metadata.
solid answer
~40 sEvery other element of `ChatPromptTemplate.from_messages([...])` renders to exactly one message. `MessagesPlaceholder("history")` renders to *n* messages: at format time you pass `history=[HumanMessage(...), AIMessage(...)]` and the list is spliced in at that position. That matters because the messages arrive as real objects — roles intact, and for assistant turns the `tool_calls` and tool-result messages intact too — rather than as text you flattened yourself. Position is meaningful: the placeholder normally sits after the system message and before the current human turn, so instructions stay first and the newest input is last. `MessagesPlaceholder("history", optional=True)` lets you omit the key entirely on the first turn instead of raising a missing-variable error, and `n_messages` truncates to the last *n*. The shorthand `("placeholder", "{history}")` inside `from_messages` builds the same thing.
code
python · 17 linesfrom langchain_core.messages import AIMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant."),
MessagesPlaceholder("history", optional=True),
("human", "{input}"),
])
# first turn: no history key needed because the placeholder is optional
print(prompt.format_messages(input="hi"))
# later turn: prior messages are spliced in with their roles intact
print(prompt.format_messages(
history=[HumanMessage("hi"), AIMessage("hello, how can I help?")],
input="and what about latency?",
))go deeper
Know that MessagesPlaceholder marks the spot in a chat prompt where a list of earlier messages gets inserted, and that you supply that list by variable name when formatting.
Explain that it expands to n messages rather than one, that optional=True allows the key to be missing, and why the messages must stay typed objects rather than concatenated text.
Argue the production case: flattening history destroys tool-call/tool-result pairing and breaks prefix caching, and a naive tail truncation can split a call from its result. Show where you place the slot and why.
Own the prompt layout as a contract — stable instructions first for cacheability, explicit slots for conversation and for in-turn scratch messages, and a trimming policy defined once rather than per feature.
## The shape mismatch it fixes A chat prompt is a list of messages, and most of that list is static structure you wrote: a system message, a human message template. But a conversation has a variable-length middle — an unknown number of prior turns — and the tuple form of `from_messages` cannot express "and now zero or more messages here". `MessagesPlaceholder` is that hole. It is a prompt element whose rendered value is a *list* of messages supplied at format time. ``` from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder prompt = ChatPromptTemplate.from_messages([ ("system", "You are a helpful assistant."), MessagesPlaceholder("history"), ("human", "{input}"), ]) prompt.input_variables # ['history', 'input'] ``` At format time, `history` must be a list of message objects (or things coercible to them). Each is inserted verbatim at the placeholder's position. ## Why not just format history into a string? This is the heart of the question. The tempting alternative is to render prior turns into text — `"Human: ...\nAssistant: ..."` — and interpolate that into one human message. Three things break. First, roles collapse. The provider sees one long user message instead of an alternating conversation, and models are trained on the alternating shape. Second, tool calls cannot round-trip. An assistant turn that called a tool carries structured `tool_calls`, and the follow-up is a tool message keyed by call id. Flattened to text, that structure is gone; the model can no longer see that it already called the tool and what came back, and some providers reject or mishandle a subsequent tool call whose preceding call has no matching result. Third, provider features keyed on message boundaries — caching of a stable prefix, per-message metadata, multimodal content blocks — stop applying. The placeholder exists precisely so the history stays a list of typed messages end to end. ## Options that matter `optional=True` makes the variable non-required: if the key is absent when formatting, the placeholder contributes nothing rather than raising. Without it, the very first turn of a conversation must still pass `history=[]`. Making it optional is friendlier for callers, but be deliberate — an optional placeholder also silently absorbs a typo'd key name, and you will debug a model that has quietly forgotten everything. `n_messages` caps how many messages are used, keeping the last *n*. It is a crude but real guard against unbounded growth. It is crude because it counts messages, not tokens, and because a blind tail slice can decapitate a tool call from its result or start the window on an assistant turn. Treat it as a safety net, not a memory strategy. ## Position is part of the design Where you put the placeholder is a design decision: - After system, before the current human turn: the standard layout. Instructions are first and stable (good for prefix caching); the newest user input is last, where models attend to it most reliably. - Multiple placeholders: entirely legal. A common agent layout is system, `history`, current human input, then a second placeholder such as `agent_scratchpad` holding the in-progress tool-call/tool-result messages for this turn. - Before the system message: almost always wrong — it buries your instructions. ## Shorthand and equivalents Inside `from_messages` you may write `("placeholder", "{history}")` instead of constructing the object; it produces the same element. The explicit object is preferable when you need `optional` or `n_messages`, and it reads more clearly in a prompt module. A related but distinct case: passing a concrete message object into `from_messages` inserts exactly one fixed message and does not template it. The placeholder is the variable-count counterpart. ## Failure modes to recognise - Passing a string where a list of messages is expected: you get a type error, or worse, an iterable of characters treated as messages. - Forgetting the key with `optional=False`: a missing-variable error at format time — a good failure, because it is loud. - Duplicating the current user input both inside the history list and in the trailing human template, so the model sees the question twice. - Letting history grow without bound: the placeholder itself imposes no limit, so the token budget is your problem. Trimming, summarising, and where the messages are stored are separate concerns from the placeholder — the placeholder only defines the slot.
- What does optional=True change, and what does it cost you?With `optional=True` the variable may be absent at format time and the placeholder contributes no messages, so the first turn does not need `history=[]`. The cost is silence: a misspelled key name is no longer an error, it just produces a prompt with no history at all. If you rely on it, assert on the rendered message count in a test.
- Why can a blind tail truncation of history be worse than dropping the whole history?Because a message window can cut between an assistant turn that issued a tool call and the tool message carrying its result. The model then sees a result with no call, or a call with no result — a state some providers reject and others answer incoherently. Trim on conversational boundaries, not on a raw message count.
- Can one chat prompt contain more than one placeholder?Yes, and agent prompts commonly do: one slot for prior conversation turns and a second, later slot for the current turn's tool-call and tool-result messages. Keeping them separate lets you trim long-term history without touching the in-progress reasoning for this turn, which must stay intact for the call/result pairing to hold.
saying these in an interview costs you the question
- Passing a pre-joined history string instead of message objects
- Thinking the placeholder itself limits history growth
- Putting the placeholder before the system message
- Believing flattened history preserves tool-call structure
- Assuming the variable is optional by default