1. Главная
  2. Блог
  3. Компактификация контекста в ИИ-агенте секретаря: как удержать «как договаривались во вторник» и не разориться на токенах

Компактификация контекста в ИИ-агенте секретаря: как удержать «как договаривались во вторник» и не разориться на токенах

Компактификация контекста в ИИ-агенте секретаря: как удержать «как договаривались во вторник» и не разориться на токенах

Менеджер открывает карточку клиента — а там переписка почти на месяц. Полторы сотни сообщений, четыре смены темы, три обещания «перезвонить», один согласованный счёт и одна отменённая доставка. Наш ИИ-агент, который эту переписку вёл, только что ушёл в GigaChat Pro с запросом на всю историю, чтобы ответить на короткое «когда привезёте, я на выезд опаздываю». И это не самый жирный диалог из активных.

Когда мы запускали платформу для создания ИИ-агентов у себя внутри, вопрос стоимости токенов казался чем-то абстрактным. «Ну подрастёт немного, у нас же не миллионы диалогов». А потом бухгалтерия принесла первый счёт за GigaChat, и мы сели считать. Оказалось, что мы упёрлись в стену, о которой в статьях про AI-агентов почти не пишут: цена запроса растёт линейно с длиной истории. Обычные бенчмарки меряют латентность на четырёх тысячах токенов, а прод — это десятки тысяч, и это только начало жизни диалога.

Почему «просто обрезать историю» не работает

Первое, что приходит в голову, — отрезать всё старше N сообщений. Мы попробовали. Через несколько дней клиент написал: «как мы договаривались во вторник — счёт на прошлую позицию, оплата после майских». Секретарь честно ответил: «уточните, пожалуйста, о какой позиции идёт речь». Клиент положил трубку. Менеджер положил ноутбук.

Проблема в том, что важность сообщения не коррелирует с его свежестью. «Договорились после майских» — это факт, который живёт неделями. А «спасибо, до связи» через двадцать минут можно смело выкидывать.

Второй подход — суммировать всю историю одним промптом и держать только саммари. Тоже плохо. LLM склеивает разнородные факты, теряет даты, путает контрагентов. Через пару недель саммари превращается в кашу: «клиент интересовался поставками, обсуждали условия, договорились созвониться». Никакой опоры для ответа.

Стало ясно: нужна схема, которая помнит и обещания, и последний контекст, и структуру диалога, но платит только за нужное. Иначе никакие доступные ИИ-агенты не окупятся ни на одном тарифе.

Три слоя памяти: recent buffer, saved facts, rolling summary

Мы разложили контекст на три независимых хранилища в PostgreSQL.

  • Recent buffer — последние двенадцать сообщений, как есть. Это «оперативка», в неё попадают все свежие реплики без обработки.
  • Saved facts — структурированная таблица извлечённых обязательств, дат, договорённостей, отказов. Отдельный промпт после каждого нового сообщения проверяет: появилось ли новое обещание, изменился ли статус старого, нужно ли добавить дедлайн.
  • Rolling summary — сжатый текст из блоков по двадцать сообщений, где живёт «тон диалога» и общий фон, без цифр и дат.

Схема таблицы фактов у нас выглядит так:

create table dialog_facts (
    id           bigserial primary key,
    dialog_id    bigint not null,
    fact_type    text not null,  -- promise | deadline | rejection | preference
    actor        text not null,  -- client | agent
    subject      text not null,  -- о чём факт
    payload      jsonb not null, -- структура зависит от типа
    valid_from   timestamptz not null,
    valid_until  timestamptz,
    source_msg_id bigint,
    created_at   timestamptz default now()
);

create index on dialog_facts (dialog_id, valid_until) 
    where valid_until is null or valid_until > now();

Ключевой момент — valid_until. Обещание «перезвоню в пятницу» становится нерелевантным в субботу, и мы либо архивируем его, либо помечаем как невыполненное. Это отдельный маленький ночной джоб.

Долго спорили внутри команды, надо ли хранить факты как JSON в одной колонке или разносить по типизированным таблицам. Победил компромисс: тип и субъект — плоские поля с индексами, всё остальное — в jsonb. Так и запросы быстрые, и не приходится каждый раз таскать миграции.

Как это собирается в момент запроса

Когда клиент пишет новое сообщение, мы за один SQL-запрос собираем: последние двенадцать реплик, все актуальные факты по диалогу и последний rolling-summary. Итоговый промпт выглядит примерно так:

[SUMMARY]: клиент — торговая компания, обсуждали поставку кабеля ВВГ 
и щитового оборудования, отношения тёплые, тон деловой.

[FACTS]:
- 2026-06-23: клиент обещал прислать реквизиты (статус: получено 24.06)
- 2026-06-28: агент обещал счёт на позицию 7 (статус: отправлен 29.06)
- 2026-06-28 (вторник): договорились об оплате после майских праздников
- предпочтение: доставка утром до 10:00

[RECENT]: <последние 12 сообщений>

[NEW]: когда привезёте, я на выезд опаздываю

Итоговый промпт получается в разы короче, чем «свалка всей истории». Секретарь помнит про «после майских» и утреннюю доставку, не выдумывает и не переспрашивает. И, что важно, ответ приходит через пару секунд, а не после долгой паузы, за которую клиент успевает разозлиться.

