Перейти к содержанию

Глава 7. Извлечение контекста, уплотнение и фоновые обновления

1. Память бесполезна, если ты не умеешь возвращать из нее нужное

После того как ты развел short-term, long-term и profile memory, возникает следующий практический вопрос: как именно агент должен доставать нужные записи обратно в prompt?

Именно здесь многие системы начинают деградировать:

  • в prompt летит слишком много нерелевантного контекста;
  • retrieval возвращает похожее, но не полезное;
  • summaries растут, но не становятся понятнее;
  • каждая новая итерация делает контекст только тяжелее.

То есть проблема уже не в том, что память есть. Проблема в том, что память стала слишком шумной и дорогой для чтения.

2. Retrieval это не поиск “всего похожего”, а отбор того, что помогает решить текущую задачу

У retrieval очень плохая привычка: если его не ограничить, он пытается быть слишком щедрым. В итоге в prompt попадает не самый полезный контекст, а самый похожий по embedding или keyword overlap.

Для production-систем полезнее думать так:

  • retrieval не обязан возвращать много;
  • retrieval обязан возвращать объяснимо;
  • retrieval должен уважать tenant, source и trust boundaries;
  • retrieval должен подчиняться budget по размеру контекста.

Нормальный retrieval pipeline почти всегда учитывает не только similarity, но и:

  • tenant isolation;
  • memory class;
  • recency;
  • confidence;
  • provenance;
  • policy filters.

3. Хороший prompt любит не полноту, а плотность сигнала

Очень соблазнительно думать, что “чем больше контекста, тем умнее агент”. На практике часто наоборот: чем больше мусора ты кладешь в prompt, тем слабее модель держит приоритеты.

Поэтому retrieval должен отвечать не на вопрос “что мы можем достать?”, а на вопрос “что сейчас повышает шанс на правильное решение?”.

Полезный practical rule:

  • лучше 3 очень релевантных records, чем 20 условно похожих;
  • лучше маленький summary с источником, чем длинный сырой документ;
  • лучше отдельный profile hint, чем целая история предпочтений;
  • лучше пустой retrieval, чем retrieval без доверия и объяснимости.

4. Compaction это не косметика, а способ удержать систему в рабочем состоянии

Если memory layer только растет, рано или поздно prompt assembly начинает работать как мусоросборник без правил. Именно поэтому compaction должен быть частью архитектуры, а не разовым cleanup-проектом.

Compaction может означать разные вещи:

  • сжать несколько записей в summary;
  • удалить устаревшие working notes;
  • объединить дубликаты;
  • заменить большой blob на нормализованный record плюс ссылку на источник;
  • понизить приоритет старых записей вместо вечного хранения “на первом плане”.

Retrieval и compaction лучше мыслить как один цикл обслуживания памяти

flowchart TD
    A["New run"] --> B["Query memory"]
    B --> C["Apply filters and ranking"]
    C --> D["Assemble prompt context"]
    D --> E["Model + tools"]
    E --> F["Create new memory candidates"]
    F --> G["Background compaction and review"]
    G --> H["Normalized memory store"]
    H --> B

5. Не все обновления памяти должны происходить в hot path

Это один из самых полезных архитектурных сдвигов для агентных систем. В начале почти все пытаются сделать память “сразу готовой”: агент что-то увидел, сразу переписал summaries, сразу обновил profile, сразу сохранил knowledge.

Но это почти всегда слишком дорого и слишком рискованно.

Что обычно разумно оставить в hot path:

  • минимальный session state;
  • короткие working notes;
  • безопасные transient records с понятным TTL;
  • update, без которого текущий workflow реально ломается.

Что обычно лучше уводить в background:

  • compaction длинных сессий;
  • пересборку summaries;
  • нормализацию facts;
  • deduplication;
  • review memory candidates перед persistent write.

Именно background updates позволяют сделать память аккуратной, а не просто быстрой.

6. Хорошая система разделяет retrieval query и maintenance jobs

