Агентные системы ломаются не на сложных задачах и не на плохих моделях. Главная причина — недетерминизм LLM: температура, апдейты моделей, дрейф мира. Как отлаживать то, что не воспроизводится? Как перезапустить упавший пайплайн не с нуля? Как вообще понять поведение системы, если каждый запуск чуть-чуть другой?
Event Sourcing — паттерн, где состояние не снапшот, а иммутабельный лог событий. Недетерминизм он не убирает, но даёт инструменты для работы с ним.
Немного теории
Сразу замечу — идея не моя и совсем не новая. Event Sourcing как паттерн систематизировал Фаулер в 2005-м, связку ES + CQRS закрепил Грег Янг в докладе 2014 года. Паттерн в основном живёт в финансах и распределённых системах — далеко от LLM, но с ровно теми же тремя свойствами лога. Ближайшая родня, шагнувшая к агентам ещё раньше, — durable execution engines (Temporal, Restate): то же «храним историю событий, реплеим детерминированно», только в словаре workflow/activity вместо stream/tool_call.
Прямое применение ES к LLM-агентам — это уже 2026 год: ESAA (arXiv:2602.23193, та самая работа, под впечатлением которой писался zymi), OpenKedge (arXiv:2604.08601, формализует governance поверх event-sourced state).
Эта статья — не литературный обзор, а practitioner-essay поверх перечисленного. Если что-то из тезисов захочется копнуть глубже — ESAA и OpenKedge за формализацией, FAIR 2025 за аксиомами аудируемости, CoALA и Generative Agents за связью с памятью, Temporal и Restate за «как это решают вне агентского контекста».
Как устроена ESA
ESA — это event-driven архитектура с одним добавленным инвариантом: лог неизменяем, разрешена только запись. То есть шина событий тут не просто транспорт между сервисами, а ещё и единственное место истины — всё, что произошло в системе, лежит в нём и никогда не редактируется. Агенты на этой шине играют роль обычных подписчиков: каждый ждёт появления в логе нужных ему типов событий, забирает их в работу, а свои результаты пишет туда же.

Этот иммутабельный лог событий и есть самая ценная архитектурная идея подхода — он даёт сразу несколько преимуществ — правда, при условии что задача укладывается в события: перезапуск пайплайнов, единый источник правды, иммутабельный и аудируемый журнал. Эти три — на самом деле одно и то же свойство лога, рассмотренное с трёх сторон. И именно поэтому, когда любое из трёх перестаёт быть достижимым (перезапуск не воспроизводит реальность, source-of-truth не успевает за событиями, логи надо «удалять» по запросу) — ломается сразу всё, а не одно из трёх.
Лично для меня паттерн стал не столько готовой спецификацией, сколько удачной формулировкой базового инварианта: состояние агентной системы должно выводиться из журнала событий. Дальше в zymi начались уже более прикладные вопросы: как ветвить выполнение, как не повторять внешние эффекты, как собирать контекст в виде проекции и как сделать human-in-the-loop частью того же журнала.
Перезапуск пайплайна с любого места
ESA позволяет нам не просто перезапустить пайплайн, а запустить его с любого произвольного шага — допустим, у вас есть свой агент-исследователь, который сначала собирает информацию, а потом готовит отчёт на её основе. И если у вас есть вопросы к моменту подготовки отчёта, вы можете просто внести правки в этот шаг и перезапустить пайплайн с этого момента, не собирая данные заново.

Здесь как и у классического EDA есть свои проблемы — если в EDA была проблема с внешними событиями, например, списание со счёта нельзя провести ещё раз при перезапуске — то тут мы сталкиваемся ещё и с недетерминизмом LLM. Fork-resume даёт нам возможность хотя бы снизить его влияние на конечный результат — не нужно заново вызывать тех агентов, результаты которых нас устраивают.
Пример из zymi
В zymi это работает примерно так:
zymi resume <stream_id> --from-step writer
создаёт новый стрим и физически копирует туда событийный префикс исследователя — его LLM-вызовы, результаты поиска в сети, всё. Писатель перезапускается с актуальным промптом из agents/writer.yml, остальное замораживается.
Внешние эффекты внутри переисполняемой части закрываются отдельным флагом — если у инструмента стоит no_resume: true (например, send_email), при resume он не вызывается: в журнал пишется ToolCallCompleted { replayed: true } с честным плейсхолдером, и агент в следующем ходе видит «этот email уже был отправлен раньше, повторно не делал».
Лог как единый источник правды
Сам по себе лог это, конечно, хорошо, но важен нюанс — раскрывается он только в случае, когда мы делаем его в нашей системе единым и единственным источником правды. Все потребители, все источники должны взаимодействовать только через лог — это гарантирует нам и перезапуски, и аудируемость.

