0Pricing
AI Engineering Academy · Урок

Память буфера и окна

Реализуйте ConversationBufferMemory и ConversationBufferWindowMemory, чтобы сохранять в контексте последние N реплик, и измерьте влияние размера окна на связность и стоимость.

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

Память буфера: сохраняем всё

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

from langchain.memory import ConversationBufferMemory

memory = ConversationBufferMemory(
    return_messages=True,  # return Message objects, not a string
    memory_key='history'   # key to inject into prompt template
)

# Add messages manually
memory.chat_memory.add_user_message('What is a neural network?')
memory.chat_memory.add_ai_message('A neural network is a system of layers...')

# Load what will be injected
print(memory.load_memory_variables({}))

Интеграция памяти буфера с цепочкой

Чтобы использовать память буфера с LCEL, подключите её через RunnableWithMessageHistory или используйте ConversationChain в устаревшем варианте. Объект памяти хранит историю, а цепочка использует MessagesPlaceholder в шаблоне запроса, чтобы вставить её. После каждого вызова память автоматически добавляет новый обмен сообщениями.

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

store = {}

def get_history(session_id: str):
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]

chain = (
    ChatPromptTemplate.from_messages([
        ('system', 'You are helpful.'),
        MessagesPlaceholder('history'),
        ('human', '{input}')
    ])
    | ChatOpenAI(model='gpt-4o-mini')
    | StrOutputParser()
)

with_memory = RunnableWithMessageHistory(
    chain, get_history,
    input_messages_key='input',
    history_messages_key='history'
)

Проблема неограниченного буфера

Память буфера работает, пока разговор не становится слишком длинным и не превышает окно контекста модели. При окне контекста 128K у GPT-4o-mini обычный чат может продлиться на 200–400 обменов, прежде чем произойдёт переполнение. На практике даже при 50 обменах Вы отправляете более 50K токенов в каждом запросе — это значительные расходы. Вам нужна стратегия, которая ограничивает размер истории.

import tiktoken

enc = tiktoken.encoding_for_model('gpt-4o-mini')

def count_history_tokens(messages: list) -> int:
    total = 0
    for msg in messages:
        total += len(enc.encode(msg.content))
        total += 4  # per-message overhead
    return total

# Check how big the history has grown
history = store.get('session-1')
if history:
    token_count = count_history_tokens(history.messages)
    print(f'History size: {len(history.messages)} messages, {token_count} tokens')

Память окна: сохраняем последние N обменов

Память окна сохраняет только последние K обменов в разговоре, отбрасывая более старые сообщения. Это ограничивает контекст значением K * avg_tokens_per_turn независимо от продолжительности разговора. Компромисс заключается в потере самого раннего контекста — модель может забыть то, что пользователь сказал много обменов назад. Для большинства чат-ботов общего назначения окно в 5–10 обменов обеспечивает хорошую связность при приемлемых расходах.

from langchain.memory import ConversationBufferWindowMemory

# Keep last 5 turns (10 messages: 5 user + 5 assistant)
memory = ConversationBufferWindowMemory(
    k=5,              # number of TURNS to keep (each turn = user + AI)
    return_messages=True,
    memory_key='history'
)

# After 10 turns, only turns 6-10 will be in context
# Turns 1-5 are silently dropped

Реализация памяти окна с помощью InMemoryChatMessageHistory

InMemoryChatMessageHistory в LangChain сохраняет все сообщения, но перед вставкой истории в запрос Вы можете сократить её. Распространённый подход — хранить полную историю для ведения журнала, но передавать LLM только последние N сообщений. Используйте обозначение среза Python для history.messages, чтобы получить самое новое окно.

from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables import RunnableLambda

WINDOW_SIZE = 10  # last 10 messages (5 turns)

def get_windowed_history(session_id: str):
    full_history = store.get(session_id, InMemoryChatMessageHistory())
    store[session_id] = full_history
    return full_history

# In the chain, trim before injecting
def trim_history(messages):
    return messages[-WINDOW_SIZE:] if len(messages) > WINDOW_SIZE else messages

# Use in prompt with trimming
from langchain_core.messages import trim_messages
trimmer = trim_messages(
    max_tokens=2000,
    strategy='last',
    token_counter=ChatOpenAI(model='gpt-4o-mini'),
    include_system=True
)

Окно по токенам и по обменам

Размер окна можно задавать в обменах (последние K пар сообщений пользователя и ИИ) или в токенах (последние T токенов истории). Окно по токенам надёжнее, поскольку длина обменов различается: обмен с кодом в 10 раз длиннее обмена со словом «да». Утилита LangChain trim_messages() поддерживает обе стратегии и может сохранять системный запрос при сокращении истории.

from langchain_core.messages import trim_messages, SystemMessage, HumanMessage, AIMessage

