Почему 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, и ваш пользователь могут разобраться, откуда взялся такой ответ:
достаточно пройти по связям:
Это не «приятный бонус», а сама суть агента, который готов отвечать за свои слова (§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, компилирует эффекты в один атомарный патч, записывает чтения/записи/связи и будит следующую волну. Оркестрация «вытекает из состояния» — вы занимаетесь предметной областью, а не склейкой.
Цикл событий — как реально происходит «реакция»¶
Реактивный движок — это один обычный цикл; никакого планировщика узлов настраивать не нужно. Агенты реагируют не на «ребро графа», а на событие, и цикл выглядит так:
- Применение патча порождает
Event—artifact_created,artifact_updated,artifact_deleted— каждый ссылается на артефакт по типу и id (CONSTITUTION, фаза 4 «Реактивный рантайм»). - Рантайм забирает эти события (
arun_once→drain_events()) и для каждого спрашивает каждого агента: ты реагируешь? (agent.matches(event)). - Решение о реакции — это ровно ваши объявления
Consume: тип артефакта и, опционально,condition(например,Consume.by_status(...)). Никакого центрального реестра и таблиц связки: набор реакций и есть объявление. - Сработавшие агенты запускаются (с учётом бюджета, параллельности, приоритета); их эффекты компилируются в следующий атомарный патч; цикл повторяется — пока очередная волна не породит событий и запуск не устаканится.
Две детали делают это полезным, а не просто эффектным:
- События возникают сами, их не нужно отправлять вручную. Вы не вызываете
никакой
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.