Skip to content

Почему reactifact — идея на одной странице

Это обоснование дизайна, а не перечень возможностей. Если вы уже знаете, что умеет reactifact, эта страница объясняет почему он устроен так: почему effects, а не граф; почему версионируемое состояние; почему детерминизм — это позиция, а не настройка. Инварианты, стоящие за каждым утверждением, описаны в constitution.md (на английском).

Если вы знаете Celery — вы уже знаете модель

Ментальная модель Celery: объяви задачу, скажи, что её запускает, — рантайм сам её выполнит; результат попадает в result backend. reactifact применяет ровно это к агентам:

  • задача — это @produce(Model): единица работы, которая пишет артефакт;
  • триггер — не сообщение, которое вы пушите: создание входного артефакта и есть триггер, а Consume(Type) говорит, какой тип артефакта будит задачу;
  • chain / group / chord — это просто несколько consumes / produces; порядок рантайм выводит из состояния, а не из проводки;
  • result backend — это Context: типизированные версионируемые артефакты, поэтому вы получаете провенанс и воспроизводимость, которых у очереди задач нет.

Честная оговорка: сегодня reactifact — один процесс, это Celery-образная модель, а не её распределённый брокер/воркеры. Дальше на этой странице — почему такая модель (состояние важнее исполнения) верна для открытой работы со знаниями.

Проблема «агент = граф, который вы рисуете»

Большинство фреймворков строят агентов вокруг графа (или цепочки): соединяй узлы, подключай память, описывай поток управления. Так удобно, пока задача — фиксированный конвейер. Но агенты, работающие со знаниями, устроены иначе.

Пользователь спрашивает: «Почему во втором квартале выросли расходы на инфраструктуру?» (§1.1 CONSTITUTION). Для ответа нужны Confluence, заявки в GitLab, CSV-файлы, расчёты, проверка фактов и, возможно, уточняющие вопросы. Следующий вопрос потребует другого пути. Универсального графа не существует, а заставлять разработчика рисовать граф под каждый возможный вопрос — значит требовать предсказать будущее.

reactifact переворачивает картину. Вы не описываете исполнение. Вы описываете какие данные есть, какие артефакты есть и что агенты умеют с ними делать, а runtime сам решает, что запускать дальше — исходя из изменения состояния. Агенты реагируют на события. Рисовать нечего.

Почему self.effects, а не «вернуть патч»

Во многих фреймворках производитель возвращает свой результат (вызов инструмента, сообщение, словарь), а какой-то оркестратор применяет его. В reactifact авторство устроено наоборот (§24):

async def produce(self, call: ProduceCall) -> None:
    evidence = self.effects.create(Evidence(...), id="evidence:q1")
    answer = self.effects.create(Answer(...), id="answer:q1")
    evidence.link("extracted_from", doc)
    answer.link("supported_by", evidence)
    self.effects.update(turn, status="answered")
    return None

Почему выбрана такая форма?

  • Автор описывает намерение, а не обвязку. self.effects читается как предложение: создай это, свяжи с тем, обнови статус. Вы никогда не собираете Patch — его собирает runtime.
  • Работаешь с артефактами, а не с id. evidence создан здесь, связан чуть ниже — это один и тот же объект, на который не нужно каждый раз «находить по номеру». Код читается как разговор об артефактах, а не об идентификаторах.
  • Один атомарный коммит. Ничто не применяется, пока produce не завершился (§41). Не бывает «наполовину внесённых» изменений, не нужны ручные откаты. События, проверка по объявленным produces и запись в трейс строятся из одного и того же набора операций.
  • Параллельная безопасность — по построению. У каждого produce свой слот эффектов; одновременно работающие продюсеры не видят чужое незавершённое состояние (§42).
  • HITL появляется сам собой. self.effects.ask(...) — просто ещё один эффект (§60); человек — не особый случай, прикрученный сбоку.

Если внутри produce хочется писать Patch() — остановитесь. Это работа runtime.

Почему артефакты, а не сообщения

В «месседж-стайле» агент обменивается строками — из них видно только текст и больше ничего. Артефакты — другое дело: это типизированные, версионируемые объекты с провенансом (Claim, Evidence, Answer, Calculation). каждый производный артефакт связан с тем, из чего появился — поэтому и runtime, и ваш пользователь могут разобраться, откуда взялся такой ответ: достаточно пройти по связям:

Answer —supported_by→ Claim —derived_from→ Evidence —extracted_from→ Doc

Это не «приятный бонус», а сама суть агента, который готов отвечать за свои слова (§15, §34).

