LangChain

Память и история диалога в LangChain | Курс по LangChain урок 4

Память и история диалога в LangChain | Курс по LangChain урок 4
Mikhail
Автор
Mikhail
Опубликовано 23.02.2026
0,0
Views 4

Цель урока

Вы поймёте, почему LLM не помнят предыдущие сообщения. Научитесь вручную управлять историей диалога и подключать RunnableWithMessageHistory, чтобы память между запросами велась автоматически и изолированно по сессиям.

Необходимые знания:

  • Уроки 2–3 (LCEL, цепочки, шаблоны промптов)
  • Базовое понимание списков и словарей в Python

Ключевые концепции:

  • Stateless LLM (без сохранения состояния)
  • Ручное управление историей с помощью списка сообщений
  • ChatMessageHistory и InMemoryChatMessageHistory
  • MessagesPlaceholder
  • RunnableWithMessageHistory
  • Сессии и session_id

Почему модель не помнит предыдущие сообщения

LLM stateless по природе, где каждый вызов .invoke() - это независимый HTTP запрос к API. Модель получает входные данные, возвращает ответ и ничего не сохраняет.

Память это ответственность приложения, а не модели. Чтобы модель "помнила" разговор, нужно при каждом вызове передавать всю историю сообщений.

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI

load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"))

model.invoke("Меня зовут Алексей.")
response = model.invoke("Как меня зовут?")
print(response.content)
# "Я не знаю вашего имени, так как у меня нет информации о вас. ..." — модель не помнит первое сообщение

Ручное управление историей

Самый простой подход это хранить историю в обычном списке и передавать его в модель.

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage

load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"))

history = [
    SystemMessage(content="Ты дружелюбный ассистент-разработчик. Отвечай кратко."),
]

def chat(user_input: str) -> str:
    history.append(HumanMessage(content=user_input))
    response = model.invoke(history)
    history.append(response)  # response уже AIMessage
    return response.content

print(chat("Меня зовут Михаил."))
# "Привет, Михаил! 👋 Рад познакомиться. Чем я могу помочь?"

print(chat("Как меня зовут?"))
# "Тебя зовут Михаил! 😊"

print(chat("Напиши функцию на Python для вычисления факториала."))
# def factorial(n): ...

Каждый раз в model.invoke() передается весь список history. Модель видит полный контекст диалога.

Проблемы ручного подхода:

  • Одна глобальная история, она не масштабируется для нескольких пользователей
  • Нет изоляции между сессиями
  • Историю нужно везде передавать

ChatMessageHistory

LangChain предоставляет объект для хранения истории сообщений.

ChatMessageHistory - это просто обертка над списком сообщений. Её можно сериализовать, сохранить в БД или Redis. Сейчас воспользуемся её базовой версией.

from langchain_community.chat_message_histories import ChatMessageHistory

history = ChatMessageHistory()

history.add_user_message("Меня зовут Алексей.")
history.add_ai_message("Привет, Алексей!")
history.add_user_message("Как меня зовут?")

print(history.messages)
# [HumanMessage(content='Меня зовут Алексей.'),
#  AIMessage(content='Привет, Алексей!'),
#  HumanMessage(content='Как меня зовут?')]

RunnableWithMessageHistory

RunnableWithMessageHistory — обертка над любым Runnable, которая автоматически:

  1. Загружает историю нужной сессии перед вызовом
  2. Добавляет новое сообщение пользователя и ответ модели в историю после вызова
import os
from dotenv import load_dotenv
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

load_dotenv()

model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"))

prompt = ChatPromptTemplate.from_messages([
    ("system", "Ты дружелюбный ассистент-разработчик. Отвечай кратко."),
    MessagesPlaceholder(variable_name="history"),  # сюда подставится история
    ("human", "{input}"),
])

chain = prompt | model

# Хранилище сессий: session_id -> ChatMessageHistory
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="history",   # ключ для MessagesPlaceholder
)

Теперь общаемся через chain_with_history.invoke(), передавая config с session_id:

import os
from dotenv import load_dotenv
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

load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"))

prompt = ChatPromptTemplate.from_messages([
    ("system", "Ты дружелюбный ассистент-разработчик. Отвечай кратко."),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}"),
])

chain = prompt | model

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="history",
)


config_alice = {"configurable": {"session_id": "alice"}}
config_bob   = {"configurable": {"session_id": "bob"}}

# Диалог с Алисой
response = chain_with_history.invoke(
    {"input": "Меня зовут Алиса."},
    config=config_alice,
)
print(response.content)
# "Привет, Алиса!"

