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:
AR 15 M4
2026-04-23 20:18:05 +05:00
parent 7ec2ba3c8f
commit 907fdbec84
+206
View File
@@ -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, встраивается в новую архитектуру.