Почему версионируемый контекст

Каждый запуск — это коммит. У вас есть diff, rollback, branch() и merge() для состояния разговора (§39–§40) и детерминированный replay (§55). Разница между транскриптом, который можно посмотреть, и историей, которую можно перемотать, разветвить и безопасно слить.

Почему детерминизм — это позиция, а не настройка

Доминирующий подход — «пусть LLM делает всё». Позиция reactifact обратная (§67):

  • Расчёты считаются по-настоящему. Spreadsheet → Calculation над CSV — детерминированный код, а не правдоподобно выдуманное число.
  • Сработать или нет — решает состояние. Условие (guard) produce решает, запускаться ли ему; LLM вызывается только на по-настоящему генеративных шагах.
  • Честность важнее имитации. При сбое produce возвращает None (или не создаёт ничего), а не уверенное предположение (§59). Модель — компонент рассуждения, но никогда не источник истины.

Правило: что можно вычислить — вычисляйте; LLM — только там, где нужны язык и суждение.

Почему runtime, а не «приложение связывает всё вручную»

В традиционном фреймворке приложение само передаёт сообщения между компонентами. Здесь приложение объявляет, что агенты потребляют и производят; runtime сопоставляет события потребителям, собирает входные данные, запускает produces, компилирует эффекты в один атомарный патч, записывает чтения/записи/связи и будит следующую волну. Оркестрация «вытекает из состояния» — вы занимаетесь предметной областью, а не склейкой.

Цикл событий — как реально происходит «реакция»

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

  1. Применение патча порождает Eventartifact_created, artifact_updated, artifact_deleted — каждый ссылается на артефакт по типу и id (CONSTITUTION, фаза 4 «Реактивный рантайм»).
  2. Рантайм забирает эти события (arun_oncedrain_events()) и для каждого спрашивает каждого агента: ты реагируешь? (agent.matches(event)).
  3. Решение о реакции — это ровно ваши объявления Consume: тип артефакта и, опционально, condition (например, Consume.by_status(...)). Никакого центрального реестра и таблиц связки: набор реакций и есть объявление.
  4. Сработавшие агенты запускаются (с учётом бюджета, параллельности, приоритета); их эффекты компилируются в следующий атомарный патч; цикл повторяется — пока очередная волна не породит событий и запуск не устаканится.

Две детали делают это полезным, а не просто эффектным:

  • События возникают сами, их не нужно отправлять вручную. Вы не вызываете никакой emit: достаточно создать, обновить или удалить артефакт — событие появится автоматически, его породит коммит (§41). Цепочка причин не может разойтись с реальным состоянием, потому что события и есть журнал его изменений.
  • event приходит прямо в produce. Сигнатура — produce(self, context, inputs, event=None): внутри можно отреагировать именно на то изменение, которое разбудило агента (какой артефакт и его тип), а через inputs — увидеть и всю картину по потребляемому типу.

Именно поэтому «реактивность» — не маркетинг: единственный способ передать управление в reactifact — через изменение состояния, и события — это ровно то изменение, которое видят агенты. Никакого if x: вызови y руками — состояние решает, цикл исполняет.

Ментальная модель из трёх слоёв

Слой Что это Кто пишет
Produce реакция: условие → LLM/расчёт → self.effects.*None вы
Effects описанный набор изменений в границах хода вы, через self.effects
Patch скомпилированные операции, применяемые одним коммитом runtime

Почему даже прикладной слой тонкий

ChatAssistant (сессии, ходы, история) и create_chat_router (SSE-контракт для вашего FastAPI) закрывают канонический чат, не загоняя вас в готовое «фреймворк-приложение». Вы оставляете свой session store, свои ресурсы и свои артефакты; фреймворк предоставляет цикл. Если ваш цикл отличается — параллельные форки или отдельный роут для ответов человека — строительные блоки (run_message, create_agent, effects) компонуются под вами.

Значит ли это «вообще никаких графов»?

Нет. Если рабочий процесс действительно фиксирован, reactifact умеет выразить и его — но суть в том, что вас не принуждают рисовать граф там, где задача открыта. Канонические паттерны (ReAct, reflection, map-reduce, supervisor, summarize, time-travel, plan-and-execute) имеют эталонные реализации в examples/ (см. port-matrix); они возникают из состояния и produces, а не из заранее нарисованной диаграммы.

Где reactifact не подойдёт

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

Всё остальное — агенты, которые собирают доказательства, проверяют утверждения, считают, соблюдают бюджет, спрашивают людей и поддаются аудиту, — ровно то, ради чего создан reactifact.