Files
RAG_helper/docs/OPTIMIZATION_CONVERSION_v1.md
AR 15 M4 52b46bc53e feat(sprint6c+sprint7): терминология, сверка примеров с кодом, мульти-RAG (часть A)
Спринт 6c — терминология и сверка документации с реальным кодом:
- Словарь терминов в static/docs.html: «маршрутизатор» вместо «роутер»,
  «защитное условие» вместо «guard», «пошаговая ветка» вместо «многошаговая».
  Разделены концепты «намерение» (intent) и «ветка» (branch) с пометкой,
  что в коде они хранятся как одна сущность 1:1.
- Песочница: «Решение маршрутизатора» виден всегда (зелёный/жёлтый),
  счётчик переключений «N из 3» отдельной плашкой, бейджи под словарь.
- Настройки: «Условия перехода» → «Защитные условия (guards, JSON)».
- GRAPH_ARCHITECTURE_v4.md: имена полей thread_state и слоты приведены
  к реальной БД (db/models/thread_state.py) и таксономии промптов шагов
  (prompts/intents/new_booking/steps/). Ссылки на *_v2 примеры. На v3
  поставлена шапка «устарело».
- 4 примера переписаны как *_v2: реальные current_intent_code/
  current_step_code/slots_json, реальные allowed_next без двойных переходов,
  реальная таксономия слотов name/reason/specialist/preferred_time/confirmed.
  Удалены вымышленные CRM tool calls и слоты, которых нет в коде.
- static/example.html — параметризованная страница с навигацией между
  4 примерами; роут GET /api/docs/examples/{name} в main.py отдаёт
  markdown без дублирования файлов.
- Редактирование документов в Отладке: GET/PUT /documents/{id}/raw,
  textarea с переразметкой и обновлением Chroma при сохранении.

Спринт 7, часть A — мульти-RAG через подписку ветка↔документы:
- Миграция: таблица intent_documents (M:N), модель IntentDocument,
  индекс по document_id для обратного поиска.
- API: GET/PUT /intents/{code}/documents и GET/PUT /documents/{id}/intents
  с PUT-семантикой «полный список», атомарно. Сервис
  services/intent_document_service.py.
- Retrieval-фильтр в chat_service: подтягивает document_ids активной
  ветки и передаёт в vectorstore.query(). Дефолт пустой подписки —
  document_ids=[] (= 0 чанков), не «вся коллекция»: пустая подписка
  означает «ветка не настроена», подмешивать случайное хуже, чем
  ничего. vectorstore.query() различает None (нет фильтра) и [] (0).
- UI Настроек: блок «Документы базы знаний» в правом сайдбаре,
  всегда видим независимо от вкладки, сортировка по имени, счётчик
  «N из M», PUT при сохранении.
- UI Отладки: третья кнопка «привязка» рядом с «удалить» —
  раскрывашка со списком веток (галочки), быстрая привязка прямо
  на странице загрузки.
- Песочница: блок «Срез RAG» с подпиской/найдено, ворнинг при пустой
  подписке. Поле rag_subscription в QueryResponse и ChatResponse.
- Системный промпт страницы Отладки переехал в обычную ветку _debug
  («Страница отладки»). Удалён prompts/system_prompt.md и логика
  DEFAULT_SYSTEM_PROMPT в llm_client. routers/query.py подтягивает
  активный конфиг ветки _debug и её подписки. Дефолт пустой подписки
  для _debug — None (вся коллекция), не [] как для пациентских — чтобы
  Отладка работала «из коробки». На странице Отладки info-bar показывает
  активную версию и счётчик подписок, ссылка → Настройки.
- Тест-блок «Тест-вопрос» в центре Настроек: расширил /query
  параметрами intent_code (default _debug), system_prompt (override
  для теста черновика из textarea), disable_rag (для _router).
  Редактор промпта обёрнут в <details open> — можно свернуть до
  одной строки. Под ним — три колонки результата (RAG / промпт /
  ответ). Для _router показывается подсказка про отсутствие RAG.

