Как работает мультиагентная система
Здесь объясняется, что такое ветка (intent), как реплика пациента доходит до ответа, и какие защитные механизмы стоят на пути «петель» и потерянного контекста. Английские термины оставлены в скобках — на них завязан код и логи.
Зачем это всё
На пилоте у нас был «один большой системный промпт» — модель пыталась одновременно записывать на приём, отвечать на вопросы по ценам и эскалировать острые случаи. По мере усложнения скрипта запись начинала «плыть»: модель забывала шаги, путала ветки, перескакивала через мини-интервью.
Мы перешли на графовую архитектуру (graph-based routing): реплика пациента сначала идёт в маршрутизатор (router), который определяет тему, а потом — в ветку (intent), отвечающую только за свой узкий сценарий. У сложных веток внутри есть собственный пошаговый сценарий (state machine).
Главные термины
new_booking, price_question и т. п.) и активная версия настроек._router: отдельный, дешёвый вызов модели, который по последней реплике пациента возвращает один из кодов веток. Не отвечает пациенту напрямую — только классифицирует. Вызывается на КАЖДОЙ реплике, не один раз при входе.new_booking: 6 шагов от приветствия до подтверждения записи. Модель на каждой реплике видит, на каком шаге сейчас, и какие слоты уже собраны.intro, qualify, present, offer_time, book, close), свой кусок промпта и список допустимых переходов.qualify разрешено в qualify (остаться) или present (двигаться вперёд), но не в close. Если модель попытается перепрыгнуть через шаг — валидатор переходов (transition validator) отклонит запрос, мы останемся на шаге.name, reason, specialist, preferred_time, confirmed. Модель на каждой реплике их видит и обновляет — старые не переспрашиваются.[INTENT_CHANGE: <код_ветки>] и передать диалог другой ветке. Например, если пациент в записи упомянул хирургию — ветка new_booking сама вернёт [INTENT_CHANGE: escalate_human].[INTENT_CHANGE] или маршрутизатор предложил другую ветку, которая не имеет пошагового сценария.Защита от ложных переключений внутри пошагового сценария. Это не второй вызов маршрутизатора — один вызов, но с расширенным промптом. Работает пошагово:
- Пациент пишет что-то вроде «а сколько стоит приём?» внутри записи.
- Маршрутизатор анализирует реплику и возвращает:
general_info(в Песочнице — бейдж «роутер предложил: general_info»). - Система видит: тред уже идёт по многошаговой ветке
new_booking, шагqualify. Переключать опасно — потеряем контекст записи. - Вместо переключения:
effective_codeостаётсяnew_booking, в системный промпт ветки добавляется блок:[ПОДСКАЗКА РОУТЕРА] Роутер счёл, что тема — `general_info`. Ты ведёшь сценарий `new_booking`. Если пациент сменил тему — выдай [INTENT_CHANGE: general_info]. Если реплика в сценарии — зафиксируй в слот и продолжай. - Модель
new_bookingполучает весь этот контекст и сама решает:- Ответить на вопрос и остаться → вернёт
{"state_after": "qualify", "soft_insertion": true}→ бейдж «удержались в ветке» + «боковой вопрос». - Решить, что тема реально сменилась → вернёт
[INTENT_CHANGE: general_info]→ жёсткое переключение.
- Ответить на вопрос и остаться → вернёт
Ключевое: решение принимает модель ветки за один вызов, а не отдельный роутер. Подсказка роутера — просто контекст в промпте.
Ситуация, когда модель пошаговой ветки отвечает на вопрос вне сценария, не продвигая шаг и не меняя ветку. Типичные примеры внутри new_booking: «а сколько стоит приём?», «у вас есть парковка?», «как долго идёт приём?».
Откуда система знает, что это боковой вопрос? Модель сама сообщает об этом в структурированном ответе:
STATE_JSON: {"state_after": "qualify", "slots_updated": {}, "soft_insertion": true}
Флаг soft_insertion: true + текущий шаг в state_after + пустые slots_updated = «ответил на отвлечение, сценарий не двинулся».
Счётчик soft_insertion_count в состоянии диалога инкрементируется на каждом таком ответе и сбрасывается при смене шага или ветки. При трёх подряд — в системный промпт ветки добавляется жёсткое указание: «верни пациента к вопросу текущего шага».
В Песочнице: бейдж «тип ответа: боковой вопрос» (жёлтый) на ответе ассистента.
STATE_JSON: {"state_after": "qualify", "slots_updated": {"name": "Алексей"}}
Парсер вырезает этот блок (пациент его не видит), валидатор проверяет легальность state_after, обновляет состояние диалога. Необязательный флаг soft_insertion: true сигнализирует, что это был боковой ответ без продвижения сценария.suspended_intent, resumable_step_code, resumable_slots. Когда маршрутизатор увидит, что пациент возвращается к исходной теме («ладно, продолжаем запись»), мы автоматически восстановим шаг и слоты.Правило, которое блокирует переход шага вперёд, пока не заполнены нужные слоты. Хранится в поле guards каждого шага (Настройки → Шаги → поле Guards). Проверяется после того, как модель вернула корректный state_after — то есть даже если модель «захотела» перейти, валидатор не пустит без нужных данных.
Формат — JSON-объект, где каждый ключ — имя guard'а:
{
"require_legal_rep": {
"description": "Для записи ребёнка нужны ФИО и телефон законного представителя",
"trigger_slot": "is_child",
"trigger_value": true,
"required_slots": ["legal_rep_name", "legal_rep_phone"]
}
}
Поля:
trigger_slot— слот, при значении которого guard активируется. Если опущен — guard активен всегда.trigger_value— значение, которое должен иметьtrigger_slotдля активации (например,true).required_slots— список слотов, которые должны быть заполнены для разрешения перехода.description— пояснение для операторов; показывается в «Состоянии диалога» в Песочнице при срабатывании.
Сейчас guard задан на шаге qualify ветки new_booking: при is_child: true нельзя перейти в present, пока не заполнены legal_rep_name и legal_rep_phone. В Песочнице при срабатывании появляется красный блок: «🔒 guard require_legal_rep не пройден — ждём: legal_rep_name, legal_rep_phone».
handoff_count в состоянии диалога считает все переключения ветки. При превышении 3 переключений за диалог следующее переключение блокируется: диалог автоматически уходит в escalate_human с шаблонным ответом «Уточню детали с администратором клиники, свяжемся с вами в течение ближайшего часа». Это страховка от циклов вроде «запись ↔ цены ↔ запись ↔ цены».handoff_count и поля отложенного сценария. Видно в Песочнице справа, в блоке «Состояние диалога».Что происходит на каждой реплике
suspended_intent и маршрутизатор вернул именно его — восстанавливаем шаг и слоты, очищаем поля, обнуляем счётчик переключений.[ПОДСКАЗКА РОУТЕРА].[INTENT_CHANGE] — переключаемся в новую ветку (если из sm-ветки — запоминаем в отложенный сценарий) и зовём модель ещё раз. Если есть STATE_JSON: — валидируем переход, обновляем шаг и сливаем слоты.Маршрутизатор обрабатывает каждую реплику пациента, но результат его решения материализуется в следующем ответе ассистента: именно там формируется ветка, шаг, события (sticky, боковой вопрос и т.д.). Поэтому в Песочнице бейджи стоят под ответом бота, а не над вопросом пациента — они описывают обработку предшествующей реплики.
Защитные механизмы
- Удержание в ветке защищает от ложного сброса сценария на коротких репликах вроде «Алексей» или «болит ухо».
- Валидатор переходов блокирует «прыжки через шаг» — модель не сможет уйти из
introсразу вbook. - Guard (условие перехода) блокирует переход вперёд до заполнения обязательных слотов. Настраивается в поле Guards каждого шага. Пример: при записи ребёнка нельзя уйти с шага
qualify, пока не указаны ФИО и телефон родителя. - Защита от петли ограничивает число переключений ветки за диалог. После 3-го — авто-перевод на оператора.
- Отложенный сценарий возвращает прерванный сценарий с теми же слотами и шагом — пациент не должен повторять имя или повод.
- Ретрай LLM: и маршрутизатор, и ветка делают один повтор при сетевом сбое DeepSeek. При полном падении — откат транзакции и понятный ответ «модель временно недоступна».
Где что настраивается
- Настройки — список веток, активные версии промптов, поля «Системный промпт», «Правила», «Условия выхода». Для веток с пошаговым сценарием — вкладка «Шаги» с редактором каждого шага и его допустимых переходов.
- Песочница — живые диалоги от лица пациента. В правой панели видны: состояние диалога (ветка, шаг, слоты, счётчик переключений, отложенный сценарий), решение маршрутизатора, RAG-фрагменты и собранный системный промпт.
- Отладка — база знаний (загрузка / переразметка документов), одиночные тестовые вопросы без памяти диалога.