AI Engineering Academy · Урок

Зачем бессостояниным LLM нужна внешняя память

Поймите, почему каждый вызов API начинается с нуля, как наивное добавление всего контекста приводит к резкому превышению лимита токенов и какие существуют стратегии памяти — от простых до сложных.

Урок 1 из 413 шагов

«Зачем бессостояниным LLM нужна внешняя память» — бесплатный урок AI Engineering Academy на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Engineering Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Engineering Academy содержит 4 уроков всего.

По умолчанию у LLM нет памяти

Каждый вызов API к LLM является полностью независимым. Модель получает сообщения, отправленные в этом единственном запросе, и обрабатывает их — не более того. При следующем вызове модель не помнит предыдущий разговор. Это похоже на разговор с человеком, у которого нет кратковременной памяти. Такое отсутствие состояния задумано специально: оно упрощает масштабирование и развёртывание моделей, но переносит задачу хранения памяти на Ваше приложение.

Проблема амнезии на практике

Без памяти чат-бот не справится с простыми многоходовыми задачами. Если пользователь на первом ходу говорит «Меня зовут Алиса», а на третьем спрашивает «Как меня зовут?», модель ответит, что не знает. Каждый ход выглядит как новый разговор. Пользователей это крайне раздражает. Решение заключается в том, чтобы приложение хранило историю диалога и включало её в каждый вызов API.

# Naive stateless approach — model forgets everything
from openai import OpenAI

client = OpenAI()

def chat_stateless(user_message: str) -> str:
    response = client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[  # Only the new message — no history!
            {'role': 'user', 'content': user_message}
        ]
    )
    return response.choices[0].message.content

chat_stateless('My name is Alice.')   # Model: 'Hello Alice!'
chat_stateless('What is my name?')    # Model: 'I do not know your name.'

Наивное решение: добавление всей истории

Самый простой подход к организации памяти — добавлять каждый ход в растущий список и отправлять весь список с каждым запросом. Это работает, но у такого подхода есть серьёзный недостаток: окно контекста имеет конечный размер. Окно в 128K токенов кажется большим, однако длинный чат со службой поддержки с фрагментами кода может исчерпать его за несколько минут. Отправка всей истории также означает, что за одни и те же токены приходится снова и снова платить.

messages = []  # grows with every turn

def chat_with_full_history(user_message: str) -> str:
    messages.append({'role': 'user', 'content': user_message})
    response = client.chat.completions.create(
        model='gpt-4o-mini',
        messages=messages  # all history every time
    )
    reply = response.choices[0].message.content
    messages.append({'role': 'assistant', 'content': reply})
    return reply

# Works at first, but messages list grows unboundedly
# After 100 turns: could be 50,000+ tokens per request

Пространство решений для памяти

Стратегии управления памятью образуют спектр компромиссов между полнотой контекста (какая часть истории сохраняется) и стоимостью токенов (сколько токенов используется на запрос). От самых простых к наиболее сложным стратегии таковы: память с полным буфером, память с раздвижным окном, память с кратким изложением, память сущностей и эпизодическая память на основе векторов. Правильный выбор зависит от длительности диалога и доступного бюджета.

Стоимость токенов истории диалога

Стоимость каждого вызова API зависит от общей длины всех сообщений — как входных данных (Вашего массива сообщений), так и вывода (ответа модели). Если включать полную историю в каждый запрос, стоимость токенов растёт квадратично с увеличением длины диалога: на ходу N отправляются N предыдущих сообщений. В диалоге из 50 ходов по 200 токенов каждый суммарно отправляется 200+400+600+...+10 000, то есть более 250 000 входных токенов.

import tiktoken

def estimate_conversation_cost(
    turns: int,
    tokens_per_turn: int,
    price_per_1k_input: float = 0.00015  # gpt-4o-mini
) -> float:
    total_input_tokens = sum(
        (i + 1) * tokens_per_turn  # each turn sends all previous turns
        for i in range(turns)
    )
    total_output_tokens = turns * tokens_per_turn
    cost = (total_input_tokens / 1000) * price_per_1k_input
    print(f'{turns} turns: {total_input_tokens:,} input tokens, ${cost:.4f}')
    return cost

estimate_conversation_cost(50, 200)

Где хранить историю диалога

Историю диалога необходимо хранить вне процесса Python, чтобы она сохранялась после перезапуска сервера и была доступна нескольким экземплярам приложения. К распространённым хранилищам относятся: Redis для быстрого доступа к данным в памяти с истечением срока хранения TTL (отлично подходит для активных сеансов), PostgreSQL для надёжного долгосрочного хранения и аналитики и DynamoDB для автоматического масштабирования в бессерверных приложениях. LangChain из коробки предоставляет соединители для всех этих хранилищ.

# Three storage options for conversation history

# 1. In-process dict (development only — lost on restart)
from langchain_core.chat_history import InMemoryChatMessageHistory

# 2. Redis (production — fast, TTL-based expiry)
from langchain_community.chat_message_histories import RedisChatMessageHistory
history = RedisChatMessageHistory(session_id='user-123', url='redis://localhost:6379')

# 3. PostgreSQL (production — durable, queryable)
from langchain_community.chat_message_histories import PostgresChatMessageHistory
history = PostgresChatMessageHistory(
    session_id='user-123',
    connection_string='postgresql://user:pass@localhost/db'
)

Идентификаторы сеансов: разделение диалогов пользователей

Если приложение обслуживает нескольких пользователей, необходимо поддерживать отдельную историю для каждого диалога. session_id (обычно UUID или сочетание ID пользователя и ID диалога) определяет, какую историю загружать для каждого запроса. RunnableWithMessageHistory в LangChain принимает функцию get_session_history, которая получает ID сеанса и возвращает соответствующий объект истории.