response = chain_with_history.invoke(
    {"input": "Как меня зовут?"},
    config=config_alice,
)
print(response.content)
# "Тебя зовут Алиса."

# Боб - отдельная сессия, не видит диалог Алисы
response = chain_with_history.invoke(
    {"input": "Как меня зовут?"},
    config=config_bob,
)
print(response.content)
# "Извини, я не знаю твоего имени — ты ещё не представился."

Каждый session_id это изолированная история. Это базовая модель для многопользовательских чат приложений.


Потоковый вывод с историей

RunnableWithMessageHistory поддерживает .stream() так же, как обычный Runnable.

История обновляется автоматически после того, как стриминг завершен.

import os
from dotenv import load_dotenv
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

load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"))

prompt = ChatPromptTemplate.from_messages([
    ("system", "Ты дружелюбный ассистент-разработчик. Отвечай кратко."),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}"),
])

chain = prompt | model
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="history",
)

config = {"configurable": {"session_id": "alice"}}

print("Ответ: ", end="")
for chunk in chain_with_history.stream(
    {"input": "Напиши функцию на Python для бинарного поиска с комментариями."},
    config=config,
):
    print(chunk.content, end="", flush=True)
print()

Ограничение длины истории

Бесконечная история это проблема, рано или поздно она превысит контекстное окно модели и увеличит стоимость запросов. Нужно обрезать историю до разумного размера.

Простой способ, хранить только последние N сообщений.

from langchain_community.chat_message_histories import ChatMessageHistory

store: dict[str, ChatMessageHistory] = {}

def get_limited_session_history(session_id: str, max_messages: int = 10) -> ChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()

    history = store[session_id]

    # Если сообщений больше лимита - обрезаем, оставляем последние max_messages
    if len(history.messages) > max_messages:
        history.messages = history.messages[-max_messages:]

    return history

LangChain также предоставляет готовый инструмент trim_messages для управления размером контекста.

trim_messages обрезает историю по количеству токенов, а не сообщений.

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import trim_messages
from langchain_core.runnables import RunnablePassthrough

load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"))

prompt = ChatPromptTemplate.from_messages([
    ("system", "Ты дружелюбный ассистент-разработчик. Отвечай кратко."),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}"),
])

trimmer = trim_messages(
    max_tokens=20,            # максимум сообщений в истории
    strategy="last",          # оставляем последние сообщения
    token_counter=len,        # считаем по количеству сообщений, не токенов
    include_system=True,      # системное сообщение сохраняем всегда
    allow_partial=False,      # не обрезать сообщения посередине
    start_on="human",         # история начинается с HumanMessage
)

# Встраиваем trimmer в цепочку перед моделью
chain = (
    RunnablePassthrough.assign(history=lambda x: trimmer.invoke(x["history"]))
    | prompt
    | model
)

Персистентная история: хранение между перезапусками

ChatMessageHistory хранит данные в памяти, при перезапуске приложения история стирается. В продакшн история хранится в базе данных.

LangChain поддерживает несколько бэкендов.

Пример с SQLite:

import os
from dotenv import load_dotenv
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 SQLChatMessageHistory

load_dotenv()
model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"))

prompt = ChatPromptTemplate.from_messages([
    ("system", "Ты дружелюбный ассистент-разработчик. Отвечай кратко."),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}"),
])

chain = prompt | model


def get_persistent_session_history(session_id: str) -> SQLChatMessageHistory:
    return SQLChatMessageHistory(
        session_id=session_id,
        connection="sqlite:///chat_history.db",  # путь к файлу БД
    )


chain_with_history = RunnableWithMessageHistory(
    chain,
    get_persistent_session_history,
    input_messages_key="input",
    history_messages_key="history",
)

Интерфейс не изменился, функция get_session_history теперь возвращает объект, который читает и пишет в SQLite. После перезапуска история сохраняется.


Распространенные ошибки

1. Ключ history_messages_key не совпадает с MessagesPlaceholder

# Ошибка: в промпте variable_name="history", но в обертке history_messages_key="chat_history"
MessagesPlaceholder(variable_name="history")
RunnableWithMessageHistory(..., history_messages_key="chat_history")  # не совпадает!

Решение: убедитесь, что variable_name в MessagesPlaceholder и history_messages_key в RunnableWithMessageHistory одна и та же строка.

2. Нет MessagesPlaceholder в промпте

KeyError: 'history'

Причина: в промпте нет места для подстановки истории.