Документы:
- data/datasets/*.md — наработки по 6 веткам (рабочие материалы оператора).
- docs/BRANCH_MAP_AND_PROMPTS_v1.md, docs/OPTIMIZATION_CONVERSION_v1.md,
  docs/guides/state_machine_and_slots.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:00:44 +05:00

175 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Оптимизация конверсии ветки `new_booking` — предложение спринта
**Дата:** 2026-04-27
**Автор предложения:** по результатам сравнения песочницы с действующим ботом-конкурентом «Александра» (NEXTBOT) на сайте ЛОР-клиники.
**Куда встраивать:** между Спринтом 7 (мульти-RAG, часть A) и Спринтом 8 (мини-eval). Желательно до eval — чтобы baseline в `eval/reports/` уже отражал новую воронку, а не старую.
---
## 1. Контекст и причина
На реальной воронке клиники (виджет на сайте) пациент пишет жалобу один раз и хочет либо получить полезный ответ, либо записаться. Каждая «промежуточная» реплика бота — потеря части лидов: пациент закрывает виджет, идёт искать на других сайтах, или просто перестаёт отвечать.
Контрольный кейс — стандартный вход «Здравствуйте! очень сильно храплю, иногда закладывает уши». На том же кейсе:
| Параметр | Конкурент (NEXTBOT/Александра) | Наш прототип |
|---|---|---|
| Количество реплик бота до запроса телефона | **2** | **3** |
| Количество реплик пациента до запроса телефона | 2 (жалоба → «хочу») | 2 (жалоба → имя) |
| Медицинская гипотеза в первом ответе | Да: перегородка / аденоиды / ринит | Нет |
| Рекомендация специалиста в первом ответе | Да: ЛОР-врач | Нет |
| Услуга и цена в первом ответе | Да: эндоскопия, 1000 ₽ | Нет |
| Явный CTA на запись в первом ответе | Да: «Хотите, чтобы я помогла записаться?» | Нет |
| Имя пациента | Спрашивается вместе с телефоном (один шаг) | Спрашивается отдельным шагом (`intro`) |
Главные содержательные различия — два:
1. **Конкурент сразу решает задачу пациента**, а потом продаёт запись. Мы сначала собираем анкету, а суть жалобы откладываем.
2. **Конкурент укладывает обмен в 4 реплики** (бот → пациент → бот → пациент), мы — в 6+. Каждая лишняя итерация — это и доп. токены, и доп. drop-off.
Нынешняя архитектура `new_booking` (intro → qualify → present → offer_time → book → close) полностью валидна для оператора, который ведёт пациента по записи. Проблема не в графе, а в **содержимом первого осмысленного ответа** и в **порядке сбора слотов `name` и `phone`**.
## 2. Цель спринта
Сделать воронку `new_booking` сопоставимой с конкурентом по конверсии при сохранении нашей архитектуры (state machine, слоты, защитные условия, soft-insertion). Конкретно — переписать содержание шагов `intro` и `qualify`, поменять момент сбора имени, добавить в `qualify` обязательную «содержательную обвязку» (гипотеза + специалист + услуга + цена + CTA).
## 3. Целевые метрики (для ручной проверки и mini-eval)
- **Сжатие воронки.** На контрольном кейсе «храп + заложенность ушей» количество реплик бота до момента, когда у нас в слотах `phone` ≠ null, должно быть **≤ 3** (сейчас ~5).
- **Содержательность первого ответа.** На любую входную реплику с явной ЛОР-жалобой первый осмысленный ответ бота должен покрывать пять пунктов:
- короткое сочувствие в одну фразу,
- 2–3 возможные ЛОР-причины формулировкой «может быть связано с»,
- рекомендация специалиста,
- упоминание профильной услуги и цены (эндоскопия / аудиограмма / приём — то, что есть в вики и применимо к жалобе),
- бинарный CTA «записать вас на приём?».
- **Сохранение защитных условий.** Все 8 ручных сценариев из блока H Спринта 6b продолжают проходить (запись ребёнка, листы ожидания, эскалация, routing_loop). Новая воронка не ломает существующие guard'ы.
- **Сохранение тона.** Тёплое обращение, «вы», без диагнозов, без дозировок — все правила базового промпта `new_booking.md` остаются.
## 4. Что меняем — обзор
Изменения локальные: четыре файла промптов и один JSON `intent_steps.allowed_next` (таблица переходов). Кода трогаем минимум.
| Файл | Что меняем |
|---|---|
| `prompts/intents/new_booking/steps/intro.md` | Урезаем до одной короткой реплики приветствия + инициирующего вопроса. Снимаем требование собрать `name` именно здесь. |
| `prompts/intents/new_booking/steps/qualify.md` | Добавляем обязательный шаблон «содержательного ответа на жалобу»: гипотеза → специалист → услуга/цена → CTA. Имя становится опциональным слотом. |
| `prompts/intents/new_booking/steps/present.md` | Сокращаем до одной фразы-подтверждения (если вообще оставляем — обсуждаемо). |
| `prompts/intents/new_booking/steps/book.md` | Запрос телефона + имени в одной реплике; имя становится частью контактного блока, а не отдельной анкетой. |
| `intent_steps.allowed_next` (сид + миграция данных) | Разрешаем `intro → book` напрямую при коротком пути «есть жалоба + согласие» (см. блок A). |
## 5. Блоки задач
### Блок A. Сжатие воронки и перестановка сбора имени
**Промпты:**
- [ ] `intro.md` — переписать. Новая задача шага: поздороваться одной фразой и **сразу спросить, чем можем помочь**, не запрашивая имя. Текст приветствия — «Здравствуйте! Я виртуальный ассистент клиники. Расскажите, что вас беспокоит — подскажу, к какому специалисту записаться.» Слот `name` со шага `intro` снимаем (становится опциональным, заполняется на `qualify` или `book`).
- [ ] `book.md` — переписать запрос контакта: «Чтобы администратор связался с вами и подтвердил время — напишите ваш номер телефона и как к вам обращаться». В одной реплике собираем `phone` и (опционально, если ещё не собрано) `name`. Если пациент назвал имя раньше — повторно не спрашиваем.
- [ ] `qualify.md` — снять требование «не уходи дальше пока нет `name`», т.к. имя теперь не обязательно для перехода с `intro` и собирается естественно по ходу.
**Таблица переходов (`intent_steps.allowed_next`):**
- [ ] Расширить `allowed_next` шага `intro`: добавить переход `intro → qualify` (как сейчас) и **новый прямой `intro → present`** на случай, когда пациент уже первой репликой назвал и жалобу, и согласие записаться (редкий, но возможный случай).
- [ ] Подтвердить, что `qualify → book` через `present` остаётся, а сам `present` мы либо радикально сокращаем (см. блок C), либо удаляем как самостоятельный шаг.
**UI-чекпойнт A:**
- [ ] В «Песочнице» прогнать кейс «Здравствуйте, болит ухо» — на первой реплике бот **не** спрашивает имя, а сразу даёт содержательный ответ (это уже эффект блока B).
- [ ] В «Песочнице» прогнать кейс «Здравствуйте, я Сергей, болит ухо, хочу записаться» — слот `name=Сергей` подхватывается на `qualify`, на `book` имя повторно не спрашивается.
- [ ] **Что проверяем глазами:** общее число реплик бота до запроса телефона — 3 или меньше. В timeline переходов нет «зависания» на `intro`.
### Блок B. Содержательный `qualify` — гипотеза, специалист, услуга, цена, CTA
**Промпты:**
- [ ] `qualify.md` — добавить обязательный шаблон ответа на первую реплику с жалобой. Шаблон в системном промпте шага описывается как пять пунктов в строгом порядке:
1. **Эмпатия** — одна фраза («Понимаю, это действительно может мешать»).
2. **Гипотеза** — 2–3 возможные ЛОР-причины формулировкой «может быть связано с» (без диагноза). Источник причин — RAG из подписанных документов ветки (Спринт 7), при отсутствии подходящего чанка — общая фраза без конкретики.
3. **Специалист** — рекомендация по профилю жалобы (ЛОР, сурдолог и т. д.).
4. **Услуга и цена** — упомянуть профильную услугу, которую врач может назначить на приёме, с ценой из вики, формулировкой «при необходимости назначит». Цена — отдельным предложением, чтобы не выглядело как «обязаны заплатить».
5. **CTA** — бинарный вопрос: «Записать вас на приём?» / «Хотите, я помогу записаться?».
- [ ] В `qualify.md` зафиксировать: если пациент сразу ответил «да/хочу/записывайте» — переход `qualify → present` (или `qualify → book` напрямую, если решим в блоке C сокращать `present`). Слот `reason` фиксируем по тексту жалобы, `specialist` — по выводу гипотезы.
- [ ] Сохранить все три «особые ситуации» (ребёнок, конкретный врач, первичная жалоба на слух) — они срабатывают как сейчас и не конфликтуют с новым шаблоном (просто добавляются в логику ответа).
**RAG (зависимость от Спринта 7):**
- [ ] Подписать на ветку `new_booking` документы вики, содержащие связки «жалоба → возможные причины → специалист → услуга → цена». Если на момент Спринта документов нет — завести задачу для Натальи: подготовить wiki-страницы по 5–7 типовым жалобам (храп, заложенность ушей, боль в горле, тугоухость, насморк, головокружение, шум в ушах) в формате «жалоба → 2–3 ЛОР-причины → специалист → процедура и цена».
- [ ] Для жалоб, не покрытых вики, шаблон деградирует мягко: эмпатия + рекомендация ЛОР-врача + CTA, без гипотез и услуги. Это лучше, чем выдумывать.
**UI-чекпойнт B:**
- [ ] В «Песочнице» прогнать 5 контрольных кейсов: храп + уши, боль в горле, тугоухость, насморк > месяца, звон в ушах. На каждом — первый ответ бота должен содержать все 5 пунктов шаблона (или явно деградировать на 3, если документа в подписке нет).
- [ ] В отладочной панели «Найденные фрагменты» — видно, какие чанки пошли в гипотезу/услугу.
- [ ] **Что проверяем глазами:** на контрольном кейсе из раздела 1 наш ответ субъективно «не хуже» ответа Александры. Можно показать ответы рядом и сравнить.
### Блок C. Сокращение или удаление шага `present`
**Решение требует обсуждения** перед началом работ:
**Вариант 1 (минимальное вмешательство):** оставить `present` как есть, но переписать на одну короткую фразу-подтверждение («{name?}, оформляю запись к {specialist}, на приёме врач уделит внимание {reason}»). Сразу после — переход на `book` (запрос контакта), без отдельного шага `offer_time` для текущей итерации воронки.
**Вариант 2 (агрессивный):** убрать `present` как самостоятельный шаг. Подтверждение плана зашить в первую фразу `book` («Записываю к {specialist}. Чтобы администратор связался — телефон и имя?»). Тогда воронка: `intro → qualify → book → close`, всего 4 шага вместо 6.
**Аргументы за вариант 2:** ровно так делает конкурент (сразу после «Хочу» — запрос телефона). Каждый шаг — это +1 реплика бота, и `present` без нового действия от пациента ощущается как «вода».
**Аргументы за вариант 1:** меньше риска сломать ручные сценарии 1–8 из Спринта 6b, проще откатить, шаг `present` остаётся точкой, куда возвращаемся при пересогласовании специалиста.
Предлагаю **вариант 2** с явным фолбэком: если на ручных кейсах пациенты теряют ощущение, что их услышали, — возвращаем `present` обратно в граф.
**Задачи (для варианта 2):**
- [ ] `present.md` — пометить как deprecated в рамках спринта, не удалять файл (история).
- [ ] `book.md` — добавить в начало шаблон одной фразы-подтверждения с использованием слотов `specialist` и `reason`.
- [ ] Миграция `intent_steps`: убрать `present` из `allowed_next` шага `qualify`, добавить прямой переход `qualify → book`. Шаг `present` оставить в таблице как «висящий» на случай отката.
- [ ] Обновить `prompts/intents/new_booking/transitions.yaml` (если есть) или соответствующий сид.
**UI-чекпойнт C:**
- [ ] Прогнать в «Песочнице» все 8 сценариев Спринта 6b. Сценарии 7 (ребёнок) и 8 (конкретный врач) — проверить отдельно, что guard'ы и waitlist-рукав не сломались.
- [ ] **Что проверяем глазами:** базовый кейс из раздела 1 закрывается за 4 реплики бота вместо 6. Ручной сценарий 7 (ребёнок) — guard `require_legal_rep` всё ещё блокирует переход.
### Блок D. Тест-кейсы и регрессия
**Подготовка eval-набора (заготовка для Спринта 8):**
- [ ] В `eval/MANUAL_CASES.md` добавить раздел «Конверсионные кейсы» с 5 контрольными жалобами из блока B. Для каждого — ожидаемые слоты после первой реплики пациента, ожидаемая структура первого ответа бота (проверяется глазами по чек-листу из 5 пунктов), ожидаемое количество реплик до сбора `phone`.
- [ ] Добавить негативный кейс: «Здравствуйте» (без жалобы) — бот должен задать открытый вопрос, не уйти в шаблон гипотезы (т. к. нет `reason`).
- [ ] Добавить кейс с быстрой записью: «Запишите меня к ЛОРу на завтра» — бот должен пропустить блок гипотезы (жалоба не описана) и сразу подтвердить + спросить контакт.
**Проверка отсутствия регрессии:**
- [ ] Все 8 сценариев из блока H Спринта 6b проходят без правок ожиданий.
- [ ] `eval/router_cases.csv` — accuracy не упала. Особое внимание: на кейсах с жалобами роутер по-прежнему возвращает `new_booking`, а не `medical_question` (наш sticky state machine это страхует, но всё равно проверяем).
- [ ] Soft-insertion (Спринт 6b блок D) работает: «а сколько стоит приём?» внутри новой короткой воронки — отвечается на месте, шаг не сбрасывается.
## 6. Принятые компромиссы и риски
- **Цены в первом ответе.** Чтобы упоминать цену, нужен корректный документ в RAG. Если документа нет — бот не выдумывает, и тогда первый ответ без цены и без гипотез — просто эмпатия + специалист + CTA. Это всё ещё лучше текущего «как к вам обращаться?», но без цены воронка слабее. Прогресс по этому риску напрямую зависит от качества вики (задача Натальи).
- **Имя пациента может потеряться.** Если пациент не назвал имя ни на `intro` (где мы его теперь не спрашиваем), ни на `book`, в слот `name` останется пустым. Это нормально — `name` всё равно опциональное поле для вежливого обращения, а не идентификатор. На `book` спрашиваем явно, поэтому шанс потерять минимальный.
- **Subjective trade-off: тон.** Перенос имени с `intro` на `book` ощущается «менее персонально» в первой реплике. Компенсируем содержательностью ответа — пациент видит, что бот понял его проблему, и это сильнее, чем «как к вам обращаться?».
- **Конкурент тоже не идеален.** Александра упоминает цену на эндоскопию, но не предлагает её альтернативы и не уточняет жалобу. Это окей для нашего MVP, но в бэклог стоит внести задачу «варьировать услугу по типу жалобы» (для тугоухости — аудиограмма, не эндоскопия).
## 7. Критерий готовности спринта
- [ ] На контрольном кейсе раздела 1 наш бот в «Песочнице» отвечает по 5-пунктовому шаблону, и весь обмен до запроса телефона укладывается в 3 реплики бота.
- [ ] Все 8 ручных сценариев из блока H Спринта 6b проходят без правок ожиданий.
- [ ] 5 контрольных конверсионных кейсов из блока D добавлены в `eval/MANUAL_CASES.md` и прогнаны вручную; результаты — в `eval/MANUAL_REPORT.md`.
- [ ] Промпты `intro.md`, `qualify.md`, `book.md` обновлены, изменения видны во вкладке «Шаги» (Спринт 6a, блок A) — оператор может прочитать без выгрузки кода.
- [ ] Если выбран вариант 2 блока C — миграция таблицы переходов выполнена, `present` помечен как deprecated.
## 8. Что НЕ делаем в этом спринте
- Не трогаем `_router.md` — изменения локальные внутри ветки.
- Не делаем confidence threshold для RAG (это в бэклоге, нужно после прогона eval).
- Не пишем CRM-интеграцию (мок-инструменты `crm.create_booking` — отдельный пункт бэклога).
- Не трогаем шаги `offer_time` и `close` — они внутренние, конкурент их вообще не показывает в первой воронке. Их роль (выбор времени из календаря и финал) станет актуальна, когда подключим реальный календарь в Спринте 9 / при подключении канала.
## 9. Дальнейшие идеи (на потом)
- **Вариация услуги по жалобе.** Сейчас предлагаем стандартную эндоскопию. После наполнения вики — научить ветку выбирать профильную процедуру по `reason` (тугоухость → аудиограмма, насморк > 4 недель → риноскопия и т. д.). Это требует отдельного слота `suggested_procedure` и подсказки в промпте `qualify`.
- **A/B тестирование двух версий первого ответа.** После Спринта 8 (eval) запустить две версии `qualify` параллельно и сравнить, какая даёт лучшее покрытие 5-пунктового шаблона на ручных кейсах.
- **Постпродажа на `close`.** После сбора телефона — короткое «также можем напомнить за день до приёма SMS» / «оставить второй контакт для родственника». Конкурент этого не делает; это не догоняние, а попытка обогнать. Завести в идеи только после стабилизации основной воронки.
---
**Зависимости:**
- Спринт 6a (вкладка «Шаги», структурированный выход) — должен быть закрыт **до** старта этого спринта, иначе править промпты шагов через UI не получится.
- Спринт 7 (мульти-RAG) — желателен закрытым, чтобы цены и услуги попадали в `qualify` через подписки документов. При незакрытом 7 спринт делаем на устаревшем механизме «вся коллекция», результат будет хуже.
**Оценка трудозатрат (в условных единицах):**
- Блок A (промпты + переходы): 1 день.
- Блок B (содержательный qualify + RAG-увязка): 1.5 дня. Зависит от готовности вики.
- Блок C (вариант 2): 0.5 дня.
- Блок D (eval-кейсы): 1 день.
- Итого: ~3–4 дня инженерного времени + ~2 дня Натальи на вики (параллельно).