import uuid
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import InMemoryChatMessageHistory

store = {}  # session_id -> history (use Redis in production)

def get_session_history(session_id: str) -> InMemoryChatMessageHistory:
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]

# Create a new session for each user conversation
def new_session() -> str:
    return str(uuid.uuid4())

alice_session = new_session()
bob_session = new_session()
# Alice and Bob's histories are completely independent

Обёртка RunnableWithMessageHistory

RunnableWithMessageHistory — нативный для LCEL способ LangChain добавить память к любой цепочке. Она оборачивает Вашу цепочку, автоматически загружает историю перед каждым вызовом, добавляет новое сообщение пользователя и ответ ИИ, а затем сохраняет всё обратно в хранилище. Вы указываете, какой ключ входных данных содержит сообщение пользователя и какая переменная шаблона должна получить историю.

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.output_parsers import StrOutputParser

prompt = ChatPromptTemplate.from_messages([
    ('system', 'You are a helpful assistant.'),
    MessagesPlaceholder(variable_name='history'),  # history injected here
    ('human', '{input}'),
])

chain = prompt | ChatOpenAI(model='gpt-4o-mini') | StrOutputParser()

chain_with_memory = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key='input',
    history_messages_key='history'
)

Вызов цепочки с поддержкой памяти

При вызове цепочки, обёрнутой в RunnableWithMessageHistory, передайте словарь config с параметром configurable: {session_id: ...}. Это сообщает обёртке, какое хранилище истории нужно загрузить. Всё остальное цепочка выполняет сама: загружает историю перед вызовом, вставляет её в шаблон запроса и сохраняет новый обмен сообщениями после получения ответа.

session_id = 'user-alice-session-1'

# First message
response1 = chain_with_memory.invoke(
    {'input': 'My name is Alice and I love Python.'},
    config={'configurable': {'session_id': session_id}}
)
print(response1)  # 'Hello Alice! Nice to meet you.'

# Second message — model now knows Alice's name and interest
response2 = chain_with_memory.invoke(
    {'input': 'What is my name and what do I love?'},
    config={'configurable': {'session_id': session_id}}
)
print(response2)  # 'Your name is Alice and you love Python!'

Ошибки реализации памяти, которых следует избегать

Три распространённые ошибки при реализации памяти: отсутствие изоляции сеансов — повторное использование одного объекта истории для всех пользователей раскрывает личные данные между разговорами. Игнорирование TTL — бессрочное хранение истории переполняет базу данных; задавайте срок действия для неактивных сеансов. Чрезмерное доверие к истории — пользователи могут внедрить ложные воспоминания («Я говорил Вам, что я администратор»), поэтому проверяйте утверждения по достоверному источнику, а не только по истории разговора.

# Mistake 1: Shared history for all users
global_history = InMemoryChatMessageHistory()  # BAD!

# Fix: per-session history
store = {}  # keyed by session_id

# Mistake 2: No TTL on Redis history
# BAD: history = RedisChatMessageHistory(session_id=sid, url=url)
# Good: set TTL to 24 hours
history = RedisChatMessageHistory(
    session_id=session_id,
    url='redis://localhost',
    ttl=86400  # 24 hours in seconds
)

Выбор стратегии памяти

Используйте память полного буфера только для коротких разговоров, общий контекст которых гарантированно останется в пределах ограничений. Используйте скользящее окно для обычных чат-ботов (сохраняйте последние N обменов сообщениями). Используйте память сжатого содержания, если разговоры не ограничены по длине и могут стать очень длинными. Используйте векторную память, когда пользователям нужно вспоминать конкретные факты из значительно более ранней части длинного разговора. В следующих уроках мы рассмотрим каждую из этих стратегий.

Быстрая проверка

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

Итоги урока

В этом уроке Вы узнали, что LLM по своей конструкции не сохраняют состояние — каждый вызов API видит только те данные, которые Вы отправляете в этом запросе; наивная передача всей истории увеличивает расходы на токены по квадратичному закону и в конечном итоге достигает ограничений контекста; а RunnableWithMessageHistory — это удобный способ LangChain добавить внешнюю память с любым хранилищем (Redis, PostgreSQL), используя идентификатор сеанса в качестве ключа. Далее мы рассмотрим конкретные стратегии памяти: память буфера и память окна.

Можно начать бесплатно

Изучай Python с ИИ-репетитором — бесплатно

Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.

Курсы
30
Уроки
120

Часто задаваемые вопросы

Урок «Зачем бессостояниным LLM нужна внешняя память» бесплатный?

Да — полный текст урока «Зачем бессостояниным LLM нужна внешняя память» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Engineering Academy, подпишись на CoddyKit PRO. Курс AI Engineering Academy содержит 4 уроков всего.

Чему я научусь в уроке «Зачем бессостояниным LLM нужна внешняя память»?

Поймите, почему каждый вызов API начинается с нуля, как наивное добавление всего контекста приводит к резкому превышению лимита токенов и какие существуют стратегии памяти — от простых до сложных. Ты практикуешь AI Engineering Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать AI Engineering Academy?

Предыдущий опыт не требуется. AI Engineering Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.

Сколько времени занимает урок «Зачем бессостояниным LLM нужна внешняя память»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке AI Engineering Academy?

Да. Каждый урок AI Engineering Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Зачем бессостояниным LLM нужна внешняя память
  2. Память буфера и окна
  3. Память сводки и усечение с учётом токенов
  4. Сохранение истории чата в Redis и PostgreSQL
← Назад к AI Engineering Academy