English translation is not available yet. Showing Russian content.

Как вы управляете контекстным окном (context window) для длинных диалогов?

Краткий тезис

Управление контекстным окном — это набор стратегий, позволяющих удерживать полезную информацию из длинной истории диалога, не превышая лимиты токенов LLM. Основные подходы: sliding window (храним последние N токенов), summarization (сжимаем историю в саммари), selective pruning (удаляем менее важные части) и semantic compression (сокращаем сообщения через специальные промпты). На практике для production-систем лучше всего работает комбинация: суммаризация каждые 10 сообщений с сохранением последних нескольких реплик в исходном виде.


1. Термин: Контекстное окно (context window)

Что это Максимальное количество токенов, которое LLM может обработать за один вызов (например, 8192 для GPT-4, 128k для Claude 3, 1M для Gemini 1.5 Pro). В диалогах каждое новое сообщение пользователя и ответ ассистента добавляют токены в историю, что может привести к превышению лимита.

Почему это важно для production

  • Если контекстное окно переполняется, LLM либо обрезает историю (обычно с середины — проблема lost in the middle), либо выпадает в ошибку.
  • Без управления окном длинные диалоги теряют связность: модель забывает, что было сказано 20 реплик назад.
  • Оптимизация контекста напрямую влияет на латентность (больше токенов → дольше генерация) и стоимость (токены оплачиваются).

2. Sliding window (скользящее окно)

Идея хранить только последние N токенов (или последние K сообщений) диалога. Всё, что раньше, отбрасывается.

Реализация

MAX_TOKENS = 4096
messages = []

def add_message(user_msg: str, assistant_msg: str):
    messages.append({"role": "user", "content": user_msg})
    messages.append({"role": "assistant", "content": assistant_msg})
    # подсчёт токенов (упрощённо — по длине строки)
    total_tokens = sum(len(m["content"]) for m in messages)
    while total_tokens > MAX_TOKENS:
        removed = messages.pop(0)  # удаляем самое старое сообщение
        total_tokens -= len(removed["content"])

Плюсы

  • Простота реализации.
  • Никаких дополнительных вызовов LLM.
  • Низкая задержка.

Минусы

  • Полная потеря раннего контекста.
  • Если важная информация была в начале диалога, она исчезает навсегда.

3. Summarization (суммаризация)

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

Пример промпта для суммаризации

Ты — ассистент, который сжимает историю диалога в краткое саммари.
Сохрани все ключевые факты, предпочтения пользователя, принятые решения.
Формат: одно-два предложения.
История: {old_messages}
Саммари:

Схема работы

  1. Держим два буфера: long-term (саммари) и short-term (последние L сообщений).
  2. При добавлении каждого сообщения проверяем количество токенов.
  3. Если превышен порог — запускаем суммаризацию буфера short-term (или объединяем со старым саммари), записываем новое саммари в long-term, а short-term очищаем.
  4. В итоговый контекст LLM отправляем: [long-term summary] + [последние N сообщений из short-term].

Плюсы

  • Сохраняется суть всего диалога.
  • Гибкость — можно настраивать частоту суммаризации.

Минусы

  • Дополнительные вызовы LLM (стоимость, задержка).
  • Возможна потеря деталей при сжатии.

4. Selective pruning (выборочная обрезка)

Идея не отбрасывать всё подряд, а удалять только те части диалога, которые LLM считает неважными. LLM оценивает каждое сообщение по важности (relevance) и удаляет наименее важные, когда приближаемся к лимиту.

Реализация через промпт

Оцени важность следующих сообщений по шкале 1-5 (5 — критически важно).
Ответ в формате JSON: [{"index": 0, "importance": 4}, ...]
Сообщения: {list_of_messages}

Затем сортируем по важности, оставляем только верхние K сообщений до заполнения контекста.

Плюсы

  • Более умная стратегия, чем sliding window.
  • Может сохранять неожиданные, но важные детали.

Минусы

  • Дополнительный вызов LLM (задержка, стоимость).
  • Сложнее в отладке — решение LLM может быть нестабильным.

5. Semantic compression (семантическое сжатие)

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

Пример промпта

Сократи следующий текст до 30% исходной длины, сохранив все ключевые факты.
Текст: {message}
Сжатый вариант:

Плюсы

  • Позволяет вместить больше реплик в то же окно.
  • Сохраняет структуру диалога (каждое сообщение остаётся отдельным).

Минусы

  • Каждое сообщение требует вызова LLM (дорого).
  • Может исказить тон или нюансы.

6. Сравнение методов

МетодСохранение раннего контекстаЗатраты (LLM вызовы)СложностьТипичный use-case
Sliding windowНетНетНизкаяКороткие диалоги (< 20 реплик)
SummarizationЧастичноеОдин раз каждые N сообщенийСредняяДлинные диалоги с повторяющимися темами
Selective pruningВыборочноеПри каждой обрезкеВысокаяСложные диалоги с разной важностью реплик
Semantic compressionДа (сжато)На каждое сообщениеВысокаяКогда нужно уместить максимум реплик в окно

7. Инструменты и библиотеки

LangChain предоставляет готовые реализации:

  • ConversationBufferWindowMemory — sliding window.
  • ConversationSummaryMemory — суммаризация (каждые N сообщений).
  • ConversationSummaryBufferMemory — гибрид: сохраняет последние сообщения и саммари всей истории.
  • ConversationTokenBufferMemory — обрезка по токенам, не по количеству сообщений.

Пример использования ConversationSummaryBufferMemory:

from langchain.memory import ConversationSummaryBufferMemory
from langchain.chat_models import ChatOpenAI

llm = ChatOpenAI(model="gpt-4", temperature=0)
memory = ConversationSummaryBufferMemory(
    llm=llm,
    max_token_limit=2000,   # триггер для суммаризации
    return_messages=True,
    memory_key="chat_history"
)

# В цикле диалога:
memory.save_context({"input": user_msg}, {"output": assistant_msg})
history = memory.load_memory_variables({})["chat_history"]

Другие инструменты

  • MemGPT (специализированная система управления памятью для LLM).
  • Microsoft Guidance (позволяет писать кастомные pipeline сжатия).
  • LlamaIndex (memory module для чат-ботов).

8. Практические рекомендации для production

На основе моего опыта, суммаризация каждые 10 сообщений работает лучше всего для большинства кейсов. Почему:

  • Не создаёт излишней нагрузки (один вызов LLM на 10 реплик).
  • Сохраняет достаточно контекста для связности.
  • В комбинации с сохранением последних 3–5 сообщений в исходном виде даёт хороший баланс между полнотой и стоимостью.

Пошаговый алгоритм для production

  1. Использовать ConversationSummaryBufferMemory из LangChain.
  2. Установить max_token_limit равным 50% от контекстного окна модели (например, 4096 для 8k).
  3. Дополнительно хранить последние 5 сообщений без сжатия.
  4. Для чувствительных к стоимости сценариев — переключиться на ConversationBufferWindowMemory с большим окном, если диалоги редко превышают 20–30 реплик.
  5. Мониторить: logs с метриками (количество токенов, латентность суммаризации, оценка релевантности саммари).

Метрики оценки

  • Coherence score — оценка связности ответов (через LLM-as-judge).
  • Recall важных фактов — доля фактов из ранних сообщений, которые модель использует в ответах.
  • Cost per conversation — средняя стоимость вызовов LLM на суммаризацию.

9. Пример кода: гибридный подход (sliding + summarization)

import json
from langchain.memory import ConversationSummaryBufferMemory
from langchain.chat_models import ChatOpenAI
from langchain.schema import SystemMessage, HumanMessage, AIMessage

class HybridContextManager:
    def __init__(self, llm, max_token_limit=3000, keep_last_raw=5):
        self.memory = ConversationSummaryBufferMemory(
            llm=llm,
            max_token_limit=max_token_limit,
            return_messages=True,
            memory_key="chat_history"
        )
        self.keep_last_raw = keep_last_raw
        self.raw_buffer = []

    def add_message(self, user_msg: str, assistant_msg: str):
        self.raw_buffer.append(HumanMessage(content=user_msg))
        self.raw_buffer.append(AIMessage(content=assistant_msg))
        # Сохраняем в memory (но там будет суммаризация)
        self.memory.save_context({"input": user_msg}, {"output": assistant_msg})
        # Обрезаем raw_buffer до keep_last_raw
        self.raw_buffer = self.raw_buffer[-self.keep_last_raw:]

    def build_prompt(self, system_prompt: str) -> list:
        history = self.memory.load_memory_variables({})["chat_history"]
        # В LangChain history уже содержит саммари + последние сообщения
        # Но мы можем добавить raw_buffer поверх для свежести
        return [SystemMessage(content=system_prompt)] + list(history) + self.raw_buffer

llm = ChatOpenAI(model="gpt-4", temperature=0)
manager = HybridContextManager(llm)

10. Продвинутые техники

  • Hierarchical summarization (иерархическая суммаризация) — сжимаем диалог на нескольких уровнях: сначала микро-саммари для групп сообщений, затем мега-саммари для всего дня.
  • Token-aware caching — кэшируем K, V матрицы для старых частей контекста (подход, используемый в KV-cache при инференсе, но для диалогов — можно хранить вычисленные ключи/значения для суммаризированного контекста).
  • Adaptive threshold — динамический порог для суммаризации в зависимости от темы диалога (например, если пользователь меняет тему — сразу сжимать старую).

Пет-проект для закрепления

Задача Создать чат-бота для поддержки клиентов, который может вести диалоги длиной до 200 сообщений, не превышая лимит в 4096 токенов и не теряя важные детали.

Инструменты Python, LangChain, OpenAI API (или любая LLM), база данных SQLite для логирования.

Шаги:

  1. Реализовать класс ContextManager с двумя режимами: sliding window (окно 50 сообщений) и summarization (каждые 10 сообщений).
  2. Создать простой веб-интерфейс (Gradio или Streamlit) для симуляции диалога.
  3. Добавить кнопку "Evaluate" — LLM-as-judge оценивает, насколько хорошо бот помнит факты из начала диалога (например, имя пользователя, его проблему).
  4. Сравнить метрики для двух режимов (coherence, recall, cost).
  5. Написать отчет: какой режим лучше для вашего сценария.

Ожидаемый результат Работающий прототип с выводом статистики (токены, вызовы LLM, оценка памяти). Вы сможете на собеседовании показать код и объяснить выбор стратегии.

Связь с другими вопросами

ВопросТема
65Как вы обрабатываете запросы, требующие многошаговых рассуждений (multi-step reasoning)?
67Как вы работаете с очень большими контекстами (например, 100k токенов)?
62Как вы управляете памятью в чат-ботах?
64Как вы предотвращаете перезапись важной информации в контексте?
68Как вы используете кэширование для ускорения диалогов?
70Как вы оцениваете качество диалогового AI?

Навигация