Как это чувствуется на живой нагрузке

Один из наших заказчиков — торговая компания с несколькими десятками активных диалогов в день. До того, как мы включили компактификацию, средний контекст на запрос был толстый — если пересчитать в деньги, получалось, что один ответ секретаря стоил как пара минут работы живого менеджера. И это на короткие реплики.

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

Косвенный эффект оказался важнее прямого. Латентность ответа упала в несколько раз. Клиенты начали воспринимать секретаря как живого — не потому что он стал «умнее», а потому что перестал думать по несколько секунд над коротким сообщением. Мы даже не сразу это заметили: обратную связь принесли менеджеры, которым перестали жаловаться на «тормозного бота».

Что сломалось на второй неделе

Первую неделю всё было прекрасно. Потом начались странности. Клиент через двадцать минут после конца диалога писал «а можешь ещё раз повторить условия?». Recent buffer уже сдвинулся, факты были извлечены, но сама формулировка условий — то, как секретарь их проговорил, — исчезла. Он честно пытался восстановить по фактам, но получалось шершаво, не тем языком, что в первый раз.

Пришлось добавить четвёртый слой: последние два «крупных ответа» секретаря сохраняются отдельно, целиком, до конца диалога. Это дешёвая страховка на такие «повтори». По объёму — несколько сотен токенов, по эффекту — жалобы на «а мне сказали по-другому» почти исчезли.

Урок простой: когда настраиваешь ИИ-агента для бизнеса, нельзя проектировать память только под «идеальный сценарий из документации». Люди пишут не в вакууме. Они возвращаются, переспрашивают, ловят на слове. Память должна это выдерживать.

Извлечение фактов: JSON Schema и жёсткая валидация

Отдельный подводный камень — как надёжно вытаскивать факты из свободного текста. Мы гоняем это через российскую LLM с function calling и строгой JSON-схемой. Пример инструкции для извлечения:

{
  "type": "object",
  "properties": {
    "new_facts": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "fact_type": {"enum": ["promise", "deadline", "rejection", "preference"]},
          "actor": {"enum": ["client", "agent"]},
          "subject": {"type": "string", "maxLength": 200},
          "valid_until": {"type": "string", "format": "date-time"}
        },
        "required": ["fact_type", "actor", "subject"]
      }
    },
    "updated_facts": {"type": "array", "items": {...}}
  }
}

Если JSON невалиден — повторный запрос с явным напоминанием про схему. Если и это не помогает — падаем в fallback и сохраняем сырой текст в отдельное поле, чтобы менеджер мог посмотреть глазами. За несколько месяцев работы такое случалось буквально несколько раз, но fallback всё равно нужен: без него в один прекрасный день ты обнаружишь, что кусок диалога просто провалился в никуда.

Отдельная боль — русский. Модель периодически «размечает» одну и ту же договорённость то как promise, то как deadline. Пришлось внутрь subject укладывать нормализованные ключевые слова и добавить сверху дедуп по эмбеддингу — если новый факт слишком похож на существующий, обновляем старый, а не плодим дубли.

Когда компактификацию НЕ надо делать

Не всё нужно жать. Мы отключили компактификацию для нескольких случаев:

  • юридические согласования, где важна дословность формулировок — там любая потеря нюанса может стоить компании денег;
  • короткие диалоги, где вся история и так помещается в пару экранов — overhead на суммирование просто не окупается;
  • клиенты на дорогом тарифе, где заказчик сознательно платит за максимальный контекст и не хочет никакой автоматической «мудрости» между собой и моделью.

Это не техническое ограничение, а бизнес-правило. Записано в конфиге, включается флагом. Кстати, именно наличие таких флагов отличает нормальную платформу для создания ИИ-агентов от «универсальной коробки»: реальный прод всегда требует исключений, и лучше заложить их заранее, чем героически рефакторить в проде.

Что дальше

Следующий шаг, к которому мы движемся, — иерархическая память по клиенту, а не по диалогу. Один и тот же контрагент может писать в трёх разных диалогах: доставка, бухгалтерия, гарантия. Предпочтения из одного должны подтягиваться в другой. Технически это тот же слой фактов, но с другим ключом — client_id вместо dialog_id. Уже пилотируем, пока осторожно — потому что смешение контекстов между диалогами легко ломает приватность и порождает ответы вида «а вы же в прошлый раз с бухгалтерией такое обсуждали», от которых у клиента волосы дыбом.

Главный вывод, к которому мы пришли: контекст LLM — это не «положить всю историю и надеяться на лучшее». Это отдельная инженерная задача с собственным дизайном хранилища, промптами извлечения и метриками качества. Тот, кто относится к контексту как к базе данных со схемой и индексами, платит за прод в разы меньше и отвечает клиентам заметно быстрее.

Если вы сейчас настраиваете ИИ-агента и не знаете, какой у вас средний размер контекста в проде, — начните с этого числа. Часто именно оно объясняет, почему счёт за LLM не сходится с ожиданиями, а клиенты жалуются на «тормозного бота», хотя модель вроде бы быстрая.

Похожие статьи

Хотите так же?

Обсудим, как это внедрить у вас

Опишите задачу — подберём ИИ-решение под ваш процесс и бюджет: что сделать агентом, какие нужны данные и интеграции, с чего начать пилот.