Но эта строгая дисциплина в ответ наградит нас удобством при подключении новых источников/потребителей — достаточно просто создать новый тип события, которое они будут слушать/создавать, и всё. Никаких дополнительных интерфейсов, всё работает из коробки.
Пример из zymi
В zymi есть команда, которая проверяет целостность хэш-цепочки лога:
zymi verify
Когда я её добавлял, ожидал работы с рантаймом: где-то прокинуть хуки, не сломать пайплайн-исполнитель, не задеть подписчиков. По факту вышел файл на 76 строк, который не импортирует рантайм вообще: открывает тот же SQLite, перебирает стримы, валидирует SHA-256. Всё. Ровно по тому же пути живут zymi observe (TUI с живой графой пайплайна) и zymi runs (CLI-листинг прогонов) — каждая команда это самостоятельный потребитель поверх одного лога, и в ядро никто из них не лезет.
Выгода единого источника заметна сразу: новый потребитель — это новый файл рядом, а не новый интерфейс в рантайме.
Есть в едином источнике правды ещё один плюс — контекст это больше не мутабельный набор данных в памяти, это проекция из лога, которой мы можем крутить как хотим. Размер контекста, порог его сжатия, маскирование определённых результатов (это вообще отдельная большая тема, сильно экономящая токены — в планах расписать подробней в отдельной статье), любые манипуляции — у нас совершенно развязаны руки, т.к. исходники в целости и сохранности лежат в логе. В zymi это ContextBuilder поверх того же SQLite — поднять окно с 10 ходов до 30, замаскировать старые tool_result'ы плейсхолдерами, пересобрать контекст под другого агента — всё это операции над проекцией, исходники в логе не трогаются.
При этом проекции лога, само собой, очень сложно масштабировать на миллионы событий в секунду, а лог событий как единственная точка правды — не лучшая идея для координации тысяч параллельных агентов; здесь ESA бессильна и только добавит головной боли.
Иммутабельный и аудируемый журнал
Третья грань этого же лога — иммутабельность. Раз мы договорились, что лог — единственный источник правды и все потребители читают из него, то достаточно один раз потребовать «события только дописываются, никогда не редактируются» — и журнал перестаёт быть просто хранилищем, становится инвариантом, который можно проверить.

Следствий тут сразу несколько. Поверх неизменяемого лога естественно ложится хэш-цепочка: каждая запись содержит SHA-256(event_id + data + prev_hash), и отдельная команда пробегает стрим и говорит, не было ли подмены — журнал становится не просто иммутабельным по дисциплине, а математически верифицируемым. И самое приятное — аудит-трейл выпадает как побочный эффект из любого нового события, которое ты кладёшь на шину; ничего отдельного для этого делать не нужно.
Отдельно стоит выделить, что у каждого события понятно, откуда оно взялось и что его вызвало. Прямо в конверте лежат три поля: correlation_id (одна пользовательская заявка от начала до конца), causation_id (какое событие породило это) и source («cli», «telegram», «scheduler», «agent»). На любой факт в логе можно ответить «кто, когда, в ответ на что» — без отдельной системы аудита, просто потому что эти поля и так есть в конверте. Для агентов это убийца извечного вопроса «почему модель так решила?» — он превращается в «покажи цепочку событий, приведших к этому LlmCallStarted», и она там лежит целиком, от исходного user_message до конкретного tool_result, который попал в окно контекста.
Пример из zymi
Согласования действий «человеком в петле» сначала жили в HashMap внутри HTTP-обработчика — обычное in-memory-состояние. Перезапустил процесс — забыл, кто что одобрил полчаса назад. После перевода согласования на шину (ApprovalRequested → ApprovalGranted с полем decided_by: "slack:@alice"), починилось сразу несколько вещей: согласования стали видны из TUI, переживают рестарт, и — побочным эффектом — появился постоянный аудит-трейл «кто, что и когда одобрил». Никто отдельно не строил систему аудита; она выпала из факта, что одобрения теперь живут в том же логе, что и всё остальное.
Цена у этого свойства тоже вполне ощутимая. Эволюция модели данных — лог за полгода накопил миллионы записей в старом формате, ты добавляешь новое поле, и встаёт вопрос «что делать со старыми событиями?». Право на забвение из GDPR — неизменяемый лог по определению не умеет «забыть» одну строку, а просто стереть её = сломать хэш-цепочку. На обе проблемы есть рабочие ответы:
Со схемой спасает связкой дефолтов для новых полей и явной пометкой пониженной точности.
С GDPR хитрее: формально иммутабельный журнал и право на забвение противоречат друг другу. Решений два: crypto-shredding — каждое событие с персональными данными шифруется своим ключом, и при запросе на удаление мы просто выбрасываем ключ, событие физически остается на месте, но расшифровать его уже никто не сможет; tombstone-события — специальная запись поверх, которая для всех проекций означает «забудьте про предыдущие данные по этому субъекту».
Платить за это приходится сложностью. Проекциям нужно уметь жить со старыми версиями схемы. А шифрование payload'ов — это уже целая инфраструктура управления ключами, которую тоже надо где-то хранить и не терять.
Ограничения архитектуры
Кроме разобранных выше ограничений есть и менее очевидная цена: ESA меняет жанр дебага. Привычная картина «упало → стек-трейс → строка кода» не работает — её место занимает «упало → найди correlation_id → пройди по цепочке событий → восстанови состояние на момент сбоя». Это та же по сложности работа, но в другом наборе примитивов — порядок, причинность, проекция состояния. Команде, которая приходит из мира функциональных вызовов и стек-трейсов, нужно время на смену оптики; это не дороже, чем дебажить распределённую систему, но и не дешевле — просто другое.
Эта архитектура проигрывает там, где сама природа задачи не позволяет оперировать событиями: непрерывные потоки сигналов; данные, которые нужно уметь удалять по требованию; тысячи параллельных агентов без единой точки координации; внешние сервисы с непредсказуемым поведением.
Хорошая система, основанная на событийном подходе, знает свои границы и явно делает шаг в сторону там, где они достигнуты, а не пытается «event-source'ить всё» через силу.
Заключение
Если коротко вернуться к тому, с чего начали: иммутабельный лог даёт три свойства — детерминированный перезапуск, единый источник правды, аудируемость. И ключевое здесь — это не три разных бонуса, которые можно набирать по отдельности. Это одно и то же свойство лога, повёрнутое к нам тремя гранями. Поэтому компромисс по любой из них рушит остальные две: если перезапуск не воспроизводит реальность — значит, лог не источник правды; если в логе можно подправить запись — значит, аудиту нельзя верить; если потребители ходят мимо лога — значит, fork-resume не воспроизведёт картинку, которую они видели.
Все практические кейсы в этой статье получены при разработке опенсорс-фреймворка для агентов — zymi-core. Писал о нём в своей предыдущей статье — Что если собирать агентов как dbt-проект? Проект сейчас двигается в сторону MCP сервера — бэкенд для агентов, который можно подключить к вашему любимому Claude Code / Codex / OpenClaw и прочему.
Про агентов, Event Sourcing и смежные темы пишу также в телеграме.
Комментарии (6)

