Суммаризация и компрессия контекста без потери смысла

30 минут Урок 20

Урок 4: Суммаризация и компрессия контекста без потери смысла

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

В этом уроке мы разберем, как превратить гигабайты текста в компактные смысловые единицы, не потеряв при этом суть. Мы отойдем от примитивного «обрезания» старых сообщений и перейдем к интеллектуальной компрессии.

Почему контекст — это ресурс, а не просто лимит?

Долгое время разработчики боролись с жесткими ограничениями (4k, 8k токенов). С появлением Gemini 1.5 и грядущих версий, проблема «нехватки места» сменилась проблемой «шума в сигнале».

  • Стоимость (Cost): Каждый токен входного контекста стоит денег. Передавать 500 страниц документации в каждом запросе чат-бота — экономическое самоубийство.
  • Задержка (Latency): Чем больше контекст, тем дольше модель его обрабатывает до начала генерации первого токена (TTFT — Time To First Token).
  • Точность (Accuracy): Существует феномен «Lost in the Middle». Даже мощные модели могут упускать детали, находящиеся в середине огромного массива данных, уделяя больше внимания началу и концу промпта.

Наша цель — научиться сжимать информацию так, чтобы для модели она оставалась столь же информативной, как и исходник, но занимала в 10–100 раз меньше токенов.

Стратегии компрессии: От простого к сложному

Рассмотрим основные подходы, которые применяются в профессиональной разработке.

1. Скользящее окно (Rolling Window)

Самый примитивный метод. Мы храним только последние N сообщений.
Минус: Катастрофическая забывчивость. Если пользователь упомянул свое имя в начале диалога, а сейчас идет 50-е сообщение, бот его забудет.

2. Суммаризация предыдущего контекста

Когда контекст заполняется, мы просим саму модель: «Сократи этот диалог до одного абзаца, сохранив ключевые факты». Затем этот абзац подается как системное сообщение для следующего этапа.

3. Структурированная экстракция (Entity Extraction)

Вместо пересказа текста мы извлекаем факты в JSON. Например, мы не храним текст «Меня зовут Иван, я живу в Москве», мы храним объект {"name": "Ivan", "city": "Moscow"}. Это наиболее экономный способ.

4. Итеративное уплотнение (Chain of Density)

Продвинутая техника, предложенная исследователями из MIT и Salesforce. Идея заключается в том, чтобы генерировать саммари, а затем просить модель добавить пропущенные сущности в то же количество слов, повышая плотность информации шаг за шагом.

python
import google.generativeai as genai
from dataclasses import dataclass
from typing import List, Dict

# Инициализация модели (предполагаем наличие API ключа в переменных среды)
model = genai.GenerativeModel('gemini-1.5-pro-latest')

@dataclass
class Message:
    role: str
    content: str

def naive_summarization(history: List[Message]) -> str:
    """
    Базовый подход: просим модель сжать историю в один нарратив.
    """
    conversation_text = "\n".join([f"{msg.role}: {msg.content}" for msg in history])
    
    prompt = f"""
    Проанализируй следующий диалог между пользователем и AI.
    Создай краткое резюме (summary), которое содержит все ключевые факты, 
    принятые решения и контекст, необходимый для продолжения разговора.
    Игнорируй приветствия и светскую беседу.
    
    ДИАЛОГ:
    {conversation_text}
    
    РЕЗЮМЕ:
    """
    
    response = model.generate_content(prompt)
    return response.text

# Пример использования
history = [
    Message("user", "Привет, я хочу написать код на Python для анализа данных."),
    Message("model", "Привет! Отличный выбор. Какие библиотеки планируешь использовать? Pandas, NumPy?"),
    Message("user", "Да, Pandas точно. И мне нужно загружать данные из CSV файлов весом по 5 гигабайт."),
    Message("model", "Для таких объемов чистый Pandas может быть медленным. Рассмотри использование Dask или Polars."),
    Message("user", "О, Polars звучит интересно. Давай попробуем его.")
]

summary = naive_summarization(history)
print(f"Сжатый контекст:\n{summary}")

Техника «Chain of Density» (Цепочка плотности)

Давайте углубимся в более сложный, но крайне эффективный метод. Проблема обычной суммаризации в том, что модели любят «воду». Они часто пишут общие фразы вроде «Пользователь и Ассистент обсудили библиотеки Python», упуская, что именно Polars был выбран из-за файлов в 5 ГБ.

Метод Chain of Density (CoD) заставляет модель циклически переписывать саммари, добавляя всё больше сущностей (entities) без увеличения длины текста. Это создает сверхплотный контекст, идеальный для системного промпта.

Алгоритм CoD:

  1. Сгенерируй начальное саммари.
  2. Выдели ключевые сущности из исходного текста, которые пропущены в саммари.
  3. Перепиши саммари той же длины, включив эти пропущенные сущности.
  4. Повтори N раз.

