Multi-turn и чат¶
Разговор — это просто состояние, живущее между ходами: приложение вносит
сообщение пользователя, агенты реагируют, ответ читается обратно, а весь
Context хранится под session_id, так что следующий ход — и следующий
процесс — его видит. Эта страница сводит воедино элементы multi-turn (и их
аналоги в LangGraph).
Если вы знаете LangGraph¶
| LangGraph | reactifact |
|---|---|
thread_id + checkpointer |
session_id + SessionStore (FileKVBackend / SQLiteKVBackend / PostgreSQLKVBackend) |
MessagesState + reducer add_messages |
Conversation / ConversationMessage (recipes.conversation); состояние — типизированные артефакты, версионируемые на каждое изменение |
graph.invoke(input, config) |
ChatAssistant.stream()/invoke() или run_message() для своего цикла |
interrupt() / Command(resume=…) |
effects.ask(...) → PendingQuestion → effects.resume(...) |
get_state_history / resume с checkpoint |
context.history() / context.checkout(v) / context.branch() / reactifact replay |
| ограничить транскрипт / суммаризовать | ChatMemory → WindowPruner / RollingDigestSummarizer; Context.compact для истории |
store (долговременная память между потоками) |
не встроена — см. Sources / SessionStore приложения |
Отличие в сути: reducer сливает непрозрачные сообщения, а reactifact хранит
типизированные версионируемые артефакты — поэтому провенанс хода,
возможность diff двух ходов и откат суть свойства самого состояния.
Канонический чат¶
ChatAssistant владеет сессиями, циклом хода и историей; create_chat_router
монтирует его как SSE на вашем FastAPI-приложении. Настройка через хуки:
from reactifact import Consume, RuntimeResources, create_agent
from reactifact.chat import ChatAssistant
from reactifact.checkpoints import SQLiteKVBackend
from reactifact.recipes.conversation import (
ConversationMessage, conversation_state, create_turn,
)
from reactifact.session import SessionStore
assistant = ChatAssistant(
store=SessionStore(SQLiteKVBackend("./sessions.db")),
agents=[create_agent("answer", steps=[ANSWER])],
user_message=ConversationMessage,
create_message=create_turn, # записать сообщение пользователя как ConversationMessage
reply=my_reply, # (ctx, msg_id) -> {"reply": ...}
session_state=conversation_state, # как history() восстанавливает поток
resources=lambda: RuntimeResources(llm=my_llm),
)
await assistant.invoke("what's the refund policy?", session_id="user-1")
await assistant.history(session_id="user-1") # поток на данный момент
Готовый quick.chat_agent(agents, store=…) делает то же с дефолтами.
Per-request метаданные в create_message. Хук может принимать
(ctx, text), (ctx, text, session_id) или (ctx, text, session_id,
request) (определяется по арности). Во время его вызова ctx.request — тот
же mapping, что передан в stream(..., request=...), поэтому категорию/
тенанта/пользователя хода можно записать в артефакт пользователя без
собственного ContextVar:
def create_message(ctx, text, session_id):
return ctx.create(UserQuery(
text=text,
session_id=session_id,
category=ctx.request.get("category", ""),
)).id
await assistant.invoke("...", session_id="s1", request={"category": "helm"})
resources= может быть и обычным экземпляром RuntimeResources, если нужен
один долгоживущий провайдер на все сессии. Per-turn budget/deadline/токены и
обёртки трейсинга ставятся на поверхностную копию, поэтому конкурентные ходы
не мутируют общий экземпляр.
Внешние side effects. Если агент в чате записывает намерение через
effects.act(...), передайте dispatcher= (и, при желании,
on_dispatch_error=) — они уходят прямо в Runtime, который ассистент собирает
на каждый ход, так что закоммиченные действия доставляются один раз, после
коммита генерации, ровно как в обычном прогоне:
async def dispatch(context, action):
await send_notification(action.data.payload)
ChatAssistant(..., dispatcher=dispatch)
Без него записанный PendingAction остаётся pending (runtime предупреждает),
пока что-нибудь его не дренирует — см. раздел про outbox в
устойчивости.
Рецепт разговора¶
ConversationMessage — канонический артефакт role/text/turn/session_id,
так что вы перестаёте лепить вручную Question/Answer плюс сборщик промпта.
Conversation обобщён над вашей моделью сообщения — подойдёт любая с
role/text:
from reactifact.recipes.conversation import Conversation, ConversationMessage
conv = Conversation() # дефолтная модель
conv = Conversation(MyMessage) # или своя (role/text/доменные поля)
conv.create_turn(ctx, "refund policy?", session_id="s1")
# ...агенты выполняются и пишут сообщение ассистента...
history = conv.transcript(ctx, window=20) # list[providers.Message] для промпта
transcript рендерит поток в провайдерские Message (от старых к новым,
window оставляет последние N); conversation_state — payload истории для
session_state=; номера turn берутся от максимального записанного, поэтому
pruning их не сбрасывает.
У большинства реальных приложений разные типы на две стороны (Question
+ FinalResponse, UserMsg + ChatReply) с ключом корреляции и нетекстовым
payload ассистента. Для них — read-only Transcript + MessageSpec, которые
сливают несколько типизированных ходов по ролям и не трогают артефакты (эту
форму используют примеры repair/devops/knowledge, каждый задавая свой
Transcript([...]) как session_state=):
from reactifact.recipes.conversation import MessageSpec, Transcript
view = Transcript([
MessageSpec(Question, "user"),
MessageSpec(FinalResponse, "assistant", render=render_answer), # contract JSON → проза
])
history = view.messages(ctx, window=20) # list[providers.Message]
# ChatAssistant(..., session_state=view.state)
Ограниченная память¶
Разговор растёт вечно, если его не ограничить. ChatMemory на ChatAssistant
подключает рецепты памяти за вас:
from reactifact.chat import ChatAssistant, ChatMemory
assistant = ChatAssistant(
...,
memory=ChatMemory(
message_type=ConversationMessage,
# message_types=[UserQuery, Answer], # гетерогенный поток / marker-база
keep=20, # держать последние 20 сырых сообщений (WindowPruner)
# или, чтобы сжимать, а не отбрасывать:
# summarize=my_summarize, # (ctx, previous_digest, stale) -> str | None
# summary_type=Summary, # артефакт дайджеста (нужно поле `text`,
# build_summary=lambda t: Summary(text=t), # либо свой builder)
compact_commits=200, # ещё и ограничить лог коммитов (Context.compact)
),
)
Используйте одну форму, не обе: pruning держит сырое окно; суммаризация
сворачивает выпавшее в растущий дайджест и удаляет исходные сообщения.
compact_commits ортогонален — он ограничивает историю
(Устойчивость и resume), а не рабочий набор, и не требует
message_type, поэтому ChatMemory(compact_commits=…) — самодостаточная
политика (безопасный выбор, когда сообщения коррелированы/типизированы, а не
плоский role/text лог, напр. отдельные типы Question/Answer).
Когда поток действительно состоит из нескольких типов артефактов, передайте
message_types=[UserQuery, Answer] — или marker-базу (ChatMessage), от
которой наследуются конкретные типы. Агент памяти просыпается на каждом
перечисленном типе (а для marker-базы — и на её наследниках), поэтому одно
окно ограничивает весь поток; message_type тоже принимает последовательность.
Ходы с человеком в цикле¶
Ход может приостановиться для человека без спецмашинерии — вопрос это просто эффект, а ответ — просто ещё один патч:
# внутри produce
effects.ask("Approve this estimate?", kind="approval")
# позже, из вашего API, когда человек отвечает:
ctx.resume(question_id, "approved") # эмитит update → агенты реагируют
Ожидающий вопрос — это состояние, поэтому он переживает рестарт как всё
остальное: клиент может вернуться с ответом на следующем ходу. См. примеры
devops и repair.
Откат и возобновление¶
- Продолжить прерванный прогон: откройте сессию заново и
arun()— отложенные триггеры восстановятся (Устойчивость и resume). - Откатиться к более ранней точке:
context.checkout(version)(отказывает ниже baseline компакции) илиcontext.branch()для альтернативы без изменения оригинала. - Изучить ход:
context.diff(v1, v2)показывает, что именно изменилось,reactifact replayвосстанавливает почему.
См. также¶
- Устойчивость и resume — сессии, at-least-once, компакция.
- Рецепты — рецепты памяти и рецепт разговора.
examples/chat— одно-модельный рецепт, запускается офлайн.- Quickstart — чат-бот в несколько строк.