В зрелой архитектуре почти всегда есть два разных контура:

  • read path: быстро и безопасно достать контекст для текущего run;
  • maintenance path: спокойно улучшить memory store без давления latency.

Это важно не только для производительности, но и для качества решений. Когда одна и та же цепочка одновременно:

  • выполняет задачу;
  • пересобирает summaries;
  • пишет profile memory;
  • чистит дубликаты;
  • обновляет ranking metadata,

она очень быстро становится хрупкой и плохо объяснимой.

7. Пример policy для retrieval и background updates

Ниже очень практичный шаблон. Он не пытается быть универсальным, но хорошо показывает, какие решения стоит сделать явными.

retrieval:
  max_records: 5
  max_tokens: 1800
  allowed_classes:
    - short_term
    - long_term
    - profile
  require_tenant_match: true
  min_confidence: 0.75
  deny_sources:
    - raw_external_html
    - unreviewed_summary

compaction:
  run_mode: background_only
  summary_max_tokens: 400
  deduplicate: true
  merge_similar_records: true
  drop_expired_short_term: true

Когда такие правила явны, команда уже не спорит о памяти на уровне ощущений. Она спорит о конкретных ограничениях и trade-offs.

8. Простой кодовый пример ranking перед prompt assembly

Ниже не “умный” retrieval engine, а специально понятный пример. Он показывает, что ранжирование полезно строить не только по похожести, но и по доверию, свежести и важности.

from dataclasses import dataclass


@dataclass
class RetrievedRecord:
    text: str
    similarity: float
    confidence: float
    recency_weight: float
    trusted: bool


def score(record: RetrievedRecord) -> float:
    trust_bonus = 0.15 if record.trusted else -0.2
    return (
        record.similarity * 0.5
        + record.confidence * 0.25
        + record.recency_weight * 0.1
        + trust_bonus
    )


def select_for_prompt(records: list[RetrievedRecord], limit: int = 3) -> list[RetrievedRecord]:
    ranked = sorted(records, key=score, reverse=True)
    return ranked[:limit]

Такая логика грубая, но у нее есть важное достоинство: ее можно обсуждать, тестировать и постепенно заменять чем-то более точным без потери объяснимости.

9. Summaries должны помогать читать, а не скрывать происхождение данных

Очень часто команды используют summaries как способ “впихнуть больше памяти в меньше токенов”. Это нормально, но есть одна ловушка: summary не должен превращаться в новую анонимную истину.

Хороший summary:

  • короче исходных записей;
  • сохраняет provenance;
  • не смешивает tenants;
  • не теряет критические ограничения;
  • помечен как derived artifact, а не raw fact.

Плохой summary:

  • звучит уверенно, но неясно, откуда он взялся;
  • объединяет конфликтующие facts;
  • теряет дату и владельца данных;
  • подсовывается модели как trusted instruction.

10. Что чаще всего ломается в retrieval-системах

Обычно проблемы повторяются:

  • в prompt попадают дубликаты;
  • retrieval не знает про class boundaries;
  • ranking не учитывает trust;
  • summaries становятся слишком общими;
  • background jobs отсутствуют, и memory только пухнет;
  • nobody knows why this exact chunk was retrieved.

Последний пункт особенно важен. Если ты не можешь объяснить, почему контекст оказался в prompt, система уже плохо управляется.

11. Практический чеклист

Если хочешь быстро проверить свой retrieval layer, пройди по вопросам:

  • Есть ли limit по количеству records и token budget?
  • Учитывает ли ranking не только similarity, но и confidence, recency и trust?
  • Разделены ли read path и maintenance path?
  • Есть ли compaction как регулярный процесс, а не ручная уборка?
  • Можно ли у summary увидеть provenance?
  • Есть ли защита от retrieval через чужой tenant или неподходящий class?

Если на несколько вопросов подряд ответ “нет”, значит память у тебя уже есть, а вот memory discipline пока нет.

12. Что читать дальше

На этом базовая часть про память уже начинает складываться. Дальше логично либо углубиться в retention и deletion, либо перейти к части про инструменты и выполнение.