Зачем бессостояниным LLM нужна внешняя память
Поймите, почему каждый вызов API начинается с нуля, как наивное добавление всего контекста приводит к резкому превышению лимита токенов и какие существуют стратегии памяти — от простых до сложных.
«Зачем бессостояниным 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 — локальная установка не требуется.
Все уроки этого курса
- Зачем бессостояниным LLM нужна внешняя память
- Память буфера и окна
- Память сводки и усечение с учётом токенов
- Сохранение истории чата в Redis и PostgreSQL