Ariless
29.05.2026 07:34Самое ценное в такой архитектуре с точки зрения тестируемости это связка causation_id + immutable log.
У меня в RAG-пайплайне был кейс, где тест падал с 422 (неправильная специализация). Проблема была не в модели, а в том что retrieval layer формировал некорректный контекст.
Ключевой вопрос в отладке был не "ошибся ли LLM", а "какие именно данные попали в контекст на этом шаге". Без иммутабельного лога это требовало ручной реконструкции: проверка retrieval scores через node -e, отдельный режим AI_MOCK_RESPONSE=true для исключения LLM и изоляции retrieval-слоя. В результате выяснилось, что релевантный контекст не попадал в prompt на этапе сборки.
При наличии event log с причинной связностью контекст становится воспроизводимой проекцией состояния системы. Это переводит отладку из восстановления фактов в навигацию по correlation_id. Практический эффект - тест начинает проверять не только выход системы, но и входное состояние, в котором этот выход был сгенерирован.

ArtTrek Автор
29.05.2026 07:34Прямо в точку. Про проверку входного состояния - идеальная формулировка, лучше моей.
DrMayDay
Seed модели задайте руками, и будет вам полный детерминизм, одно и то же каждый запуск на одни те же данные. Не благодарите.
ArtTrek Автор
К сожалению seed не дает полного детерминизма - OpenAI пишет «mostly deterministic».
Есть на эту тему исследование от Thinking Machine.
Ну и сама нулевая температура, есть кейсы где она актуальна (классификация, json собирать), но что если нужен живой диалог? Написание кода? Исследование?
Ну и в целом, ESA не про это - он не про воспроизводимость именно конкретного ответа, он про воспроизводимость всей системы.
DrMayDay
С LLM'ками нельзя всё сваливать в кучу. Если у вашего(я так понимаю самописного) агента начинаются проблемы типа "петель" на вызове инструментов, то тут уже не место "диалогам" - проблему нужно логировать и жёстко разгребать на конкретном seed'е. В той чудной статье 2023го года объясняют, что "mostly deterministic" имеется ввиду, что нельзя в своих государственно-сертифицированных криптоприложениях рассчитывать на идентичность, радиация там, распределённость вычислений, ненадёжность float, всё такое... Для целей отладки агентов детерминизм там полный(при условии одинаковых seed/temp/top_p/...).
И вообще агентов надо на локальных моделях типа GPT-OSS 20B тестировать(если openai-compatible) именно из-за их "несильной умности", так как они не будут пытаться сами баги вашего агента как-то сглаживать, а сразу начнут косячить и глючить, что хорошо в случае отладки. Если вы сможете сделать агента у которого Gemma4 26B заработает без "петель", в этого агента потом можете что угодно засовывать(с таким форматом потока) - он точно отработает)
ArtTrek Автор
Вот тут соглашусь, отладка на локальных моделях отличный подход.
Но главное с чем соглашусь - "проблему нужно логировать и жёстко разгребать". ESA как раз и создана для этого - всё, что происходит в системе уже в логе. Нужен перезапуск с другими параметрами? Да сколько влезет, причём нет нужды прогонять заново вообще все шаги, их можно спокойно переиспользовать.