From 248cb37f8ade31d3b7b8587a6df4ff3ab503a68c Mon Sep 17 00:00:00 2001 From: AR 15 M4 Date: Fri, 24 Apr 2026 20:17:38 +0500 Subject: [PATCH] =?UTF-8?q?docs:=20GRAPH=5FARCHITECTURE=20v2=20+=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=B1=D0=B8=D0=B2=D0=BA=D0=B0=20=D0=A1=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D0=BD=D1=82=D0=B0=206=20=D0=BD=D0=B0=206a/6b=20=D1=81=20?= =?UTF-8?q?UI-=D1=87=D0=B5=D0=BA=D0=BF=D0=BE=D0=B9=D0=BD=D1=82=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавлен GRAPH_ARCHITECTURE_v2.md (уточнения v1 после анализа скрипта записи в вике клиники: различение soft-insertion и hard-handoff, защита от петель через handoff_count, resumable state, guards в new_booking, подписка ветки на разделы вики как альтернатива отдельным коллекциям Chroma, per-step RAG, eval-набор до Спринта 5, разбивка Спринта 5 на 5a/5b). SPRINTS.md переработан: - Спринт 5 закрыт как «ядро v1» с явным списком того, что не вошло из v2. - Спринт 6 разбит на 6a (структурированный выход + intent_steps + валидатор переходов + exit_conditions_text + handoff_count + suspended/resumable) и 6b (soft-insertion + guards + reason в escalate_human + умный роутер + 8 ручных сценариев). - В каждом блоке — UI-чекпойнт и явное «что проверяем глазами», чтобы можно было смотреть результат после каждого шага, а не в конце спринта. - Мульти-RAG (Спринт 7) делается до мини-eval (Спринт 8), чтобы наборы в eval проверяли поведение уже с per-intent retrieval. - Зафиксированы 4 принятых решения: момент обновления current_step, cap на soft-insertion, сверка шагов new_booking с вики, формат структурированного выхода — JSON-блок в хвосте ответа. Co-Authored-By: Claude Opus 4.7 (1M context) --- GRAPH_ARCHITECTURE_v2.md | 405 +++++++++++++++++++++++++++++++++++++++ SPRINTS.md | 250 +++++++++++++++++++++--- 2 files changed, 629 insertions(+), 26 deletions(-) create mode 100644 GRAPH_ARCHITECTURE_v2.md diff --git a/GRAPH_ARCHITECTURE_v2.md b/GRAPH_ARCHITECTURE_v2.md new file mode 100644 index 0000000..8f631fc --- /dev/null +++ b/GRAPH_ARCHITECTURE_v2.md @@ -0,0 +1,405 @@ +# Графовая архитектура: роутер намерений + изолированные ветки + +> **Версия 2 · 2026-04-24.** Уточнения после обсуждения и анализа скрипта записи в вики клиники. Основные добавления относительно v1: различение *soft-insertion* и *hard-handoff*, защита от петель маршрутизации, resumable state при возврате, guards внутри ветки `new_booking`, альтернативный дизайн мульти-RAG (подписка на разделы вики), RAG-срез на уровне шага, позиционирование eval-набора до Спринта 5. Полный список изменений — в разделе **Changelog** в конце документа. + +Документ фиксирует направление, в которое двигается проект после пилота Спринтов 1–3. Перепланировка спринтов сделана в `SPRINTS.md` — здесь только сама архитектура и почему она нам нужна. + +--- + +## Проблема, с которой сталкиваемся + +Текущая реализация — это «мега-промпт»: в один системный промпт положен весь скрипт поведения агента, плюс правила, плюс инструкции по всем возможным темам (запись, перенос, цены, подготовка к приёму, ДМС, детский приём и т. д.). + +На MVP это работает. Но как только добавим реальные бизнес-процессы с несколькими этапами (например, запись с перехватом инициативы в 6 шагов) — модель начнёт «плыть»: + +- **Забывать начало инструкций** в конце длинного промпта. +- **Перескакивать этапы** мини-интервью. +- **Пытаться применять правила не к месту** — например, запустить скрипт записи, когда пациент просто спросил, как доехать. +- **Путать ветки** между собой, потому что они все лежат в одном контексте. + +Это классическая ловушка production-ready ассистентов. Дело не в мощности модели (DeepSeek более чем достаточно), а в архитектуре: **один промпт не должен знать про всё одновременно**. + +--- + +## Архитектура, к которой идём + +Паттерн называется **graph-based routing** (или multi-agent system). Идея проста: + +1. Входная реплика пациента идёт не сразу в отвечающего агента, а в **роутер**. +2. Роутер определяет **намерение** (intent) и передаёт диалог в конкретную изолированную ветку. +3. Каждая ветка — это отдельный узкий промпт, который умеет делать одну вещь хорошо. +4. Ветки не замкнуты: в любой момент агент может вернуть управление роутеру, если контекст изменился. + +``` +┌─────────────┐ +│ Пациент │ +└──────┬──────┘ + │ +┌──────▼──────────────────────────┐ +│ Роутер (LLM-классификатор) │ +│ определяет намерение │ +└──────┬──────────────────────────┘ + │ + ├──→ Ветка «Новая запись» (state machine, 6 шагов + guards) + ├──→ Ветка «Перенос / отмена» + ├──→ Ветка «Цены и ДМС» + ├──→ Ветка «Медицинский вопрос» (канонический ответ → запись) + ├──→ Ветка «Общая справка» (как доехать, часы работы) + └──→ Ветка «Эскалация» reason: surgery | acute_pain | + angry | explicit_request | + routing_loop +``` + +Шесть веток — в точности то, что сидится при первом запуске Спринта 4. Хирургия и острая боль не отдельные ветки, а поле `reason` внутри `escalate_human` — так решили на развороте 2026-04-23. + +--- + +## 1. Роутер (входной узел) + +Отдельный, быстрый и дешёвый вызов LLM. Не отвечает пациенту сам — только классифицирует. + +Задача роутера: + +- Проанализировать последнюю реплику + краткую историю. +- Вернуть **intent** — одну из заранее заданных категорий. +- Если детектирован острый случай (боль, кровотечение, упоминание операции) — маршрутизировать в `escalate_human` с соответствующим `reason`. + +Пример промпта роутера: + +> Определи намерение пользователя. Варианты: +> 1. `new_booking` — новая запись +> 2. `reschedule` — перенос или отмена существующей +> 3. `price_question` — цены, ДМС, оплата +> 4. `medical_question` — симптомы, диагноз, лечение (немедленная эскалация не требуется) +> 5. `general_info` — как доехать, часы работы, контакты +> 6. `escalate_human` — пациент явно просит оператора, злится, описывает острое состояние, упоминает операцию +> +> Верни только код намерения. Для `escalate_human` дополнительно верни `reason` из списка: `acute_pain`, `surgery`, `angry`, `explicit_request`. + +Роутер продолжает **незримо присутствовать** в диалоге — его вызывают на каждой реплике, не один раз при входе. Это двойная защита: если ветка не поймала exit condition сама, роутер увидит смещение intent'а и инициирует handoff. + +--- + +## 2. Узкоспециализированные ветки (sub-agents) + +Каждая ветка — отдельный промпт, который не знает про другие ветки. Он видит: + +- Свой системный промпт (узкий, под одну задачу). +- Свой срез базы знаний (см. раздел 6). +- Историю диалога (чтобы не переспрашивать имя/симптомы). +- Текущий шаг state machine (если она в этой ветке есть). + +Примеры: + +**Ветка «Новая запись».** 6-этапный промпт-продавец с guard'ами. Перехват инициативы, мини-интервью по услуге и врачу, презентация приёма, два слота + «настоять на записи», бронирование, закрытие с проговариванием даты/врача/адреса/стоимости. Подробно — в разделе 3. + +**Ветка «Перенос / отмена».** Другой промпт: извиниться, уточнить текущую запись, сверить с календарём, предложить варианты. RAG не используется — работа через CRM tool-calls. + +**Ветка «Медицинский вопрос».** Канонический ответ: «не могу консультировать, это к врачу. Записать вас к профильному специалисту?» — с мягким переходом в `new_booking`. Никакого RAG по медицинским темам намеренно. + +**Ветка «Эскалация».** Короткая: извиниться, передать оператору. Перед передачей формируется саммари с `reason`, историей, собранными слотами. + +--- + +## 3. State machine внутри ветки + +Для сложных скриптов (вроде записи) недостаточно иметь промпт — нужна ещё память о том, **на каком шаге мы сейчас находимся**. + +### 3.1 Базовая линейная цепочка + +Пример состояния для `new_booking`: + +```json +{ + "intent": "new_booking", + "step": "offer_time", + "slots": { + "patient_name": "Анна", + "is_child": false, + "service": "первичный ЛОР", + "doctor": "Сушков М. Г.", + "time_candidates": ["2026-04-24 10:00", "2026-04-24 15:00"], + "time_chosen": null + } +} +``` + +Модель на каждом ходе видит: *«Я на шаге `offer_time`, слоты `time_candidates` заполнены, значит следующим сообщением я должна получить выбор времени, а не представляться заново»*. Это убирает «перескоки» и «забывания». + +State хранится в отдельной таблице `thread_state` с JSON-колонкой под слоты (см. раздел «Что это меняет в данных»). + +### 3.2 Guards и ветвления внутри скрипта + +Линейная цепочка из шести шагов — идеальный случай. В реальном скрипте записи (см. вики) есть как минимум три guard'а, которые ломают линейность: + +- **Пациент — ребёнок.** На шаге `qualify` обязательно собрать ФИО и телефон законного представителя. Блокирует переход в `present`, пока слоты не заполнены. Юридическое требование, не косметика. +- **Запрос конкретного врача (напр., Ворончихина).** Вместо шага `offer_time` диалог уходит в рукав `waitlist`: запись в лист ожидания вместо предложения слотов. +- **Жалоба на слух без обследования у сурдолога.** На шаге `present` модель должна предложить записаться сначала к сурдологу, и только потом — к отоневрологу. + +Моделировать это можно двумя способами: + +— **Условные переходы.** Шаг `qualify` имеет два возможных next-step'а: `present` (обычно) или `collect_legal_rep` (если `is_child=true`), и только после заполнения переходит дальше. + +— **Под-состояния.** Внутри `qualify` есть `qualify.base` и `qualify.legal_rep`, последнее активируется при `is_child=true`. + +Рекомендую первый вариант — он проще и легче тестируется. + +### 3.3 Структурированный выход модели + валидатор переходов + +Чисто LLM-управляемые переходы («в промпте написано: если слот заполнен, переходи к следующему шагу») фрагильны: модель периодически «не замечает» заполнение слота и застревает или, наоборот, прыгает через шаг. + +Гибридный подход надёжнее. Модель возвращает структурированный ответ: + +```json +{ + "reply": "Записала вас на четверг, 10:00...", + "state_after": "close", + "slots_updated": { + "time_chosen": "2026-04-24 10:00" + } +} +``` + +Код: + +1. **Валидирует легальность перехода** — `offer_time → close` допустим, `intro → book` нет. +2. **Сохраняет слоты строго** — что модель обновила, то и попало в `thread_state`. +3. **Логирует несоответствия** — если модель вернула несуществующее `state_after`, состояние остаётся прежним, в лог пишется предупреждение. + +Модель рассуждает содержательно, код защищает механически. Прибавка — около 50 строк валидатора, снижение нестабильности — сильное. + +### 3.4 RAG на уровне шага, а не только ветки + +Разным шагам одной ветки нужны разные куски вики. Для `new_booking`: + +| Шаг | RAG-срез | Tool | +|-----|----------|------| +| `intro` | — | — | +| `qualify` | `/wiki/services/**`, `/wiki/doctors/**` | — | +| `present` | `/wiki/services/**`, `/wiki/doctors/**`, `/wiki/preparation/**` | — | +| `offer_time` | `/wiki/services/**` (для боковых вопросов) | `crm.get_slots` | +| `book` | — | `crm.create_booking` | +| `close` | `/wiki/contacts/**`, `/wiki/preparation/**` | — | + +Поле `wiki_sources` имеет смысл определять на уровне шага, а не только ветки. Ветка задаёт дефолт, шаг может его сузить или расширить. + +--- + +## 4. Exit conditions: динамическая маршрутизация + +### 4.1 Жёсткий handoff + +Каждая ветка знает не только **как вести**, но и **когда выйти**. В системный промпт ветки зашивается блок «условий выхода»: + +> Если в любой момент пациент упоминает операцию, наркоз, стационар, удаление гланд, септопластику, стапедопластику — прекрати скрипт записи и выдай служебный сигнал: `[INTENT_CHANGE: escalate_human]` с `reason=surgery`. + +Когда оркестратор видит такой сигнал в ответе модели: + +1. **Останавливает текущую ветку.** +2. **Сохраняет текущее состояние** как `suspended_intent` + `resumable_step` + `resumable_slots` (см. 4.4). +3. **Передаёт всю историю** в роутер. +4. **Запускает новую ветку** — бесшовно для пользователя. + +### 4.2 Мягкая вставка — боковой вопрос без выхода + +Не каждое отклонение от темы — это handoff. Частый случай: пациент посреди записи спрашивает «а сколько это стоит?» или «где вы находитесь?». Это не смена темы, это короткий параллельный вопрос, после которого нужно продолжить скрипт записи с того же шага. + +Различие: + +- **Мягкая вставка** — на вопрос можно ответить *одной репликой* без запуска собственной машины состояний. Цена услуги, адрес, длительность приёма, требования к документам. Ветка отвечает сама, `current_step` не меняется. +- **Жёсткий handoff** — вопрос сам по себе требует процесса (перенос существующей записи, запись другого человека, хирургия). Полный выход к роутеру. + +Практически: ветка `new_booking` имеет *read-only* доступ к RAG-срезам `price` и `info`, и в её промпте прописано правило: «короткие боковые вопросы отвечай сам, не покидая шаг». Модели этого обычно достаточно; если проскакивает ошибка — двойной прогон роутера поймает её. + +### 4.3 Защита от петель: `handoff_count` + +Без ограничения легко получить цикл «`booking` → `price` → `booking` → `price`» на несогласованных промптах. Поэтому в `thread_state` заводится счётчик: + +- `handoff_count` инкрементится при каждом жёстком handoff. +- Кап — 2–3 переключения за сессию. +- При превышении — автоматическая маршрутизация в `escalate_human` с `reason=routing_loop`. + +Это дешёвая страховка, которая окупается на первом же багованном промпте. + +### 4.4 Возобновление после handoff: `suspended_intent` + `resumable_state` + +Если ветка вышла по soft-handoff'у для короткого ответа — ок, через мгновение продолжает. Если произошёл жёсткий handoff и detour-ветка закрылась — пациент часто возвращается к исходной задаче. Пример: + +- Пациент в `new_booking` на шаге `offer_time`. +- Переспросил про цену — ушли в `price_question`. +- Получил ответ, говорит «ок, тогда бронируем на четверг». +- Должен вернуться в `new_booking` на шаг `offer_time`, не в `intro`. + +Для этого при выходе из ветки в `thread_state` сохраняются: + +```json +{ + "current_intent": "price_question", + "current_step": null, + "suspended_intent": "new_booking", + "resumable_step": "offer_time", + "resumable_slots": { /* копия slots new_booking */ } +} +``` + +Роутер, приняв решение о возврате, восстанавливает `current_intent` из `suspended_intent`, `current_step` из `resumable_step`, слоты — из `resumable_slots`. + +--- + +## 5. Передача человеку (escalation) + +Часть сценариев не заканчивается в боте — агент **маршрутизирует пациента в контакт-центр**. Важное отличие от «просто сбросить диалог» — система отдаёт оператору **полный контекст**: + +- Полную историю переписки. +- Распознанный intent + `reason` (из списка `acute_pain` / `surgery` / `angry` / `explicit_request` / `routing_loop`). +- Собранные слоты, если они уже есть (ФИО, телефон, услуга, предпочитаемый врач). +- Флаг `suspended_intent`, если эскалация прервала другую ветку. + +Это превращает ассистента не в «фильтр перед оператором», а в инструмент **квалификации лида**. Дальнейшая маршрутизация (какому именно оператору, в какую очередь) — задача смежного разработчика при подключении каналов. + +--- + +## 6. RAG: выбор между коллекциями и подпиской на разделы вики + +Здесь два технически рабочих подхода с очень разными эксплуатационными свойствами. + +### Вариант А: отдельная коллекция на ветку + +(как описано в v1, как запланировано в Спринте 6.) + +- Каждая ветка имеет собственную Chroma-коллекцию. +- Загрузка документа требует выбора ветки. +- Поле `collection_name` в `intents`. +- **Плюсы:** жёсткая изоляция по умолчанию, простой query-путь. +- **Минусы:** дублирование (одна статья wiki часто нужна нескольким веткам); лишнее решение на каждый upload; сложнее поддерживать при росте вики. + +### Вариант Б: одна коллекция + подписка ветки на разделы + +- Одна общая Chroma-коллекция `clinic_wiki`. +- В `intents` поле `wiki_sources: list[str]` — список префиксов путей или набор документ-ID. +- Retriever применяет where-фильтр по метаданным чанка (`doc_path STARTS WITH any(...)`). +- Один документ, нужный нескольким веткам, перечисляется в `wiki_sources` нескольких веток — физического дублирования нет. +- **Плюсы:** структура вики = источник истины; новая страница в `/wiki/pricing/` автоматически попадает в `price_question` без правок конфига; операторы и так ведут вики — не добавляется отдельный процесс тегирования. +- **Минусы:** требует дисциплины в структуре папок вики. + +**Рекомендация для проекта — Вариант Б.** Причина: вики у клиники уже атомарная, регулярно обновляемая, с осмысленной структурой. Добавлять поверх неё тегирование чанков или физическую фрагментацию по коллекциям — это второй слой, который будет расходиться с первым. При Варианте Б «источник правды» один — сама вика. + +### Дополнительно: `wiki_sources` на уровне шага + +Внутри ветки `new_booking` разным шагам нужны разные срезы (см. 3.4). Это решается тем, что поле `wiki_sources` существует на двух уровнях: + +- на `intents` — дефолт для ветки; +- на шаге state machine — уточнение/сужение для конкретного состояния. + +--- + +## Что это меняет в данных + +Сейчас в БД: + +- `threads`, `messages` — диалоги (Спринт 2). +- `agent_configs` — один активный системный промпт на всё (Спринт 3). +- `intents` — справочник веток (Спринт 4). + +После полного перехода на графовую архитектуру понадобится: + +- **`intents`** — добавить поле `wiki_sources: list[str]` для Варианта Б мульти-RAG. +- **`agent_configs`** — привязан к `intent_id`, у каждой ветки свой активный промпт и свои exit conditions (уже заложено в Спринте 4). +- **`thread_state`** — текущее состояние треда: + - `thread_id` (PK, FK) + - `current_intent` + - `current_step` + - `slots` (JSON) + - `handoff_count` (int, default 0) — защита от петель + - `suspended_intent` (nullable) — ветка, из которой вышли по handoff'у + - `resumable_step` (nullable) — шаг в `suspended_intent`, куда возвращаться + - `resumable_slots` (JSON, nullable) — слоты той ветки + - `updated_at` +- **State machine на ветке** — для `new_booking` справочник шагов + допустимых переходов (может быть в коде или в БД, на старте достаточно в коде). +- **`routing_log`** (опционально) — лог решений роутера: intent, срабатывание exit condition, инкремент `handoff_count`. Нужен для отладки и тюнинга. + +--- + +## Что это меняет в UI + +- «Настройки агента» — настройки веток: слева список веток, справа редактор промпта и exit conditions для выбранной ветки. Для веток с state machine — дополнительная вкладка со списком шагов и их промптами. +- В «Песочнице» отладочная панель показывает: **текущий intent**, **шаг state machine**, **собранные слоты**, **handoff_count**, **suspended_intent** (если есть), **историю переходов между ветками** в рамках треда. +- «Сценарии» (Спринт 7) прогоняют не только диалог, но и проверяют: правильно ли роутер классифицировал intent на каждой реплике, корректно ли сработали exit conditions, восстановилось ли состояние после detour'а. + +--- + +## 7. Eval-набор нужен до Спринта 5 + +В плане Спринт 7 — полноценная подсистема сценариев. Это правильная цель, но реализация bouncing в Спринте 5 требует минимального eval-набора уже на входе. Иначе реализуем handoff «на глазок», без способа понять, стало лучше или хуже после правки промпта. + +Минимум: + +- **Eval роутера.** 20–30 фраз на каждую ветку: типичные, пограничные (ловушечные), злые (опечатки, короткие, эмоциональные). Формат: CSV `фраза, ожидаемый_intent`. +- **Eval handoff'а.** 5–10 многошаговых мини-диалогов: intent на реплике 1 → пациент сменил тему на реплике 2 → на реплике 3 проверяем, что ветка ушла на роутер и роутер правильно переключил. +- **Eval resumable.** 3–5 сценариев: detour → возврат. Проверяем, что `current_step` восстановился. + +Реализация — короткий скрипт, прогоняющий набор через `/chat` и сравнивающий решения. Будет заменён полноценной подсистемой Спринта 7, но до этого закроет ~80% регрессий. + +--- + +## Открытые вопросы + +Часть вопросов из v1 закрылась на развороте 2026-04-23. Актуальный список: + +1. **Фреймворк оркестровки** — решено: пишем вручную на Python. LangGraph/n8n не берём. +2. **Роутер — отдельная модель** — отложено: пока DeepSeek через отдельный `RouterClient`, чтобы сменить модель в одном месте. Пересмотрим, когда вызовов станет много. +3. **Формат exit conditions** — текстом в промпте ветки + независимый роутер на каждой реплике. Если реальный прогон покажет, что свободный текст пропускает случаи смены темы — добавим структурированный список триггеров (keyword-match). +4. **Confidence score роутера** — не на первом спринте. После живого прогона посмотрим на реальные ошибки, и если их много — добавим clarifying question при низкой уверенности. + +Новые вопросы после v2: + +5. **Момент обновления `current_step`.** Сразу после парсинга `state_after` из ответа модели, или после того как ответ успешно показан пациенту? При ошибке доставки состояние может разъехаться с тем, что пациент видел. +6. **Cap на soft-insertion'ы.** Если пациент крутит ассистенту пять побочных вопросов подряд, не продвигаясь по записи — это нормально или это сигнал «пациент не хочет записываться, эскалировать»? Нужен ли cap на число инлайн-ответов до возврата к шагу скрипта. +7. **Шаги записи — из вики или из головы.** Шесть шагов `new_booking` формализованы нами, но скрипт в вике формулирует их слегка иначе («контакт → уточнение → презентация приёма → 2 слота → запись → закрытие»). До реализации Спринта 5 — свериться с вики по конкретной первой целевой специальности (ЛОР?) и принять официальный список шагов. + +Вопрос из v1 про границу «бот vs. оператор по хирургии» — исключён из архитектурных открытых: это продуктовое решение (как у клиники устроен контакт-центр), не архитектурное, и на код не влияет — хирургия просто эскалируется с `reason=surgery`, а дальше смежный разработчик маршрутизирует в нужную очередь. + +--- + +## Ориентир на следующие спринты + +Логичный порядок (согласован с `SPRINTS.md`, Спринты 4–7): + +1. **Разделить «один промпт» на несколько** → сделано (Спринт 4). +2. **Добавить роутер** → сделано (Спринт 4). +3. **State machine + exit conditions** → Спринт 5. +4. **Мульти-RAG** → Спринт 6. С учётом v2: дизайн пересмотреть в сторону Варианта Б. +5. **Сценарии и экспорт** → Спринт 7. С учётом v2: минимальный eval-набор сделать до Спринта 5, полный Спринт 7 реализовать позже. + +**Рекомендация v2 по Спринту 5:** разделить на 5a (handoff, exit conditions, двойной роутер, `handoff_count`, `suspended_intent`) и 5b (state machine внутри `new_booking` с guard'ами, структурированный ответ модели, валидатор переходов, per-step RAG). Объём работ неравномерный: 5a — несколько дней, 5b — неделя+. Если слить, велик шанс получить «наполовину сделано, протестировать нечем». + +--- + +## Changelog + +### v2 → 2026-04-24 + +**Добавлено:** +- Раздел 3.2: guards внутри ветки `new_booking` (ребёнок, Ворончихина, сурдолог) — из анализа скрипта записи в вике. +- Раздел 3.3: структурированный выход модели `{reply, state_after, slots_updated}` и валидатор переходов в коде. +- Раздел 3.4: `wiki_sources` на уровне шага, а не только ветки. +- Раздел 4.2: мягкая вставка (soft-insertion) для боковых вопросов без выхода из ветки. +- Раздел 4.3: `handoff_count` с капом и автоматическим уходом в `escalate_human` с `reason=routing_loop`. +- Раздел 4.4: `suspended_intent` + `resumable_step` + `resumable_slots` для возврата в исходную ветку после detour'а. +- Раздел 6 (новый): две схемы мульти-RAG. Рекомендация — Вариант Б (одна коллекция + подписка ветки на разделы вики через `wiki_sources`). +- Раздел 7 (новый): eval-набор (20–30 фраз на ветку + handoff-сценарии) нужен до Спринта 5, а не в Спринте 7. +- Рекомендация разделить Спринт 5 на 5a/5b. + +**Исправлено:** +- Убрана «Хирургия» как отдельная ветка (была в v1). Актуальная модель (решение разворота 2026-04-23) — одна ветка `escalate_human` с полем `reason`: `acute_pain | surgery | angry | explicit_request | routing_loop`. +- Пример exit condition переписан с `[INTENT_CHANGE: surgery]` на `[INTENT_CHANGE: escalate_human]` + `reason=surgery`. +- Список веток в разделе 1 приведён к шести (как в сиде Спринта 4). +- Открытый вопрос #4 из v1 (граница бота и оператора по хирургии) исключён из архитектурных — это продуктовый вопрос, на код не влияет. + +**Без изменений:** +- Раздел «Проблема» — в v1 сформулирована точно. +- Роутер как отдельный дешёвый вызов на каждой реплике. +- `[INTENT_CHANGE: code]` как формат служебного сигнала из ветки. +- Эскалация с полным контекстом (история, intent, слоты). +- `routing_log` для отладки. +- Общий ориентир на спринты (совпадает с `SPRINTS.md`). diff --git a/SPRINTS.md b/SPRINTS.md index 9523e02..85ee43b 100644 --- a/SPRINTS.md +++ b/SPRINTS.md @@ -204,57 +204,253 @@ ### Цель Научить ветки вести многошаговые скрипты и бесшовно передавать тред в другую ветку, если пациент сменил тему. -### Статус: ⏳ Запланирован +### Статус: ✅ Закрыт (ядро; дотяжка до GRAPH_ARCHITECTURE v2 — в бэклоге) ### Задачи **Данные:** -- [ ] Таблица `thread_state` (thread_id, current_intent, current_step, slots JSON) +- [x] Таблица `thread_state` (thread_id, current_intent_code, current_step, slots_json, updated_at) + миграция Alembic (batch-режим под SQLite) **State machine (первая ветка — `new_booking`):** -- [ ] 6-шаговый скрипт: приветствие → перехват инициативы → мини-интервью по симптому → презентация двух слотов → подтверждение → запись -- [ ] Модель на каждой реплике видит текущий шаг + собранные слоты (имя, симптом, выбранный слот) -- [ ] Переход шагов управляется правилами в промпте ветки («если на шаге 3 пациент назвал время — перейди к шагу 5») +- [x] 6-шаговый скрипт: приветствие → повод → специалист → удобное время → подтверждение → запись +- [x] Модель на каждой реплике видит блок `[ТЕКУЩЕЕ СОСТОЯНИЕ]` с `step` и `slots` +- [x] Переход шагов управляется служебным тегом `[STATE: step=N; slots={...}]` в ответе модели (строковый тег, парсится балансировкой фигурных скобок) **Exit conditions и bouncing:** -- [ ] В промпт каждой ветки добавляется блок условий выхода -- [ ] Парсер ответа ассистента ловит служебный сигнал `[INTENT_CHANGE: ]` → останавливает ветку -- [ ] Роутер на каждой реплике: если классификация ≠ текущему `thread_state.current_intent` → `thread_state` сбрасывается, тред идёт в новую ветку с полной историей +- [x] В промпт `new_booking` добавлен блок условий выхода с сигналом `[INTENT_CHANGE: ]` +- [x] Парсер в `services/chat_service._parse_assistant_signals` вырезает служебные теги из ответа +- [x] Bouncing: одна итерация (`MAX_BOUNCES=1`) — ветка может передать управление другой, делаем повторный вызов LLM +- [x] Роутер на каждой реплике: если классификация ≠ `thread_state.current_intent_code` → сброс `step` и `slots` **UI:** -- [ ] В «Песочнице» новый блок «состояние треда»: текущий intent, шаг, собранные слоты -- [ ] История переходов между ветками в рамках треда (timeline) +- [x] В «Песочнице» блок «Состояние треда»: intent, шаг, слоты (JSON), список переходов в текущей реплике +- [x] В отладке роутера — пометка, если ветка «передала управление» ### Критерий готовности -- [ ] Сценарий из `GRAPH_ARCHITECTURE.md` («запись → пациент упомянул операцию → хирургия/оператор») проходит без сброса контекста -- [ ] Ветка `new_booking` уверенно ведёт 6-шаговый скрипт на 3+ тестовых диалогах -- [ ] В отладке видна вся цепочка: начальный intent → шаги → смена ветки → финальный intent +- [x] Сценарий new_booking проходит: ФИО → повод → специалист → время → подтверждение собираются в `thread_state.slots` +- [x] Переключение ветки через роутер: «Сколько стоит приём?» внутри записи → state сбрасывается в `price_question` +- [x] В отладке видна вся цепочка: роутер-intent, served-intent, шаг, слоты, переходы + +### Что НЕ вошло в этот спринт (по сравнению с GRAPH_ARCHITECTURE_v2.md) +Реализовано ядро v1. Вся дотяжка до v2 — Спринт 6. --- -## Спринт 6. Мульти-RAG +## Спринт 6a. State machine v2 — ядро, защита от петель, возврат в ветку ### Цель -Дать каждой ветке свою коллекцию в Chroma, чтобы детская wiki не засоряла ответы общей записи, а скрипты возражений — ответы по ценам. +Заменить строковый тег `[STATE: ...]` на структурированный выход модели с валидатором переходов по таблице `intent_steps`; добавить `handoff_count` с автовыходом в `escalate_human: routing_loop`; научить систему возобновлять прерванную ветку через `suspended_intent`. В конце Спринта 6a уже видно глазами: вкладка «Шаги» в «Настройках» для `new_booking`, в «Песочнице» — handoff_count и suspended_intent, timeline переходов первой версии. + +### Статус: ⏳ Запланирован + +### Принятые решения (зафиксировано 2026-04-24, действуют и для 6b) +- **Момент обновления `current_step`** — после успешного коммита сообщения ассистента в БД. +- **Cap на soft-insertion'ы подряд** — 3 (реализация в 6b). +- **Шаги `new_booking` — сверить с вики клиники по ЛОР** до переписывания промпта в блоке A. +- **Формат структурированного выхода** — JSON-блок в хвосте ответа, парсим сами балансировкой скобок + `json.loads`. + +### Задачи и UI-чекпойнты (порядок: A → A2 → B → C) + +**Блок A. Структурированный выход + таблица `intent_steps` + валидатор переходов (v2 §3.3)** + +*Бекенд:* +- [ ] Новая таблица `intent_steps`: `id, intent_id FK, code (intro/qualify/present/offer_time/book/close), name, order_index, system_prompt Text, allowed_next JSON, guards JSON (пустой на этом спринте — наполняется в 6b/F)`. Миграция Alembic. +- [ ] Сид шагов `new_booking` при старте: читает `prompts/intents/new_booking/steps/{code}.md` + `prompts/intents/new_booking/transitions.yaml`. +- [ ] Разделить `prompts/intents/new_booking.md` на базовый промпт ветки (общие правила) + отдельные файлы на каждый из 6 шагов. +- [ ] В `services/chat_service` — сборка промпта: `base_prompt + intent_steps[current_step].system_prompt + state_context`. +- [ ] Парсер нового формата ответа: `{reply, state_after, slots_updated}` — JSON-блок в хвосте. +- [ ] Валидатор: сверка `state_after` с `intent_steps.allowed_next`. Легален → применяем, иначе — остаёмся на текущем шаге + warning в лог. Слоты сливаем `{**old, **slots_updated}`. +- [ ] `assistant_msg.text` = `reply`; служебные поля — в `assembled_prompt` для отладки. + +*UI-чекпойнт A:* +- [ ] В «Настройках» для ветки `new_booking` появляется вкладка **«Шаги»**: список шагов из `intent_steps` + на клик открывается редактор (textarea с `system_prompt`, чекбоксы с `allowed_next`). Кнопка «Сохранить шаг» — `PATCH /intents/{code}/steps/{step_code}` пишет сразу (без версионирования). +- [ ] В «Песочнице» бейдж текущего шага берётся из `intent_steps.name`, а не из сырого числа. Если валидатор отклонил `state_after` — красная пометка «модель просилась в `X`, остались на `Y`». +- [ ] **Что проверяем глазами:** открыть `new_booking` → вкладка «Шаги» видит 6 шагов; править любой промпт → применяется в новом треде; в песочнице прогнать «Здравствуйте, хочу записаться» → шаг подписан словами («Приветствие»), а не числом. + +**Блок A2. `exit_conditions_text` — отдельное поле в `agent_configs` (v2 §UI)** + +*Бекенд:* +- [ ] Миграция: добавить `exit_conditions_text Text NULLABLE` в `agent_configs`. +- [ ] `compose_full_system_prompt` склеивает: `system_prompt + rules_text + exit_conditions_text`. +- [ ] Миграция данных: при старте для существующих конфигов попытаться распарсить блок «Условия выхода» / `[INTENT_CHANGE: ...]` из хвоста `system_prompt` и перенести в новое поле. Не удалось — оставить пусто. + +*UI-чекпойнт A2:* +- [ ] В «Настройках» на вкладке активной версии — третья textarea `exit_conditions_text` рядом с `system_prompt` и `rules_text`. +- [ ] **Что проверяем глазами:** у ветки `general_info` после миграции данных в поле `exit_conditions_text` лежат правила `[INTENT_CHANGE: ...]`, а не в теле промпта. В песочнице поведение не изменилось. + +**Блок B. `handoff_count` и защита от петель (v2 §4.3)** + +*Бекенд:* +- [ ] Миграция `thread_state`: добавить `handoff_count INT NOT NULL DEFAULT 0`. +- [ ] В `chat_service` инкрементить при каждом hard-handoff (INTENT_CHANGE или router-инициированное переключение). +- [ ] При `handoff_count >= 2` — авто-уход в `escalate_human` c `reason=routing_loop`. Ответ-заглушка формируется без нового вызова LLM («Передаю ваш вопрос администратору»). +- [ ] Счётчик сбрасывается на 0 при возврате из `suspended_intent` (блок C) и при переходе в `escalate_human`. + +*UI-чекпойнт B:* +- [ ] В «Песочнице» в «Состоянии треда» — строка `handoff_count: N`. При автоуходе в `escalate_human: routing_loop` — явная отметка в timeline. +- [ ] **Что проверяем глазами:** искусственная петля «хочу записаться → сколько стоит → хочу записаться → сколько стоит» → после второго-третьего handoff'а бот говорит «передаю администратору»; в песочнице `handoff_count` вырос, ветка сменилась на `escalate_human`. + +**Блок C. `suspended_intent` + `resumable_step` + `resumable_slots` (v2 §4.4)** + +*Бекенд:* +- [ ] Миграция: добавить колонки `suspended_intent`, `resumable_step INT`, `resumable_slots_json TEXT` (все nullable) в `thread_state`. +- [ ] При hard-handoff из многошаговой ветки (`new_booking`) — сохранять `current_*` в `suspended_*` перед сбросом. +- [ ] Возврат: роутер классифицировал реплику в `suspended_intent` → восстанавливаем `current_*` из `suspended_*` и очищаем поля. Альтернативный триггер — сигнал `[RESUME]` из ветки detour'а (наполняем в 6b). + +*UI-чекпойнт C:* +- [ ] В «Состоянии треда» — `suspended_intent` и `resumable_step` (если заполнены). +- [ ] Timeline переходов между ветками в рамках треда: список типа `new_booking (step=4) → price_question → new_booking (step=4, восстановлено)`. Собирается на бекенде из diff'ов `intent_id` у соседних сообщений + лога handoff'ов. +- [ ] **Что проверяем глазами:** запись до 4 шага → «сколько это стоит?» → `suspended_intent=new_booking, resumable_step=4` видно в панели → «ок, тогда бронируем» → слоты `new_booking` вернулись, шаг=4, timeline показывает три перехода. + +### Критерий готовности 6a +- [ ] Сценарии 1 (базовая запись), 3 (handoff с suspended), 4 (возврат из suspended), 6 (routing_loop) из блока H Спринта 6b проходят в «Песочнице». +- [ ] `handoff_count` и `suspended_intent` видны глазами в «Состоянии треда». +- [ ] Вкладка «Шаги» в «Настройках» работает — можно отредактировать промпт шага и увидеть эффект в песочнице без рестарта. +- [ ] Третья textarea `exit_conditions_text` работает; данные старых веток мигрированы. +- [ ] `current_step` пишется только после коммита `assistant_msg` — проверяется код-ревью. +- [ ] Парсер структурированного выхода устойчив к невалидному `state_after`. + +--- + +## Спринт 6b. Глубина сценария — soft-insertion, guards, reason, умный роутер + +### Цель +Поверх ядра из 6a — добавить различение soft/hard-handoff, guards в `new_booking`, структурированный reason в `escalate_human`, умный роутер, видящий `thread_state`. В конце Спринта 6b все 8 ручных сценариев из блока H проходят в «Песочнице». + +### Статус: ⏳ Запланирован (после 6a) + +### Задачи и UI-чекпойнты (порядок: D → F → E → G → H) + +**Блок D. Soft-insertion vs hard-handoff (v2 §4.2)** + +*Бекенд:* +- [ ] В промпт ветки `new_booking` (базовый + шаги `qualify/present/offer_time`) — правило «короткие боковые вопросы (цена услуги, адрес, часы, длительность приёма, требования к документам) отвечай сам, не покидая шаг». Модель возвращает `state_after=текущий_шаг`, `slots_updated={}`. +- [ ] Миграция `thread_state`: добавить `soft_insertion_count INT NOT NULL DEFAULT 0`. +- [ ] На soft-insertion счётчик инкрементится; на продвижение по шагу — сбрасывается в 0. +- [ ] При `soft_insertion_count >= 3` — ветка в промпте получает явное указание «вернуть пациента к вопросу шага». + +*UI-чекпойнт D:* +- [ ] В «Состоянии треда» — `soft_insertion_count: N`. +- [ ] В timeline переходов помечать soft-insertion как `new_booking · soft-answer (price)` — без смены ветки. +- [ ] **Что проверяем глазами:** запись до шага 3 → «а сколько стоит?» → ответ по цене, шаг=3 сохранился, `soft_insertion_count=1`. Повторить 3 раза → на 3-м ответе бот возвращает к вопросу шага. + +**Блок F. Guards в `new_booking` (v2 §3.2)** + +*Бекенд:* +- [ ] В `intent_steps.guards` наполняем условия для `new_booking`: ребёнок → `legal_rep_name+legal_rep_phone` до перехода из `qualify`; запрос конкретного врача с листом ожидания → рукав `waitlist`; жалоба на слух без предварительного сурдолога → сначала `surgologist` в `specialist`. +- [ ] Слоты: `is_child`, `legal_rep_name`, `legal_rep_phone`, `requested_doctor`, `waitlist_flag`, `needs_surgologist_first`. +- [ ] Валидатор переходов (блок A 6a) проверяет `guards`: если не пройден — блокирует `state_after`, оставляет на шаге, возвращает пациенту ответ модели как есть. +- [ ] Обновить промпты шагов под сценарии guard'ов. + +*UI-чекпойнт F:* +- [ ] На вкладке «Шаги» — отдельная textarea для `guards` (JSON) с валидацией формата. +- [ ] В «Состоянии треда» — если валидатор заблокировал переход guard'ом, явная отметка «guard `require_legal_rep` не пройден, ждём `legal_rep_phone`». +- [ ] **Что проверяем глазами:** сценарий 7 (ребёнок) — на шаге `qualify` после «это для сына, 5 лет» бот спрашивает ФИО и телефон родителя; пока не заполнены — не переходит; в песочнице видна причина блокировки. Сценарий 8 (конкретный врач) — переход в рукав `waitlist`. + +**Блок E. `reason` в `escalate_human` (v2 §1, §5)** + +*Бекенд:* +- [ ] Обновить промпт `_router`: при `escalate_human` возвращать пару `code + reason` (`acute_pain / surgery / angry / explicit_request / routing_loop`). +- [ ] `RouterClient.classify` парсит reason, дефолт при неразобранном — `explicit_request`. +- [ ] Ветка `escalate_human.md` и шаги (если есть) — reason влияет на текст первой реплики. +- [ ] В `messages` — колонка `escalation_reason NULLABLE` (миграция). В API-ответе `/chat` поле `escalation_reason`. +- [ ] Заготовка саммари для оператора: при эскалации формируется `{reason, history, slots_from_suspended}`, логируется в файл/консоль (канал передачи — Спринт 9). + +*UI-чекпойнт E:* +- [ ] В «Состоянии треда» — при активной эскалации показывать `reason`. +- [ ] В «Отладке ответа» под блоком роутера — сгенерированное саммари оператора (read-only preview). +- [ ] **Что проверяем глазами:** сценарий 5 («упомянул хирургию») → эскалация с `reason=surgery`, превью саммари содержит всю историю + собранные слоты. Сценарий 6 (петля) → эскалация с `reason=routing_loop`. + +**Блок G. Умный роутер (видит `thread_state`)** + +*Бекенд:* +- [ ] `RouterClient.classify` принимает снимок `thread_state` (intent, step, slots, suspended_intent, handoff_count). Вставляет в системный промпт роутера блок «Сейчас идёт сценарий X на шаге Y, слоты Z. Если реплика — реакция или ответ на вопрос шага, скорее всего intent тот же». +- [ ] Обновить `prompts/intents/_router.md` под новый формат. +- [ ] Это снимает проблему Спринта 5: «Меня Алексей зовут» внутри `new_booking` сейчас уходит в `general_info`. + +*UI-чекпойнт G:* +- [ ] В «Отладке ответа» → блок «Решение роутера» — свернуть/развернуть кнопкой просмотр промпта, который ушёл в роутер (включая блок состояния треда). Полезно для отладки. +- [ ] **Что проверяем глазами:** тот же сценарий 1 (базовая запись) прогнать повторно — «Меня Алексей зовут» остаётся в `new_booking`, не сбрасывается в `general_info`. В развёрнутом промпте роутера видно блок `[ТЕКУЩЕЕ СОСТОЯНИЕ]`. + +**Блок H. Финальный прогон 8 ручных сценариев (прокси-eval)** +- [ ] Зафиксировать в `eval/MANUAL_CASES.md` полный список 8 сценариев (уже описан в этом документе выше, просто консолидируем). +- [ ] Прогнать в «Песочнице». Для каждого сценария — в `eval/MANUAL_REPORT.md` фиксируем результат (ок / расхождение + детали). + 1. Базовая запись (6 шагов → `confirmed=true`). + 2. Запись → вопрос про цену (soft-insertion, без смены шага). + 3. Запись → перенос старой записи (hard-handoff в `reschedule`, `suspended=new_booking`). + 4. Запись → detour → возврат «бронируем на четверг» (восстановление из `suspended`). + 5. Запись → упоминание хирургии (`escalate_human: surgery`, саммари). + 6. Искусственная петля (`routing_loop` после cap). + 7. Запись ребёнка (guard блокирует переход). + 8. Конкретный врач (waitlist-рукав). + +### Критерий готовности 6b +- [ ] Все 8 сценариев из блока H проходят в «Песочнице» без ручной правки state. `MANUAL_REPORT.md` закоммичен. +- [ ] Все UI-чекпойнты (D, F, E, G) проверены глазами. +- [ ] Роутер при активной state machine не сбрасывает intent на коротких репликах внутри сценария. +- [ ] Саммари оператору формируется и логируется при эскалации — пусть пока и без канала передачи. + +--- + +## Спринт 7. Мульти-RAG (вариант Б из v2: подписка ветки на разделы вики) + +### Цель +Дать каждой ветке собственный срез базы знаний, чтобы детская wiki не засоряла ответы по записи, а скрипты возражений — ответы по ценам. Согласно `GRAPH_ARCHITECTURE_v2.md` §6 — **Вариант Б** предпочтительнее отдельных коллекций: одна общая коллекция + фильтр по разделам вики в метаданных чанков. Делаем **до** мини-eval, чтобы наборы в Спринте 8 проверяли поведение уже с реальным per-intent retrieval. ### Статус: ⏳ Запланирован ### Задачи -- [ ] Рефакторинг `services/vectorstore.py`: фабрика коллекций, `collection_by_intent(intent_code)` вместо единственной `operators_wiki` -- [ ] В `intents` — поле `collection_name` (nullable; если пусто — используется общая `common_wiki`) -- [ ] В UI загрузки документа — селектор «в какую ветку залить (или в общую)» -- [ ] `POST /documents/upload` принимает `intent_code` как опциональный параметр -- [ ] `reindex-all` учитывает коллекции (одна команда — все коллекции) -- [ ] В «Отладке» — фильтр по веткам для просмотра документов +- [ ] В `intents` — поле `wiki_sources: list[str]` (префиксы путей или doc-ID). Миграция. +- [ ] В метаданные чанка при загрузке записывать `doc_path` / раздел вики. +- [ ] В `services/vectorstore.py` — where-фильтр по `doc_path` на основе `wiki_sources` активной ветки при query. +- [ ] UI «Настройки» — редактор `wiki_sources` у ветки (список префиксов). +- [ ] Если `wiki_sources` пуст — дефолт: вся коллекция (для `general_info`). +- [ ] Задел под v2 §3.4: опциональный `wiki_sources_by_step` (на уровне шага state machine) — сделать именно здесь, раз у нас уже есть state machine из Спринта 6. ### Критерий готовности -- [ ] Документ, загруженный в ветку «детский приём», не появляется в retrieval для других веток -- [ ] Общая коллекция `common_wiki` — fallback для веток без собственной базы (например, `general_info`) -- [ ] После переключения ветки в диалоге retrieved-чанки берутся из нужной коллекции +- [ ] Документ раздела `/wiki/pricing/*` автоматически используется только в `price_question` (без ручного дублирования). +- [ ] При переключении ветки в диалоге retrieval берёт нужный срез. +- [ ] В «Отладке» видно: какие префиксы активны, какие чанки пришли из каких разделов. +- [ ] Для шага `offer_time` в `new_booking` отдельный per-step срез работает (если ветка его заполнила). --- -## Спринт 7. Сценарии + экспорт графа +## Спринт 8. Мини-eval: роутер, handoff, resumable + +### Цель +После дотяжки v2 (Спринт 6) и мульти-RAG (Спринт 7) — зафиксировать автоматизированный тест-набор, чтобы следующие правки промптов и `wiki_sources` не ломали собранное. Формализует ручные сценарии из блока H Спринта 6. + +### Статус: ⏳ Запланирован + +### Задачи + +**Eval-наборы (отдельные файлы в репозитории, без БД):** +- [ ] `eval/router_cases.csv` — 20–30 фраз на каждую из 6 веток: типичные, пограничные (ловушечные), злые (короткие, эмоциональные, с опечатками). Колонки: `text, expected_intent, note`. +- [ ] `eval/handoff_cases.yaml` — 5–10 многошаговых мини-диалогов: реплики пациента по порядку + ожидаемый intent на каждую. +- [ ] `eval/resumable_cases.yaml` — 3–5 сценариев detour-и-возврат: реплики + ожидаемые `current_intent`, `current_step`, ключевые слоты на каждом шаге. +- [ ] `eval/loop_cases.yaml` — 1–2 сценария искусственной петли с проверкой `reason=routing_loop`. +- [ ] `eval/guard_cases.yaml` — сценарии на guards (ребёнок, waitlist). +- [ ] `eval/rag_cases.yaml` — сценарии на мульти-RAG: реплика внутри ветки → проверка, что retrieved-чанки из правильного раздела вики. + +**Запускалка (CLI, не часть сервиса):** +- [ ] `eval/run.py` — читает наборы, прогоняет через живой сервис. Режимы: + - `router` — прямой вызов `RouterClient.classify()` на фразах из CSV (быстро). + - `dialog` — полный `/chat` на чистых тредах, сверка intent + step + slots + handoff_count + reason + источники. +- [ ] Вывод: per-ветка accuracy, confusion matrix, список расхождений с текстом реплики. +- [ ] Отчёт: stdout + `eval/reports/{timestamp}.md` (добавлять в git для сравнения во времени). + +**Документация:** +- [ ] В `README.md` — раздел «Как прогнать eval» (одна команда). +- [ ] Договорённость: перед правкой промпта роутера / ветки / `wiki_sources` — прогнать eval, зафиксировать baseline; после — сравнить. + +### Критерий готовности +- [ ] `eval/run.py` работает одной командой, полный набор проходит за ≤ 3 минуты. +- [ ] Отчёт покрывает все 8 сценариев из блока H Спринта 6 + базовые kv-тесты роутера + RAG-проверки Спринта 7. +- [ ] Baseline зафиксирован в `eval/reports/{date}_baseline.md` и добавлен в git. + +--- + +## Спринт 9. Сценарии + экспорт графа ### Цель То, что изначально планировалось как Спринты 4 + 5 до архитектурного разворота. Теперь встроено в граф: прогон сценария проверяет не только текст ответов, но и правильность маршрутизации; экспорт — снапшот всего графа (intents + промпты + коллекции). @@ -285,6 +481,7 @@ ## Бэклог +### Дальнейшие идеи - Раздельные правила по доменам — **перекрыто архитектурой: теперь это ветки (`intents`)** - A/B сравнение двух версий промпта на одном тест-наборе (в рамках одной ветки или между ветками) - Метрики качества ответов (MRR, CSAT по сценариям) @@ -293,7 +490,8 @@ - Перевод правил из свободного текста в структурированный список (pattern → instruction) - Мультипользовательский режим (несколько операторов одновременно настраивают) - Хранение исходных файлов (`./data/uploads/{document_id}.{ext}` + `source_path` в метаданных Chroma) — чтобы переиндексировать без повторной загрузки и показывать оператору оригинал документа -- Confidence score роутера + clarifying question при низкой уверенности — включить после реального прогона, если будет много ошибок классификации +- Confidence score роутера + clarifying question при низкой уверенности — включить после реального прогона eval'а, если будет много ошибок классификации - Визуализация графа (веток и переходов между ними) — возможно, в виде отдельной панели - Вынесение роутера на отдельную более дешёвую модель (gpt-4o-mini, локальная Qwen) — когда вызовов станет много - Структурированные exit conditions (список триггеров с keyword-match) — если свободный текст в промпте будет пропускать реальные случаи смены темы +- `routing_log` (таблица решений роутера по каждой реплике) — для офлайн-анализа и тюнинга, когда eval покажет, что нужно