feat(sprint7.6): оптимизация воронки new_booking до 4 шагов (вариант 2)

Воронка сжата с 6 шагов до 4: intro → qualify → book → close.
Спецификация: docs/OPTIMIZATION_CONVERSION_v1.md.
Цель: сравнимая с конкурентом (NEXTBOT/Александра) конверсия — ≤3 реплик
бота до запроса телефона, содержательный ответ на жалобу в первом
осмысленном сообщении.

Промпты шагов:
- intro.md — переписан. Приветствие + открытый вопрос «что беспокоит?».
  Имя НЕ спрашиваем (слот name со шага снят), оно собирается на book
  вместе с телефоном. Если пациент сразу написал жалобу — не зацикливаемся,
  переходим в qualify.
- qualify.md — переписан. Обязательный 5-пунктовый шаблон ответа на жалобу:
  эмпатия (одна фраза) → 2-3 ЛОР-гипотезы из RAG-выдержек («может быть
  связано с») → специалист → услуга/цена («при необходимости назначит») →
  бинарный CTA «записать?». Если в выдержках нет гипотез/цен — пункт
  пропускается, не сочиняем. Если жалоба не описана (пациент сразу
  «хочу записаться к ЛОРу») — пропускаем гипотезу/услугу, оставляем
  эмпатию-формальность + специалист + CTA.
  Три особые ситуации сохранены: ребёнок (require_legal_rep), конкретный
  врач (waitlist_flag), первичная жалоба на слух (needs_surgologist_first).
- book.md — переписан. Одной репликой: подтверждение плана с
  использованием {specialist}/{reason} + запрос телефона + имени (если
  ещё не было в истории). При is_child=true — обращение к родителю,
  legal_rep_phone используется, если уже собран.
- present.md — DEPRECATED. Файл оставлен в репо на случай отката
  (вариант 1 спецификации). Внутри — заглушка «попал по ошибке —
  выходи на book».
- close.md и offer_time.md не тронуты (offer_time станет актуален с
  реальным календарём).

allowed_next в SEED_INTENT_STEPS:
- intro: [intro, qualify] (без изменений)
- qualify: [qualify, book] (раньше: [qualify, present])
- present: [book] (изоляция; раньше: [present, qualify, offer_time])
- offer_time: [offer_time, book] (deprecated, без изменений)
- book: [book, qualify, close] (раньше: [book, qualify, offer_time, close])
- close: [close] (без изменений)

migrate_new_booking_allowed_next_v2(session) — одноразовая миграция в
services/intent_step_service.py. При старте для каждого шага
new_booking сравнивает текущий allowed_next_json с дореформенным
значением (_PRE_SPRINT_7_6_ALLOWED_NEXT). Если совпадает — обновляет
на новое из SEED. Если оператор правил вручную — пропускает,
warning в лог. Идемпотентна (на повторных запусках ничего не делает).
Подключена в main.py lifespan после ensure_seed_guards.

Защитное условие require_legal_rep на qualify сохранено. Теперь блокирует
переход qualify → book (раньше qualify → present). Логика та же:
при is_child=true и пустых legal_rep_name/legal_rep_phone валидатор
отклоняет переход.

eval/MANUAL_CASES.md — markdown-чеклист для ручных прогонов:
- §A: 5 конверсионных кейсов (храп+уши, боль в горле, тугоухость,
  насморк >месяца, звон в ушах) с чеклистом 5 пунктов на первый ответ
  и проверкой ≤3 реплик до телефона.
