• Перед любой отправкой агент читает последние 20–30 сообщений чата, без исключений
  • Файлы состояния — это индекс, чат — источник правды; индекс отъезжает незаметно
  • Правило родилось после того, как 18 адресатов получили шаблонный ответ на тишину

Задача

Агент ведёт многошаговую рассылку: первый контакт, отслеживание ответов, повторный ответ. Каждый шаг — скрипт, состояние — в JSON с метками времени. Во вторник скрипт читает JSON, по полю последнего сообщения решает, что 18 адресатов ответили, и отправляет каждому одинаковое «спасибо, передам команде». Ни один из них не отвечал. Агент поблагодарил 18 адресатов за ответы, которых не было, и переписка для каждого теперь выглядит тем, чем является: скриптом, который не читает.

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

flowchart LR
    S[Другой скрипт:<br/>пересканирование] --> T[Старое сообщение<br/>с новой меткой]
    T --> Q{Сравнение:<br/>ответил?}
    Q -->|да| X[18 одинаковых<br/>спасибо]
    classDef key stroke:#FF3600,stroke-width:2px
    class X key

Приём

Правило: перед любой отправкой в чат прочитать последние 20–30 сообщений этого чата. Файлы состояния (метки времени, счётчики стадий, позиции в воронке) — это индекс поверх чата, а не источник правды. Источник правды — сам чат.

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

flowchart LR
    W[Окно 20–30<br/>сообщений] --> P[Промпт<br/>модели]
    P --> Q{Чат подтверждает<br/>стадию из файла?}
    Q -->|да| S[Отправка<br/>ответа]
    Q -->|нет| O[Не отправлять,<br/>уведомить оператора]
    classDef key stroke:#FF3600,stroke-width:2px
    class O key

Индекс отъезжает по обычным причинам, и все они реальны:

  • один скрипт обновил поле, которое другой читает в старом смысле;
  • параллельный запуск переписал состояние посреди цикла;
  • метка времени обновилась при пересканировании без нового сообщения;
  • переход стадии применён не к той записи из-за ошибки сопоставления;
  • оператор поправил файл руками.

Файлы состояния полезны: они делают пайплайн дешёвым и позволяют быстро рассуждать о счётчиках. Но авторизовать акт отправки они не могут.

flowchart TD
    C[Чат:<br/>источник правды] --> I[Файл состояния:<br/>индекс поверх чата]
    I -->|суммирует| N[Счётчики,<br/>стадии, воронка]
    C -->|авторизует| S[Акт отправки]

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

Правило обобщается за пределы мессенджеров. Оно подходит любому действию, где внутреннее состояние агента — это индекс поверх внешнего хранилища, чтение хранилища дёшево, а действие плохо обратимо. Кодовый агент перед коммитом делает status и diff по реальным файлам, а не по своему представлению о рабочем дереве. Ops-агент читает живой конфиг из кластера перед патчем, а не кешированный снимок. Форма одна: перечитать перед отправкой.

flowchart TD
    R[Перечитать<br/>перед отправкой] --> M[Мессенджер:<br/>20–30 сообщений]
    R --> G[Кодовый агент:<br/>status и diff]
    R --> O[Ops-агент:<br/>живой конфиг]

Против правила приходят два аргумента. «Файл состояния и есть источник правды, иначе зачем он»: нет, его задача — суммировать, а не авторизовать. «Читать каждый раз расточительно, обычно ничего не изменилось»: да, обычно чтение избыточно. Цена маленькая, а цена ошибки в том одном случае, когда изменилось, — большая. Правило платит небольшой постоянный налог, чтобы убрать редкий дорогой отказ. В конфиг агента оно попало в тот же день по логике конфигурации по инцидентам.

Где ломается

  • Прочитать, но не использовать. Скрипт вызывает чтение и выбрасывает результат; модель сообщений не видит. Правило выполнено на уровне API, но не на уровне рассуждения. История должна попадать в промпт, а вывод должен её отражать.
  • Неверное окно. Пять сообщений пропускают ход двенадцатью выше; двести размывают внимание. Окно калибруется под динамику конкретного чата.
  • Дорогой источник. Если авторитетный источник — это многомегабайтный документ, который надо скачивать перед каждым действием, это уже другой расчёт затрат, и правило в чистом виде не применяется.

Итог

  • Чат — источник правды, файл состояния — индекс. Перед отправкой читать чат, без исключений.
  • 20–30 сообщений — для диалогов, 30–50 — для форумных тредов.
  • Правило обобщается на коммиты, конфиги, цитаты: перечитать реальность перед необратимым действием.
  • Один сбой на 18 адресатов стоил четыре часа; правило с тех пор стоит секунды на каждый запуск.

© Александр Никулин. Цитирование — с указанием автора и ссылкой · Версия для LLM