Files
RAG_helper/GRAPH_ARCHITECTURE.md
T
AR 15 M4 907fdbec84 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>
2026-04-23 20:18:05 +05:00

17 KiB
Raw Blame History

Графовая архитектура: роутер намерений + изолированные ветки

Документ фиксирует направление, в которое двигается проект после пилота Спринтов 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, встраивается в новую архитектуру.