LangChainMiddleCoding
Как реализовать историю диалога с помощью MessagesPlaceholder в LangChain?
MessagesPlaceholder — специальный слот в ChatPromptTemplate, который вставляет список объектов BaseMessage (историю диалога) напрямую в промпт, сохраняя тип каждого сообщения (HumanMessage, AIMessage, SystemMessage).
Зачем нужен MessagesPlaceholder
MessagesPlaceholder решает проблему встраивания динамического списка сообщений в шаблон промпта. В отличие от обычного {variable}, который подставляет строку, MessagesPlaceholder принимает список объектов BaseMessage и вставляет их «как есть», сохраняя роли (human, ai, system). Это критично для истории диалога: модель должна видеть чередование реплик пользователя и ассистента, а не их конкатенацию в строку.
Базовый пример с RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", "Ты эксперт по {domain}. Отвечай кратко."),
MessagesPlaceholder(variable_name="chat_history"), # история диалога
("human", "{input}"), # текущий вопрос
])
chain = prompt | llm
store: dict[str, ChatMessageHistory] = {}
def get_session_history(session_id: str) -> ChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
chain_with_history = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
)
cfg = {"configurable": {"session_id": "alice"}}
reply1 = chain_with_history.invoke(
{"input": "Что такое контекстное окно?", "domain": "LLM"},
config=cfg,
)
print(reply1.content)
reply2 = chain_with_history.invoke(
{"input": "А как его увеличить?", "domain": "LLM"},
config=cfg,
)
print(reply2.content) # Бот понимает «его» из контекста
Что происходит внутри промпта
После заполнения шаблон превращается в список сообщений:
from langchain_core.messages import HumanMessage, AIMessage
# MessagesPlaceholder подставляет реальные объекты сообщений
formatted = prompt.format_messages(
domain="LLM",
chat_history=[
HumanMessage(content="Что такое контекстное окно?"),
AIMessage(content="Контекстное окно — максимальное количество токенов..."),
],
input="А как его увеличить?",
)
# Результат: [SystemMessage, HumanMessage, AIMessage, HumanMessage]
for msg in formatted:
print(f"{msg.type}: {msg.content[:50]}")
optional=True для пустой истории
# При первом сообщении история пустая — без optional=True
# вылетит KeyError или ValidationError
prompt = ChatPromptTemplate.from_messages([
("system", "Ты ассистент."),
MessagesPlaceholder(variable_name="chat_history", optional=True),
("human", "{input}"),
])
# Работает даже без ключа chat_history во входных данных
result = (prompt | llm).invoke({"input": "Привет!"})
Ограничение размера истории через trim_messages
from langchain_core.messages import trim_messages
from langchain_core.runnables import RunnableLambda
trimmer = trim_messages(
max_tokens=1000,
strategy="last", # оставляем последние сообщения
token_counter=llm, # используем токенизатор модели
include_system=True, # системное сообщение не удалять
allow_partial=False,
start_on="human", # начинаем с реплики пользователя
)
chain = (
RunnableLambda(lambda x: {**x, "chat_history": trimmer.invoke(x["chat_history"])})
| prompt
| llm
)
Подводные камни
- Имя переменной в
MessagesPlaceholder(variable_name="chat_history")должно точно совпадать сhistory_messages_keyвRunnableWithMessageHistory— несовпадение даётKeyErrorв runtime - Без
optional=Trueпромпт падает при пустом или отсутствующем ключе истории — всегда добавляйте этот параметр - История хранится как объекты
BaseMessage, но Redis-сериализатор по умолчанию хранит JSON — при десериализации тип сообщения (HumanMessage vs AIMessage) должен восстанавливаться корректно - При использовании нескольких
MessagesPlaceholderв одном промпте (например, системный контекст + диалог) порядок имеет значение — LLM интерпретирует чередование ролей строго по порядку в списке - Без обрезки истории (
trim_messages) длинные сессии превышают context limit модели — ошибкаcontext_length_exceededот API, часто без понятного сообщения - Изменение
variable_nameвMessagesPlaceholderтребует синхронного изменения вRunnableWithMessageHistory— это не проверяется статически - В многопользовательском приложении при конкурентных запросах с одним
session_idистория может записываться в неправильном порядке — нужна блокировка на уровне store - Старый API
ConversationChainавтоматически управлял промптом с историей, но в LCEL это ответственность разработчика — легко забытьMessagesPlaceholderи получить бота без памяти
Common mistakes
- Объяснять
messagesplaceholder historyтолько синтаксисом без shape, dtype, состояния или режима выполнения. - Игнорировать leakage, воспроизводимость, пустые входы и скрытые копии данных.
- Не проверять production-симптомы: latency, память, ретраи, дрейф качества и несовпадение версий.
What the interviewer is testing
- Может ли связать
messagesplaceholder historyс реальным контрактом входов и выходов. - Упоминает ли тесты, метрики, reproducibility и диагностику ошибок.
- Видит ли различие между demo-кодом в ноутбуке и production-пайплайном.