python
def chain_of_density_summarization(text_to_compress: str, iterations: int = 3) -> str:
    """
    Реализация упрощенной логики Chain of Density для Gemini.
    """
    
    cod_prompt = f"""
    Я предоставлю тебе текст разговора. Ты должен сгенерировать все более краткое и информационно плотное резюме (summary).
    
    Повтори следующий процесс {iterations} раза:
    1. Идентифицируй 1-3 ключевые сущности (Entities) из исходного текста, которые отсутствуют в предыдущем резюме.
    2. Напиши НОВОЕ резюме той же длины или короче, которое включает все предыдущие сущности ПЛЮС новые.
    3. Резюме должно быть очень сухим, фактологическим, без лишних слов.
    
    ИСХОДНЫЙ ТЕКСТ:
    {text_to_compress}
    """
    
    response = model.generate_content(cod_prompt)
    return response.text

# В реальной системе мы бы парсили ответ, чтобы взять только финальную итерацию.
# Gemini часто хорошо справляется, если попросить его вывести только финальный результат в конце.

print("Запуск Chain of Density...")
# Представим, что text_to_compress - это длинная стенограмма встречи

Гибридный подход: Краткосрочная и Долгосрочная память

Самая надежная архитектура для сложных ботов (например, AI-менторов или персонажей ролевых игр) использует комбинацию подходов. Это напоминает устройство человеческой памяти.

  • Буфер (Short-term memory): Последние 3-5 сообщений в сыром виде. Это позволяет поддерживать живой стиль общения, реагировать на "ага", "нет", "почему?".
  • Активная суммаризация (Working memory): Сжатый пересказ текущей сессии разговора. Обновляется каждые N сообщений.
  • База знаний (Long-term memory): Векторная база данных (Vector Store), куда мы сохраняем важные факты и извлекаем их через RAG (Retrieval Augmented Generation), когда они становятся релевантными.

Ниже представлен архитектурный паттерн управления таким контекстом.

python
class ContextManager:
    def __init__(self, model, max_tokens=1000):
        self.model = model
        self.raw_buffer = [] # Хранит объекты Message
        self.summary = "" # Текущее сжатое состояние
        self.max_tokens = max_tokens

    def add_message(self, role, content):
        self.raw_buffer.append(Message(role, content))
        # Простая эвристика проверки переполнения (в проде нужно считать токены)
        if len(self.raw_buffer) > 5:
            self._compress_context()

    def _compress_context(self):
        """
        Переносит старые сообщения из буфера в саммари.
        """
        # Берем старые сообщения, оставляя 2 последних для связности
        to_summarize = self.raw_buffer[:-2]
        remaining = self.raw_buffer[-2:]
        
        text_block = "\n".join([f"{m.role}: {m.content}" for m in to_summarize])
        
        prompt = f"""
        Текущее знание о диалоге: {self.summary}
        
        Новые сообщения:
        {text_block}
        
        Обнови текущее знание, интегрировав информацию из новых сообщений.
        Удали устаревшую информацию. Сохрани имена, даты, предпочтения пользователя.
        Вывод должен быть только обновленным текстом саммари.
        """
        
        resp = self.model.generate_content(prompt)
        self.summary = resp.text.strip()
        self.raw_buffer = remaining
        print(f"[System] Контекст сжат. Новое саммари: {self.summary[:50]}...")

    def get_full_context(self) -> str:
        """
        Формирует промпт для модели
        """
        buffer_text = "\n".join([f"{m.role}: {m.content}" for m in self.raw_buffer])
        return f"Контекст прошлого: {self.summary}\n\nТекущий диалог:\n{buffer_text}"

# Имитация работы
manager = ContextManager(model)
manager.add_message("user", "Я живу в Берлине.")
manager.add_message("model", "Прекрасный город! Давно там?")
manager.add_message("user", "Переехал 2 года назад. Работаю инженером.")
# ... добавляем сообщения ...
# При превышении лимита сработает _compress_context

Особенности Gemini 1.5/3 и Context Caching

В экосистеме Google Gemini есть уникальная функция, которая меняет правила игры — Context Caching. Если ваш системный промпт или массив документов огромен (например, целая книга или кодовая база), и вы делаете к нему много запросов, вы можете закешировать этот контекст.

Это означает, что вам не нужно каждый раз передавать и (главное!) оплачивать обработку входных токенов огромного контекста. Вы платите за хранение кеша (что дешевле) и за новые токены запроса.

Когда использовать компрессию, а когда кеширование?

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

Упражнение

Напишите функцию `extract_profile_json(dialogue_history)`, которая принимает список сообщений и использует Gemini для извлечения профиля пользователя в формате JSON. Профиль должен содержать поля: `name`, `tech_stack`, `project_goal`. Если информация отсутствует, поля должны быть null. Функция должна возвращать чистый JSON-объект (dict), а не строку.

Вопрос

Какой метод управления контекстом лучше всего подходит, если нам критически важно сохранить точные формулировки и детали из начала очень длинного разговора (например, юридические условия), но мы ограничены по токенам?