f348570b1b
- SPRINTS.md → docs/SPRINTS.md - GRAPH_ARCHITECTURE.md → docs/architecture/GRAPH_ARCHITECTURE_v1.md - GRAPH_ARCHITECTURE_v2.md → docs/architecture/GRAPH_ARCHITECTURE_v2.md - Новый docs/architecture/GRAPH_ARCHITECTURE_v3.md (билингв. термины + ссылки на примеры) - Новые docs/examples/: 01 базовая запись, 02 цена во время записи (soft vs hard), 03 запись ребёнка (guard), 04 простой general_info - README обновлён: ссылки на новые пути + раздел «Документация» Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
207 lines
17 KiB
Markdown
207 lines
17 KiB
Markdown
# Графовая архитектура: роутер намерений + изолированные ветки
|
||
|
||
Документ фиксирует направление, в которое двигается проект после пилота Спринтов 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, встраивается в новую архитектуру.
|