- §B: регрессия 8 ручных сценариев из блока H Спринта 6b со ссылками
  на docs/examples/*_v2.md.

SPRINTS.md: Спринт 7.6 →  Закрыт по коду. Применение промптов в БД
и ручная регрессия — за оператором (через UI «Настройки → Шаги»
для каждого из 4 шагов new_booking).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
AR 15 M4
2026-04-28 21:04:09 +05:00
parent 74befa484d
commit 60f8a7b398
8 changed files with 309 additions and 70 deletions
+119
View File
@@ -0,0 +1,119 @@
# Ручные кейсы для регрессии
Чеклист для прогонов в Песочнице. **Не автоматизирован** — это `markdown`-чеклист, по которому оператор/разработчик прогоняет сценарии руками. Полная подсистема прогона (`eval/run.py`) — Спринт 8.
Раздел A — конверсионные кейсы Спринта 7.6 (новые). Раздел B — регрессия 8 ручных сценариев из блока H Спринта 6b (должны проходить как раньше).
---
## A · Конверсионные кейсы (Спринт 7.6)
Все 5 кейсов — про оптимизацию воронки `new_booking` до 4 шагов: `intro → qualify → book → close`. Цель проверки — сжатие воронки и содержательность первого ответа.
### Что проверяем на каждом кейсе
**Структура первого ответа бота на жалобу пациента** (5-пунктовый шаблон, см. `prompts/intents/new_booking/steps/qualify.md`):
- [ ] **Эмпатия** — одна короткая фраза (одна, не больше).
- [ ] **Гипотеза** — 2–3 ЛОР-причины формулировкой «может быть связано с…», без диагноза. Если в RAG-выдержках причин нет — пункт допустимо пропустить.
- [ ] **Специалист** — рекомендация по профилю (зафиксирован в слот `specialist`).
- [ ] **Услуга и цена** — формулировкой «при необходимости назначит». Если в RAG нет — пункт пропускается.
- [ ] **CTA** — бинарный вопрос «записать?».
**Сжатие воронки:**
- [ ] До запроса телефона — **≤ 3 реплики бота** (раньше было 5–6).
- [ ] Имя на `intro` **не спрашивается** — спрашивается на `book` вместе с телефоном.
- [ ] Граф работает по `intro → qualify → book → close`. На `present` модель не попадает (в Песочнице бейдж шага не показывает `present`).
**RAG:**
- [ ] В отладочной панели «Найденные фрагменты» видно, что чанки пришли из подписанных документов ветки `new_booking`.
- [ ] Если ветка не подписана ни на один документ — гипотеза/услуга/цена пропускаются (5-пунктовый шаблон деградирует до эмпатия + специалист + CTA).
### Кейсы
#### A.1 · «Очень сильно храплю, иногда закладывает уши»
Контрольный кейс из `docs/OPTIMIZATION_CONVERSION_v1.md` §1 (сравнение с конкурентом «Александра»).
- Ожидаемый специалист: ЛОР.
- Ожидаемые гипотезы (из вики): искривление перегородки, аденоиды, ринит.
- Ожидаемая услуга: эндоскопия, 1 000 ₽ (если в подписанных документах есть).
- Слоты после `qualify`: `reason="храп + заложенность ушей"`, `specialist="ЛОР"`.
#### A.2 · «Болит горло уже неделю, не проходит»
- Ожидаемый специалист: ЛОР.
- Ожидаемые гипотезы: тонзиллит, фарингит.
- Слоты: `reason="боль в горле, неделя"`, `specialist="ЛОР"`.
#### A.3 · «Стал плохо слышать на одно ухо, и звон»
Особая ситуация 3 (`needs_surgologist_first`).
- Ожидаемое поведение: сначала уточнить «вас уже обследовал сурдолог?», при первичном — `specialist=ЛОР`, `needs_surgologist_first=true`.
- Объяснение: «обычно начинают с ЛОР-врача, при необходимости направит к сурдологу».
#### A.4 · «Насморк больше месяца, не проходит»
- Ожидаемый специалист: ЛОР.
- Ожидаемые гипотезы: хронический ринит, синусит.
#### A.5 · «Звон в ушах, какой-то непонятный»
Аналог A.3, проверка устойчивости.
- Ожидаемое поведение: уточнение «были у сурдолога?», при первичном — ЛОР с пометкой про сурдолога.
---
## B · Регрессия 8 ручных сценариев (блок H Спринта 6b)
После переписки воронки в Спринте 7.6 — все 8 сценариев должны продолжать работать. Сравниваем с разобранными примерами в `docs/examples/*_v2.md`.
### B.1 · Базовая запись к ЛОР-врачу
- См. `docs/examples/01_basic_booking_v2.md`.
- Ожидание: путь `intro → qualify → book → close` (3 реплики бота до телефона), без особых ситуаций.
### B.2 · Soft-insertion цена в середине записи
- См. `docs/examples/02_price_during_booking_v2.md` Вариант A.
- Ожидание: на короткое «а сколько стоит?» — ответ в-line, шаг не меняется, `soft_insertion_count++`.
### B.3 · Hard-handoff в `reschedule` и возврат
- См. `docs/examples/02_price_during_booking_v2.md` Вариант B (там `price_question`, для reschedule аналогично).
- Ожидание: `suspended_intent=new_booking`, после возврата — восстановление `current_step_code` и `slots`.
### B.4 · Возврат из `suspended_intent`
- Подразумевается в B.3.
- Ожидание: при возврате `handoff_count` сбрасывается в 0.
### B.5 · Упоминание хирургии → escalate с `reason=surgery`
- Пациент в любом месте говорит «у меня уже была операция, надо перенести» — должен сработать `[INTENT_CHANGE: escalate_human]` с `reason=surgery`.
### B.6 · Петля роутера → автоэскалация с `reason=routing_loop`
- Искусственно: чередовать `new_booking ↔ price_question` 4+ раза.
- Ожидание: на 4-м переключении автоматически уйти в `escalate_human` с `reason=routing_loop` без вызова LLM.
### B.7 · Запись ребёнка с защитным условием `require_legal_rep`
- См. `docs/examples/03_child_patient_guard_v2.md`.
- Ожидание: при `is_child=true` и пустых `legal_rep_*` валидатор блокирует переход `qualify → book`. **Внимание:** в Спринте 7.6 переход теперь `qualify → book` (раньше было `qualify → present`). Защитное условие должно продолжать работать на новом переходе.
### B.8 · Конкретный врач → лист ожидания
- Пациент: «хочу к доктору Иванову».
- Ожидание: `requested_doctor="Иванов"`, `waitlist_flag=true`, фраза «администратор свяжется для уточнения даты».
---
## Как прогонять
1. Открой Песочницу (`http://localhost:8000/sandbox.html`).
2. Создай новый тред для каждого кейса (чтобы счётчики `handoff_count` и `soft_insertion_count` были чистыми).
3. Веди диалог как пациент, проставляй галочки в чеклисте по факту.
4. Если что-то не так — отметь словесно, приложи скрин/реплику. Возвращаемся в код, правим, прогоняем снова.
## Что НЕ делает этот документ
- Не запускается автоматически. Для автозапуска — Спринт 8 (`eval/run.py`).
- Не покрывает все возможные граничные случаи маршрутизатора. Для них есть `eval/router_cases_*.jsonl` (тоже к Спринту 8).
- Не сравнивает с baseline по метрикам. Это всё прогоны «глазами».