From 907fdbec84b09398c1b696777f3e0f4f06c4f7fa Mon Sep 17 00:00:00 2001 From: AR 15 M4 Date: Thu, 23 Apr 2026 20:18:05 +0500 Subject: [PATCH] =?UTF-8?q?docs:=20GRAPH=5FARCHITECTURE=20=E2=80=94=20?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D1=84=D0=BE=D0=B2=D0=B0=D1=8F=20=D0=B0=D1=80?= =?UTF-8?q?=D1=85=D0=B8=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80=D0=B0=20(?= =?UTF-8?q?=D1=80=D0=BE=D1=83=D1=82=D0=B5=D1=80=20+=20=D0=B2=D0=B5=D1=82?= =?UTF-8?q?=D0=BA=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Фиксируем направление, в которое движется проект после пилота Спринтов 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) --- GRAPH_ARCHITECTURE.md | 206 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 GRAPH_ARCHITECTURE.md diff --git a/GRAPH_ARCHITECTURE.md b/GRAPH_ARCHITECTURE.md new file mode 100644 index 0000000..34a0020 --- /dev/null +++ b/GRAPH_ARCHITECTURE.md @@ -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, встраивается в новую архитектуру.