# Графовая архитектура: роутер намерений + изолированные ветки Документ фиксирует направление, в которое двигается проект после пилота Спринтов 1–3. Перепланировка спринтов будет сделана отдельно — здесь только сама архитектура и почему она нам нужна. --- ## Проблема, с которой сталкиваемся Текущая реализация — это «мега-промпт»: в один системный промпт положен весь скрипт поведения агента, плюс правила, плюс инструкции по всем возможным темам (запись, перенос, цены, подготовка к приёму, ДМС, детский приём и т. д.). На MVP это работает. Но как только добавим реальные бизнес-процессы с несколькими этапами (например, запись с перехватом инициативы в 6 шагов) — модель начнёт «плыть»: - **Забывать начало инструкций** в конце длинного промпта. - **Перескакивать этапы** мини-интервью. - **Пытаться применять правила не к месту** — например, запустить скрипт записи, когда пациент просто спросил, как доехать. - **Путать ветки** между собой, потому что они все лежат в одном контексте. Это классическая ловушка production-ready ассистентов. Дело не в мощности модели (DeepSeek более чем достаточно), а в архитектуре: **один промпт не должен знать про всё одновременно**. --- ## Архитектура, к которой идём Паттерн называется **graph-based routing** (или multi-agent system). Идея проста: 1. Входная реплика пациента идёт не сразу в отвечающего агента, а в **роутер**. 2. Роутер определяет **намерение** (intent) и передаёт диалог в конкретную изолированную ветку. 3. Каждая ветка — это отдельный узкий промпт, который умеет делать одну вещь хорошо. 4. Ветки не замкнуты: в любой момент агент может вернуть управление роутеру, если контекст изменился. ``` ┌─────────────┐ │ Пациент │ └──────┬──────┘ │ ┌──────▼──────────────────────────┐ │ Роутер (LLM-классификатор) │ │ определяет намерение │ └──────┬──────────────────────────┘ │ ├──→ Ветка «Новая запись» (скрипт 6 этапов) ├──→ Ветка «Перенос / отмена» ├──→ Ветка «Цены и ДМС» ├──→ Ветка «Хирургия» → сразу передача человеку ├──→ Ветка «Острая боль / медвопрос» → передача человеку └──→ Ветка «Общая справка» (как доехать, часы работы и т. п.) ``` --- ## 1. Роутер (входной узел) Отдельный, быстрый и дешёвый вызов LLM. Не отвечает пациенту сам — только классифицирует. Задача роутера: - Проанализировать последнюю реплику + краткую историю. - Вернуть **intent** — одну из заранее заданных категорий. - При необходимости — передать сигнал «нужен человек» (острая боль, конфликт, хирургия). Пример промпта роутера: > Определи намерение пользователя. Варианты: > 1. `new_booking` — новая запись > 2. `reschedule` — перенос или отмена существующей > 3. `price_question` — цены, ДМС, оплата > 4. `medical_question` — симптомы, диагноз, лечение > 5. `surgery` — хирургическое вмешательство > 6. `general_info` — как доехать, часы работы, контакты > 7. `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]`. Когда оркестратор видит такой сигнал в ответе модели: 1. **Останавливает текущую ветку.** 2. **Передаёт всю историю** в роутер (чтобы пациент не начинал с начала). 3. **Запускает новую ветку** — бесшовно для пользователя. Пример из жизни: - *Пациент:* Запишите меня к лору на завтра. - *Бот (ветка «Новая запись»):* На завтра есть окно в 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 срабатывают там, где ожидается. --- ## Открытые вопросы Решение по ним нужно до следующей перепланировки: 1. **Фреймворк оркестровки.** Писать логику маршрутизации вручную на Python (наш текущий подход) или взять готовое — LangGraph, n8n? Самописное даёт контроль и меньше зависимостей, фреймворк — быстрее старт и встроенная визуализация графа. 2. **Роутер: отдельная LLM-модель или тот же DeepSeek?** Для классификации хватит модели поменьше и подешевле (Haiku, GPT-4o-mini, локальная модель). Это важно: роутер зовётся на каждую реплику, а не один раз за тред. 3. **Как хранить exit conditions.** Текстом в конце системного промпта ветки? Отдельной структурой (список триггеров)? Первое гибче, второе — надёжнее срабатывает. 4. **Где проходит граница между ботом и человеком по хирургии.** Координацией хирургических пациентов (запись на операцию, контроль анализов) занимаются те же операторы контакт-центра, что и обычной записью, или есть отдельный хирургический куратор? От ответа зависит, куда маршрутизируется тред из ветки `surgery`. 5. **Точность роутера.** Нужна ли на старте классификация по уверенности (`confidence score`), fallback на уточняющий вопрос («Правильно понимаю, вы хотите записаться?») при низкой уверенности, или на первом этапе хватает грубой классификации? --- ## Ориентир на следующие спринты Этот документ — **ещё не план**. План будет сверстан отдельно после обсуждения. Но уже видно, что логичный порядок переходов примерно такой: 1. **Разделить «один промпт» на несколько.** Завести таблицу `intents`, сделать в `agent_configs` привязку к `intent_id`. UI настроек — по веткам. 2. **Добавить роутер.** Отдельный вызов LLM перед каждым ответом, возвращает intent. Без state machine пока — просто выбирается нужный промпт. 3. **State machine и exit conditions.** Ветки получают память по шагам и умеют передавать управление обратно. 4. **Мульти-RAG.** Каждая ветка тянет свою коллекцию. 5. **Сценарии и эскалация на оператора с контекстом.** То, что планировалось как Спринт 4, встраивается в новую архитектуру.