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

26 KiB
Raw Permalink Blame History

Оптимизация конверсии ветки 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 дня Натальи на вики (параллельно).