Контекст перед действием — почему агент должен прочитать чат, прежде чем туда писать
Студия запускает автономный hiring-пайплайн через AI-агента. Пайплайн сканирует Telegram-канал на кандидатов, квалифицирует их, отправляет первый аутрич, отслеживает ответы и отвечает повторно. Каждая стадия — скрипт; состояние — в JSON-файле с timestamp'ами. Во вторник скрипт читает JSON, по полю last_msg_ts обнаруживает, что 18 кандидатов «ответили», и отправляет каждому из них одинаковый шаблонный «спасибо». Сообщение уходит к 18 людям. Ни один из них на самом деле не отвечал на предыдущий аутрич. Агент отправил незнакомым людям «спасибо» за то, чего они не говорили. Цепочка переписки теперь выглядит для каждого кандидата точно тем, чем она является, — скриптом, который не читает.
***Паттерн, который я называю Контекст перед действием. Правило: перед любой отправкой в чат прочитать последние 20–30 сообщений этого чата. Внутренние файлы состояния агента (timestamp'ы, счётчики стадий, позиции в воронке) — это индекс, а не основной источник данных. Основной источник данных — чат. Файлы состояния могут содержать ошибки — например, когда timestamp'ы обновляются другим процессом, когда переход стадии применён неправильно, когда «доброжелательный» скрипт ставит флаг, не отражённый в чате. Чтение самого чата до действия выявляет ошибку до того, как она усилится при отправке.
Механизм — один абзац. Heartbeat-скрипт агента перед генерацией любого ответа вызывает функцию чтения чата с окном контекста 20–30 сообщений. Модель получает эти сообщения в составе промпта и генерирует следующий ответ с полным пониманием того, что было сказано. Если сообщения не подтверждают стадию, которую утверждает файл состояния, агент ничего не делает и уведомляет оператора. Правило безусловное: никаких исключений, никаких быстрых путей для случаев, когда файл состояния «надёжен», никакого спецобработчика для «мы уже знаем, что отправить».
Hiring-инцидент как канонический отказ
Скрипт кейса hiring_reply_scan.py доверился JSON-файлу состояния. Его задача — определить кандидатов, ответивших на первый аутрич агента, и продвинуть их на следующую стадию. Скрипт сравнивал last_msg_ts кандидата с last_sent_ts агента. Если last_msg_ts > last_sent_ts, скрипт заключал, что кандидат ответил.
Баг: last_msg_ts обновлялся предварительным сканирующим скриптом, который на самом деле не проверял, кто отправил сообщение. Более старое сообщение кандидата (до аутрича) получило новый timestamp при пересканировании. Сравнение сказало «ответил»; чат такого не говорил.
Следующая стадия — скрипт, отправляющий «спасибо». Отправка ушла 18 кандидатам. Файл состояния врал; чат показал бы правду; скрипт в чат не заглядывал.
Восстановление есть в кейсе: реактивно написанный hiring_audit.py, hiring_delete_extra.py для удаления 18 сообщений «для всех» (что доступно только пользовательскому аккаунту — Bot API так не умеет), откат состояния в JSON, hiring_context_dump.py для сборки реальной истории чата по каждому кандидату, потом 82 написанных вручную персонализированных ответа кандидатам, которые действительно ответили. Четыре часа на всё с уборкой; правило промоутировано в HEARTBEAT.md сразу после.
Почему файлы состояния врут
Файлы состояния — это индекс поверх чата: сводка того, что показывает чат, обновляемая кодом, оптимизированная под быстрое чтение. Как любой индекс, они могут отъезжать. Причины:
- Upstream-скрипт обновил поле, которое читает downstream-скрипт.
- Параллельный запуск того же скрипта переписал состояние посреди цикла.
- Поле было переименовано или перепрофилировано; старый код читает его в старом смысле.
- Timestamp обновлён пересканированием, когда нового сообщения нет.
- Переход стадии применён не к той записи из-за ошибки в matching ID.
- Оператор поправил файл руками, и правка конфликтует с допущениями агента.
Всё это реально. Ничего из этого не редкость. Файлы состояния полезны: они делают пайплайн дешёвым, позволяют агенту быстро рассуждать о счётчиках и стадиях, — но они не могут быть источником правды для самого акта отправки. Чат — может.
Окно 20–30 сообщений
Окно подобрано, а не произвольно. Двадцать сообщений достаточно, чтобы покрыть нормальный диалог между агентом и кандидатом (первый аутрич, ответ кандидата, подтверждение агента, уточнение кандидата, follow-up агента). Тридцать достаточно, чтобы покрыть случай, когда кого-то тегнули в длинный тред и релевантный контекст несколькими ходами назад. Дальше 30 контекст за пределами этого окна начинает отвлекать модель от недавних событий.
Окно также ограничено, чтобы чтение было дешёвым. Запрос сообщений из чата rate-limited; запрос последних 30 — быстро, запрос последних 300 начинает чувствоваться дорогим при heartbeat'е каждую минуту. Окно соразмерено реальному решению, которое агент собирается принять.
Правило обобщается за пределы мессенджеров
Паттерн подходит любому действию агента, где:
- Внутреннее состояние агента — это индекс поверх авторитетного внешнего хранилища.
- Чтение внешнего хранилища дешёво.
- Действие плохо обратимо.
Код-агент, который хочет коммитить, должен сделать git status и git diff против реальных файлов перед коммитом, а не доверять файлу состояния, описывающему то, что он думает о рабочем дереве. Ops-агент, который хочет применить config-change, должен прочитать живой конфиг из кластера перед патчем, а не доверять кешированному снапшоту. Research-агент, который хочет процитировать источник, должен запросить текущий текст источника перед цитатой, а не доверять версии, которую он встроил неделю назад. Форма одна: индекс отъезжает, авторитетный источник дёшев в запросе, действие дорого в откате, поэтому правило — перечитать перед отправкой.
Две ложные экономии
Против правила приходят два аргумента, оба неверные:
- «Файл состояния — это основной источник данных, для чего ещё нужны файлы состояния». Нет. Файлы состояния — быстрый индекс. Основной источник данных в мессенджере — реально отправленные/полученные байты в чате. Задача файла состояния — суммаризировать, а не авторизовать.
- «Читать чат каждый раз расточительно — чаще всего ничего не поменялось». Да, чаще всего чтение избыточно. Цена маленькая; цена того, чтобы оказаться неправым в том одном случае, когда поменялось, — большая. Правило платит маленький рекуррентный налог, чтобы исключить low-probability high-cost failure-режим. Инцидент с 18 сообщениями — ровно тот случай, где правило окупается.
Где паттерн подходит
- Любой агент, отправляющий сообщения людям в чатах. Кейс здесь.
- Ops-агенты, действующие на общих ресурсах (БД, конфиги, деплои).
- Кодовые агенты, коммитящие, мерджащие или иначе модифицирующие общий код.
- Hiring-, sales-, support-пайплайны со стадиями и шаблонными ответами — именно там, где соблазн доверять файлу состояния сильнее всего.
Слабее всего там, где чтение авторитетного источника действительно дорого, — research-агент, который пришлось бы каждый раз качать многомегабайтный документ, занимается не контекстом-перед-действием, а другим расчётом затрат.
Два режима отказа
- Прочитать, но не использовать. Скрипт вызывает функцию чтения чата и выбрасывает результат. Модель не видит сообщения. Правило выполнено на уровне API, но не на уровне рассуждения. Решение: подавать историю чата в контекст модели как часть промпта; проверять, что вывод модели отражает прочитанную историю.
- Неправильный размер окна. Агент читает 5 сообщений и пропускает релевантный ход 12 сообщениями ранее, или читает 200 и разбавляет внимание. Решение: калибровать под динамику чата — для коротких DM-диалогов 20–30; для длинных форум-тредов 30–50.
См. также
- tiered-agent-autonomy — более широкий паттерн управления; контекст-перед-действием — жёсткое правило, применяемое независимо от уровня
- incident-driven-configuration — цикл, который промоутировал это правило из
.learnings/вHEARTBEAT.mdпосле инцидента с 18 шаблонами - openclaw-autonomous-agent-paper — полный кейс, включая hiring-провал и восстановление