# Token-based trimming — keep last 1000 tokens of conversation
trimmer = trim_messages(
    max_tokens=1000,
    strategy='last',       # keep most recent messages
    token_counter=len,     # approximate: count characters / 4
    include_system=True,   # always include the system prompt
    allow_partial=False,   # don't split a message in half
    start_on='human'       # start window on a human message
)

trimmed = trimmer.invoke(all_messages)
print(f'Trimmed to {len(trimmed)} messages')

Измерение связности в зависимости от размера окна

Чтобы выбрать подходящий размер окна, нужно измерить, как ухудшается связность разговора при его уменьшении. Проведите набор тестовых разговоров, в которых обмен N ссылается на информацию из обменов N-5, N-10 и N-20. Проверьте, может ли модель по-прежнему правильно отвечать при окнах в 5, 10 и 20 обменов. Ответ покажет минимальный размер окна, необходимый для Вашего конкретного сценария.

def test_reference_at_distance(chain_with_memory, distances=[5, 10, 20]):
    results = {}
    for distance in distances:
        session_id = f'test-dist-{distance}'
        # Fill with filler turns
        for i in range(distance):
            chain_with_memory.invoke(
                {'input': f'Turn {i}: filler message'},
                config={'configurable': {'session_id': session_id}}
            )
        # Ask about something said at the start
        first_msg = 'Recall that the user said the magic word is AZURE.'
        response = chain_with_memory.invoke(
            {'input': 'What was the magic word?'},
            config={'configurable': {'session_id': session_id}}
        )
        results[distance] = 'AZURE' in response.upper()
    return results

Объединение системного запроса с памятью окна

При использовании памяти окна всегда сохраняйте системный запрос — он задаёт образ ИИ и правила. Без него модель может потерять заданный образ, когда окно переместится за пределы первого обмена. Используйте параметр include_system=True в функциях сокращения либо всегда вставляйте системное сообщение перед историей окна в шаблоне запроса.

# Always put system prompt BEFORE the windowed history
prompt = ChatPromptTemplate.from_messages([
    ('system', 'You are a Python tutor. Always explain with code examples.'),
    MessagesPlaceholder('history'),  # windowed history injected here
    ('human', '{input}'),
])

# The system prompt is never trimmed — only the history window is managed
# This ensures the model's persona is always present regardless of window

Как выбрать между памятью буфера и памятью окна

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

# Decision matrix in code
def choose_memory_strategy(
    expected_turns: int,
    max_context_tokens: int = 128000,
    avg_tokens_per_turn: int = 200
) -> str:
    buffer_tokens = expected_turns * avg_tokens_per_turn
    if buffer_tokens < max_context_tokens * 0.5:
        return 'buffer'  # Safe to keep everything
    elif expected_turns <= 20:
        return 'window_10'  # Keep last 10 turns
    else:
        return 'summary'  # Need summarization for long convos

print(choose_memory_strategy(5))    # 'buffer'
print(choose_memory_strategy(50))   # 'window_10'
print(choose_memory_strategy(200))  # 'summary'

Сохранение окна в Redis

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

from langchain_community.chat_message_histories import RedisChatMessageHistory

def get_windowed_redis_history(session_id: str, window: int = 10):
    # RedisChatMessageHistory stores all messages
    history = RedisChatMessageHistory(
        session_id=session_id,
        url='redis://localhost:6379',
        ttl=3600  # 1 hour TTL
    )
    # Trim to window size in-memory before use
    all_msgs = history.messages
    if len(all_msgs) > window * 2:  # window turns = window*2 messages
        history.messages = all_msgs[-(window * 2):]
    return history

Мониторинг состояния памяти в рабочем окружении

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

import time
from langchain_core.callbacks import BaseCallbackHandler

class MemoryMonitorCallback(BaseCallbackHandler):
    def on_chain_start(self, serialized, inputs, **kwargs):
        history = inputs.get('history', [])
        token_estimate = sum(len(m.content.split()) * 1.3 for m in history)
        if token_estimate > 50000:
            print(f'WARNING: Large history {token_estimate:.0f} estimated tokens')

    def on_chain_end(self, outputs, **kwargs):
        # Log usage for dashboards
        pass

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

Проверьте, насколько Вы понимаете стратегии памяти буфера и окна.

Итоги урока

В этом уроке Вы узнали, что память буфера сохраняет полную историю, но растёт без ограничений, поэтому подходит только для коротких ограниченных разговоров; память окна сохраняет только последние K обменов, обеспечивая предсказуемые ограниченные расходы за счёт потери далёкого контекста; а сокращение по токенам с помощью trim_messages() надёжнее окон по обменам, поскольку длина сообщений различается. Далее мы рассмотрим память сжатого содержания для длинных разговоров без заранее заданного конца.

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

Урок «Память буфера и окна» бесплатный?

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

Чему я научусь в уроке «Память буфера и окна»?

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

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

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

Сколько времени занимает урок «Память буфера и окна»?

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

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

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

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

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