docs: GRAPH_ARCHITECTURE — графовая архитектура (роутер + ветки)
Фиксируем направление, в которое движется проект после пилота Спринтов 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>
This commit is contained in:
@@ -0,0 +1,206 @@
|
|||||||
|
# Графовая архитектура: роутер намерений + изолированные ветки
|
||||||
|
|
||||||
|
Документ фиксирует направление, в которое двигается проект после пилота Спринтов 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, встраивается в новую архитектуру.
|
||||||
Reference in New Issue
Block a user