Решение: добавьте MessagesPlaceholder(variable_name="history") между сообщениями system и human.

3. Один store на всё приложение

store = {}  # глобальный словарь — ок для разработки

В продакшн это не масштабируется. Используйте Redis, PostgreSQL или другой персистентный бэкенд.

4. Рост истории без ограничений

Симптом: со временем запросы становятся медленнее и дороже.

Решение: добавьте trim_messages в цепочку или ограничь количество хранимых сообщений в get_session_history.


Рабочий пример кода

Пример демонстрирует все концепции урока: RunnableWithMessageHistory, изоляцию сессий, ограничение истории и персистентное хранение в SQLite.

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.messages import trim_messages
from langchain_core.runnables import RunnablePassthrough
from langchain_community.chat_message_histories import SQLChatMessageHistory

load_dotenv()

model = ChatOpenAI(model=os.getenv("MODEL_NAME", "gpt-4o"), temperature=0)

# Ограничение истории — не более 20 сообщений
trimmer = trim_messages(
    max_tokens=20,
    strategy="last",
    token_counter=len,
    include_system=True,
    allow_partial=False,
    start_on="human",
)

prompt = ChatPromptTemplate.from_messages([
    ("system", "Ты ассистент-разработчик. Отвечай кратко и по делу."),
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}"),
])

chain = (
    RunnablePassthrough.assign(history=lambda x: trimmer.invoke(x["history"]))
    | prompt
    | model
)

# История сохраняется в SQLite — не стирается после перезапуска
def get_session_history(session_id: str) -> SQLChatMessageHistory:
    return SQLChatMessageHistory(
        session_id=session_id,
        connection="sqlite:///chat_history.db",
    )

chain_with_history = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="history",
)

def chat(session_id: str, message: str) -> str:
    response = chain_with_history.invoke(
        {"input": message},
        config={"configurable": {"session_id": session_id}},
    )
    return response.content

# Изоляция сессий: alice и bob не видят историю друг друга
print("=== Сессия: alice ===")
print(chat("alice", "Меня зовут Алиса, я пишу на Python."))
print(chat("alice", "Какой язык я упомянула?"))

print("\n=== Сессия: bob ===")
print(chat("bob", "Привет! Я разрабатываю на Go."))
print(chat("bob", "Что я сказал о своём стеке?"))

print("\n=== Алиса снова ===")
print(chat("alice", "Посоветуй библиотеку для HTTP-запросов на моём языке."))
# Модель помнит, что Алиса пишет на Python → посоветует requests/httpx

История сохраняется в chat_history.db. При повторном запуске сессии alice и bob продолжатся с того же места.


Практическое задание

Создайте консольный чат бот с памятью и двумя режимами работы.

Требования:

1) Бот должен поддерживать два режима, задаваемых при старте:

  • "tutor" - терпеливый преподаватель Python, объясняет концепции с примерами
  • "reviewer" - строгий code reviewer, указывает на проблемы и предлагает улучшения

2) Реализуйте хранение истории с помощью RunnableWithMessageHistory с изоляцией по session_id

3) Добавьте команду /history - выводит количество сообщений в текущей сессии

4) Добавьте команду /clear - очищает историю текущей сессии

5) Ограничьте историю: не более 20 сообщений (используй trim_messages или обрезку в get_session_history)

Пример сессии:

Режим (tutor/reviewer): tutor
Session ID: user_42

Вы: Объясни list comprehension
Бот: List comprehension — это краткий способ создать список...

Вы: Покажи пример с условием
Бот: Конечно, вот пример с фильтрацией... (помнит контекст)

Вы: /history
История: 4 сообщения

Вы: /clear
История очищена.

Вы: Что я спрашивал раньше?
Бот: Ты ещё ничего не спрашивал — история пуста.

Итоги уроком

Сейчас цепочки обрабатывают данные линейно: вход → обработка → выход. Но многие задачи требуют ветвления, выбрать стратегию в зависимости от контекста, запустить несколько шагов параллельно, объединить результаты. В следующем уроке разберем RunnableParallel, RunnableBranch и RunnableLambda - это инструменты для построения нелинейных пайплайнов.

<< Урок 3

Урок 5 >>


Подписывайтесь на мой Telegram канал

Если вам нужен ментор и вы хотите научиться разрабатывать AI агентов, пишите, обсудим условия

Авторизуйтесь, чтобы оставить комментарий.

Комментариев: 0

Нет комментариев.

Тут может быть ваша реклама

Пишите info@aisferaic.ru

Похожие туториалы