Фиксируем направление, в которое движется проект после пилота Спринтов 1–3. Перепланировка спринтов будет сделана отдельно. Ключевая идея: отказаться от «мега-промпта», где один системный промпт знает про всё (запись, перенос, цены, ДМС, детский приём, хирургию), и перейти на graph-based routing — входная реплика идёт через LLM-роутер, который определяет намерение и передаёт диалог в узкую изолированную ветку со своим промптом, своей базой знаний и своим шагом state machine. Роутер продолжает незримо присутствовать в диалоге и перекидывает тред между ветками, когда срабатывают exit conditions (пациент меняет тему на лету). Документ описывает: - Почему мега-промпт перестаёт работать по мере роста сценариев. - Роутер как отдельный дешёвый вызов LLM на каждой реплике. - Узкие ветки (new_booking, reschedule, surgery, medical_question, general_info, escalate_human). - State machine внутри ветки с хранением шага и собранных слотов. - Exit conditions и bouncing между ветками через служебные сигналы модели ([INTENT_CHANGE: ...]). - Передача человеку не как «сброс», а как квалификация лида с полным контекстом. - Что меняется в данных (таблицы intents, thread_state, routing_log; agent_configs привязывается к intent_id; несколько RAG-коллекций). - Что меняется в UI (Настройки по веткам; в Песочнице виден текущий intent, шаг, история переходов). - Открытые вопросы: фреймворк оркестровки (LangGraph/n8n или вручную), выбор модели для роутера, формат exit conditions, граница бот/человек по хирургии, работа с уверенностью. Направление подтверждает memory-записку project_future_architecture от 2026-04-23 (мульти-пользователи, мульти-промпты, несколько специализированных RAG + RAG-маршрутизатор) и детализирует её. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
17 KiB
Графовая архитектура: роутер намерений + изолированные ветки
Документ фиксирует направление, в которое двигается проект после пилота Спринтов 1–3. Перепланировка спринтов будет сделана отдельно — здесь только сама архитектура и почему она нам нужна.
Проблема, с которой сталкиваемся
Текущая реализация — это «мега-промпт»: в один системный промпт положен весь скрипт поведения агента, плюс правила, плюс инструкции по всем возможным темам (запись, перенос, цены, подготовка к приёму, ДМС, детский приём и т. д.).
На MVP это работает. Но как только добавим реальные бизнес-процессы с несколькими этапами (например, запись с перехватом инициативы в 6 шагов) — модель начнёт «плыть»:
- Забывать начало инструкций в конце длинного промпта.
- Перескакивать этапы мини-интервью.
- Пытаться применять правила не к месту — например, запустить скрипт записи, когда пациент просто спросил, как доехать.
- Путать ветки между собой, потому что они все лежат в одном контексте.
Это классическая ловушка production-ready ассистентов. Дело не в мощности модели (DeepSeek более чем достаточно), а в архитектуре: один промпт не должен знать про всё одновременно.
Архитектура, к которой идём
Паттерн называется graph-based routing (или multi-agent system). Идея проста:
- Входная реплика пациента идёт не сразу в отвечающего агента, а в роутер.
- Роутер определяет намерение (intent) и передаёт диалог в конкретную изолированную ветку.
- Каждая ветка — это отдельный узкий промпт, который умеет делать одну вещь хорошо.
- Ветки не замкнуты: в любой момент агент может вернуть управление роутеру, если контекст изменился.
┌─────────────┐
│ Пациент │
└──────┬──────┘
│
┌──────▼──────────────────────────┐
│ Роутер (LLM-классификатор) │
│ определяет намерение │
└──────┬──────────────────────────┘
│
├──→ Ветка «Новая запись» (скрипт 6 этапов)
├──→ Ветка «Перенос / отмена»
├──→ Ветка «Цены и ДМС»
├──→ Ветка «Хирургия» → сразу передача человеку
├──→ Ветка «Острая боль / медвопрос» → передача человеку
└──→ Ветка «Общая справка» (как доехать, часы работы и т. п.)
1. Роутер (входной узел)
Отдельный, быстрый и дешёвый вызов LLM. Не отвечает пациенту сам — только классифицирует.
Задача роутера:
- Проанализировать последнюю реплику + краткую историю.
- Вернуть intent — одну из заранее заданных категорий.
- При необходимости — передать сигнал «нужен человек» (острая боль, конфликт, хирургия).
Пример промпта роутера:
Определи намерение пользователя. Варианты:
new_booking— новая записьreschedule— перенос или отмена существующейprice_question— цены, ДМС, оплатаmedical_question— симптомы, диагноз, лечениеsurgery— хирургическое вмешательствоgeneral_info— как доехать, часы работы, контактыescalate_human— пациент явно просит оператора или злитсяВерни только код намерения.
Роутер продолжает незримо присутствовать в диалоге — его вызывают на каждой реплике, не один раз при входе.
2. Узкоспециализированные ветки (sub-agents)
Каждая ветка — отдельный промпт, который не знает про другие ветки. Он видит:
- Свой системный промпт (узкий, под одну задачу).
- Свою базу знаний (специализированный RAG — эта коллекция, не общая).
- Историю диалога (чтобы не переспрашивать имя/симптомы).
- Текущий шаг state machine (см. ниже).
Примеры:
Ветка «Новая запись». 6-этапный промпт-продавец. Перехват инициативы, мини-интервью по симптомам, презентация двух слотов, бронирование.
Ветка «Перенос / отмена». Другой промпт: извиниться, уточнить текущую запись, сверить с календарём, предложить варианты.
Ветка «Хирургия». Короткая: «секунду, перевожу на координатора хирургии». Никакой попытки вести диалог — сразу передача. Запись на операцию — это другой JTBD, другой уровень стресса и чек, его не стоит отдавать боту.
Ветка «Острая боль / медвопрос». Тоже короткая: извинение, предложение записаться к врачу, эскалация на оператора.
3. State machine внутри ветки
Для сложных скриптов (вроде записи в 6 шагов) недостаточно иметь промпт — нужна ещё память о том, на каком шаге мы сейчас находимся.
Пример состояния:
{
"intent": "new_booking",
"step": 3, // «Презентация слотов»
"slots_shown": ["2026-04-24 10:00", "2026-04-24 15:00"],
"patient_name": "Анна",
"reason": "заложенность носа"
}
Модель на каждом шаге видит: «Я на шаге 3, значит следующим сообщением я должна предложить выбор времени без лишних уточнений». Это убирает «перескоки» и «забывания».
State хранится в таблице треда (можно использовать уже имеющуюся threads или добавить thread_state с JSON-колонкой).
4. Exit conditions: динамическая маршрутизация
Главная проблема «жёстких скриптов» — невозможность сменить тему на лету. Пациент — живой человек, он может вспомнить важную деталь посреди диалога. Решение:
Каждая ветка знает не только как вести, но и когда выйти. В системный промпт ветки зашивается блок «условий выхода»:
Если в любой момент пациент упоминает операцию, наркоз, стационар, удаление гланд, септопластику, стапедопластику — прекрати скрипт записи и выдай служебный сигнал:
[INTENT_CHANGE: surgery].
Когда оркестратор видит такой сигнал в ответе модели:
- Останавливает текущую ветку.
- Передаёт всю историю в роутер (чтобы пациент не начинал с начала).
- Запускает новую ветку — бесшовно для пользователя.
Пример из жизни:
- Пациент: Запишите меня к лору на завтра.
- Бот (ветка «Новая запись»): На завтра есть окно в 15:00. Бронируем?
- Пациент: Да, давайте. А он посмотрит мои снимки? Мне сказали, нужна операция на перегородке.
- (Exit condition срабатывает: это хирургия → переход в ветку «Surgery» → передача человеку.)
- Система: Поняла вас. Планирование операций требует отдельного приёма для изучения КТ. Секунду, передаю координатору хирургии.
5. Передача человеку (escalation)
Часть веток не пытаются вести диалог до конца — они маршрутизируют пациента в контакт-центр. Важное отличие от текущей реализации: система не просто скидывает диалог, а отдаёт оператору контекст:
- Полную историю переписки.
- Распознанный intent («горячий лид на хирургию», «острая боль», «жалоба»).
- Паспортные данные пациента, если он их уже назвал.
Это превращает ассистента не в «фильтр перед оператором», а в инструмент квалификации лида.
Что это меняет в данных
Сейчас в БД:
threads,messages— диалоги (Спринт 2).agent_configs— один активный системный промпт на всё (Спринт 3).
После перехода на графовую архитектуру понадобится:
intents— справочник веток (код, имя, описание, статус активно/выключено).agent_configsрастёт: каждый конфиг привязан кintent_id, у каждой ветки — свой текущий активный промпт и свой набор exit conditions. Активен не «один промпт», а набор промптов по веткам.thread_state— текущий intent треда, шаг state machine, собранные слоты (имя, симптом, выбранное время и т. п.).- Несколько RAG-коллекций вместо одной: под каждую ветку свой срез базы знаний. Уже заложено как направление в памяти —
project_future_architecture.md. routing_log(опционально) — лог решений роутера: интент, уверенность, срабатывание exit condition. Нужен для отладки и тюнинга.
Что это меняет в UI
- «Настройки агента» превращаются в настройки веток: слева список веток, справа редактор промпта и exit conditions для выбранной ветки.
- В «Песочнице» отладочная панель показывает не только найденные чанки и собранный промпт, но и: текущий intent, шаг state machine, историю переходов между ветками в рамках треда.
- «Сценарии» (то, что планировалось в Спринте 4) становятся ценнее: можно прогонять не просто «диалог агента», а проверять, что роутер правильно классифицирует намерение и что exit conditions срабатывают там, где ожидается.
Открытые вопросы
Решение по ним нужно до следующей перепланировки:
-
Фреймворк оркестровки. Писать логику маршрутизации вручную на Python (наш текущий подход) или взять готовое — LangGraph, n8n? Самописное даёт контроль и меньше зависимостей, фреймворк — быстрее старт и встроенная визуализация графа.
-
Роутер: отдельная LLM-модель или тот же DeepSeek? Для классификации хватит модели поменьше и подешевле (Haiku, GPT-4o-mini, локальная модель). Это важно: роутер зовётся на каждую реплику, а не один раз за тред.
-
Как хранить exit conditions. Текстом в конце системного промпта ветки? Отдельной структурой (список триггеров)? Первое гибче, второе — надёжнее срабатывает.
-
Где проходит граница между ботом и человеком по хирургии. Координацией хирургических пациентов (запись на операцию, контроль анализов) занимаются те же операторы контакт-центра, что и обычной записью, или есть отдельный хирургический куратор? От ответа зависит, куда маршрутизируется тред из ветки
surgery. -
Точность роутера. Нужна ли на старте классификация по уверенности (
confidence score), fallback на уточняющий вопрос («Правильно понимаю, вы хотите записаться?») при низкой уверенности, или на первом этапе хватает грубой классификации?
Ориентир на следующие спринты
Этот документ — ещё не план. План будет сверстан отдельно после обсуждения. Но уже видно, что логичный порядок переходов примерно такой:
- Разделить «один промпт» на несколько. Завести таблицу
intents, сделать вagent_configsпривязку кintent_id. UI настроек — по веткам. - Добавить роутер. Отдельный вызов LLM перед каждым ответом, возвращает intent. Без state machine пока — просто выбирается нужный промпт.
- State machine и exit conditions. Ветки получают память по шагам и умеют передавать управление обратно.
- Мульти-RAG. Каждая ветка тянет свою коллекцию.
- Сценарии и эскалация на оператора с контекстом. То, что планировалось как Спринт 4, встраивается в новую архитектуру.