Спринт 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>
26 KiB
Оптимизация конверсии ветки new_booking — предложение спринта
Дата: 2026-04-27
Автор предложения: по результатам сравнения песочницы с действующим ботом-конкурентом «Александра» (NEXTBOT) на сайте ЛОР-клиники.
Куда встраивать: между Спринтом 7 (мульти-RAG, часть A) и Спринтом 8 (мини-eval). Желательно до eval — чтобы baseline в eval/reports/ уже отражал новую воронку, а не старую.
1. Контекст и причина
На реальной воронке клиники (виджет на сайте) пациент пишет жалобу один раз и хочет либо получить полезный ответ, либо записаться. Каждая «промежуточная» реплика бота — потеря части лидов: пациент закрывает виджет, идёт искать на других сайтах, или просто перестаёт отвечать.
Контрольный кейс — стандартный вход «Здравствуйте! очень сильно храплю, иногда закладывает уши». На том же кейсе:
| Параметр | Конкурент (NEXTBOT/Александра) | Наш прототип |
|---|---|---|
| Количество реплик бота до запроса телефона | 2 | 3 |
| Количество реплик пациента до запроса телефона | 2 (жалоба → «хочу») | 2 (жалоба → имя) |
| Медицинская гипотеза в первом ответе | Да: перегородка / аденоиды / ринит | Нет |
| Рекомендация специалиста в первом ответе | Да: ЛОР-врач | Нет |
| Услуга и цена в первом ответе | Да: эндоскопия, 1000 ₽ | Нет |
| Явный CTA на запись в первом ответе | Да: «Хотите, чтобы я помогла записаться?» | Нет |
| Имя пациента | Спрашивается вместе с телефоном (один шаг) | Спрашивается отдельным шагом (intro) |
Главные содержательные различия — два:
- Конкурент сразу решает задачу пациента, а потом продаёт запись. Мы сначала собираем анкету, а суть жалобы откладываем.
- Конкурент укладывает обмен в 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— добавить обязательный шаблон ответа на первую реплику с жалобой. Шаблон в системном промпте шага описывается как пять пунктов в строгом порядке:- Эмпатия — одна фраза («Понимаю, это действительно может мешать»).
- Гипотеза — 2–3 возможные ЛОР-причины формулировкой «может быть связано с» (без диагноза). Источник причин — RAG из подписанных документов ветки (Спринт 7), при отсутствии подходящего чанка — общая фраза без конкретики.
- Специалист — рекомендация по профилю жалобы (ЛОР, сурдолог и т. д.).
- Услуга и цена — упомянуть профильную услугу, которую врач может назначить на приёме, с ценой из вики, формулировкой «при необходимости назначит». Цена — отдельным предложением, чтобы не выглядело как «обязаны заплатить».
- 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 дня Натальи на вики (параллельно).