Files
RAG_helper/docs/examples/04_general_info_simple.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

39 KiB
Raw Permalink Blame History

Пример 04 · Простые информационные запросы (general_info)

⚠️ Эта версия устарела. Актуальная — 04_general_info_simple_v2.md. У ветки general_info в коде нет машины состояний, шагов (answer, done) и слотов (info_topic, branch_mention, needs_followup) — всё это в v1 было как иллюстрация будущего. Также нет confidence threshold для RAG и per-intent фильтров по путям вики (Спринт 7). Список изменений — внизу v2 в Changelog.

Связано с GRAPH_ARCHITECTURE_v3.md, разделы 1, 2, 6. Демонстрирует простейший случай: ветка general_info без машины состояний — одна реплика пациента, ретривер, ответ, done. Никаких слотов, никаких tool calls, никаких guard'ов. Это та точка, с которой реально удобно запускать систему: классификатор + RAG + одна шаблонная ветка.

О чём этот пример

Это сборник коротких самостоятельных диалогов по одному типу запросов — общая информация (часы, адрес, проезд, телефоны, что брать с собой, чего клиника не делает). У всех у них одна и та же траектория:

  1. Роутер классифицирует реплику как general_info.
  2. Запускается ветка general_info, шаг answer (он же единственный).
  3. Ретривер достаёт чанки из соответствующего раздела вики (/wiki/contacts/**, /wiki/branches/**, /wiki/hours/**, /wiki/preparation/**, /wiki/services/_general/**).
  4. Модель формирует ответ строго по найденным чанкам.
  5. state_after = done.

Цель — на каждой реплике показать:

  • решение роутера (router) — какое намерение распознано;
  • работу ретривера (retriever) — какие чанки достаются и из каких разделов вики;
  • структурированный ответ моделиreply, state_after, slots_updated;
  • итоговое состояние треда (thread_state).

Слоты у general_info практически не используются — одно поле info_topic модель проставляет, чтобы ретривер при последующих репликах в том же треде не лез в посторонние разделы. Если в ходе ответа возникает запрос на запись («а можно тогда записаться?»), это уже жёсткий переход в new_booking (см. примеры 05+).

Конфигурация ветки general_info

intent: general_info
steps:
  answer:
    wiki_sources:
      - /wiki/contacts/**
      - /wiki/branches/**
      - /wiki/hours/**
      - /wiki/preparation/**
      - /wiki/services/_general/**
    tools: []
    next: done
  done:
    wiki_sources: []
    tools: []

Начальное состояние (общее для всех примеров ниже)

Перед каждым диалогом в thread_state пусто — это первый ход свежего треда:

{
  "thread_id": "T-9XXX",
  "current_intent": null,
  "current_step": null,
  "slots": {},
  "handoff_count": 0,
  "suspended_intent": null,
  "resumable_step": null,
  "resumable_slots": null
}

После ответа current_intent = "general_info", current_step = "done", и тред физически не закрывается — пациент может задать следующий вопрос. Если он задаст ещё один информационный вопрос, тот пройдёт по той же траектории заново (роутер его классифицирует, ветка отработает и снова уйдёт в done).


A · Часы работы

A.1 · «Во сколько вы открываетесь?»

Решение роутера: general_info. Чёткий вопрос про режим работы, никаких других намерений.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/hours/**').
  • top-k: 2.
  • Достались чанки:
    • hours/clinic-czetkina-9.md (score 0.93) — режим работы филиала на Клары Цеткин 9.
    • hours/clinic-pirogova.md (score 0.71) — режим работы филиала «Пирогова».

Модели достаточно первого чанка. Второй пригодится, если пациент уточнит про другой адрес.

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "Здравствуйте! Клиника на Клары Цеткин 9 открывается в 8:00 в будни и в 9:00 в выходные. Закрываемся в 21:00 в будни и в 18:00 в выходные. Если интересует другой филиал — подскажу отдельно.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "hours"
  }
}

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "hours"
  }
}

A.2 · «До скольки работает клиника на Цеткина?»

Решение роутера: general_info. Конкретика про филиал — это всё ещё информационный вопрос, не запись.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/hours/**', '/wiki/branches/**').
  • top-k: 2.
  • Достались чанки:
    • hours/clinic-czetkina-9.md (score 0.94) — режим работы.
    • branches/czetkina-9.md (score 0.78) — карточка филиала с адресом.

Реплика содержит «Цеткина» — в индексе совпадает по названию филиала, поэтому ретривер уверенно вытаскивает нужную карточку.

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "Клиника на Клары Цеткин 9 работает до 21:00 в будни и до 18:00 в выходные.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "hours",
    "branch_mention": "Клары Цеткин 9"
  }
}

Заметим: branch_mention модель кладёт в слоты, чтобы при возможном следующем вопросе («а проезд как?») ретривер мог сразу дать чанк именно про этот филиал, а не про другой.

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "hours",
    "branch_mention": "Клары Цеткин 9"
  }
}

A.3 · «У вас по выходным открыто?»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/hours/**').
  • top-k: 2.
  • Достались чанки:
    • hours/clinic-czetkina-9.md (score 0.86).
    • hours/clinic-pirogova.md (score 0.81).

Здесь пациент не уточнил филиал, поэтому в ответе разумно перечислить оба.

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "Да, мы работаем и в субботу, и в воскресенье. На Клары Цеткин 9 — с 9:00 до 18:00, в Клинике Пирогова — с 9:00 до 17:00. Расписание конкретного врача может отличаться, при записи подскажу.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "hours"
  }
}

Замечание: фраза «расписание конкретного врача может отличаться» — это зашитая в промпт ветки оговорка (canned disclaimer), не результат RAG. Часы работы клиники и слоты конкретного врача — разные вещи, и модель должна на этом не зависать.

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "hours"
  }
}

A.4 · «Завтра праздник, вы работаете?»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/hours/**').
  • top-k: 3.
  • Достались чанки:
    • hours/clinic-czetkina-9.md (score 0.74) — обычный режим.
    • hours/holidays-2026.md (score 0.42) — праздничный календарь, низкий score (документ короткий, неточное совпадение по датам).
    • hours/clinic-pirogova.md (score 0.71).

Score праздничного чанка — ниже порога уверенности (0.50). По правилу шага: если ни один чанк не покрывает запрос полностью — отвечаем шаблоном «уточним и перезвоним», а не выдумываем расписание праздников.

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "В обычные дни мы работаем с 8:00 до 21:00. В праздничные дни режим может меняться. Подскажите, пожалуйста, на какую дату вы планируете визит — я уточню у регистратуры и сообщу вам в течение дня.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "hours",
    "needs_followup": true
  }
}

Слот needs_followup: true — сигнал для оператора в инбоксе, что тред нужно дозакрыть человеком. На дашборде такие треды попадают в очередь «требуется уточнение».

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "hours",
    "needs_followup": true
  }
}

B · Адрес и филиалы

B.1 · «Где вы находитесь?»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/branches/**').
  • top-k: 3.
  • Достались чанки:
    • branches/_index.md (score 0.88) — общий перечень филиалов.
    • branches/czetkina-9.md (score 0.71).
    • branches/pirogova.md (score 0.69).

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "У нас два адреса в Перми: ул. Клары Цеткин, 9 (основная клиника, ЛОР, сурдология, отоневрология) и Клиника Пирогова, ул. Героев Хасана 24 (терапия, аллергология, телемед). Подсказать, какой ближе к вам или какая специальность нужна?",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "branches"
  }
}

Финальный встречный вопрос — лёгкий, без обязательного ответа. Если пациент проигнорирует, тред просто останется в done. Если ответит — пойдёт ещё один проход через роутера.

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "branches"
  }
}

B.2 · «Какой у вас точный адрес на Цеткина?»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/branches/**').
  • top-k: 1.
  • Достались чанки:
    • branches/czetkina-9.md (score 0.96) — карточка филиала: адрес, ориентиры, вход.

Высокий score, точный матч — достаточно одного чанка.

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "Адрес: г. Пермь, ул. Клары Цеткин, 9. Вход со стороны двора, ориентир — голубая вывеска «Клиника Ухо, Горло, Нос». Регистратура на первом этаже.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "branches",
    "branch_mention": "Клары Цеткин 9"
  }
}

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "branches",
    "branch_mention": "Клары Цеткин 9"
  }
}

B.3 · «Сколько у вас филиалов в городе?»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/branches/**').
  • top-k: 1.
  • Достались чанки:
    • branches/_index.md (score 0.90) — общий перечень.

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "В Перми у нас два филиала: основная клиника на ул. Клары Цеткин, 9, и Клиника Пирогова на ул. Героев Хасана, 24. Других филиалов и онлайн-клиник в других городах у нас нет — приём только в этих двух.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "branches"
  }
}

Замечание: пациенты иногда спрашивают «вы же есть в Москве?», путая клинику с одноимёнными. Ответ намеренно явно говорит «других филиалов нет» — чтобы не оставить надежду на не-существующее.

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "branches"
  }
}

B.4 · «Как вас на Яндекс Картах искать?»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/branches/**', '/wiki/contacts/**').
  • top-k: 2.
  • Достались чанки:
    • contacts/online-presence.md (score 0.84) — карточка организации, ссылки на 2ГИС / Я.Карты.
    • branches/_index.md (score 0.66).

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "На Яндекс Картах ищите по запросу «Клиника Ухо Горло Нос Пермь» — найдутся обе наших точки: на Клары Цеткин 9 и на Героев Хасана 24. Можно сразу проложить маршрут оттуда.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "branches"
  }
}

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "branches"
  }
}

C · Как добраться и парковка

C.1 · «На метро как до вас доехать?»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/branches/**').
  • top-k: 2.
  • Достались чанки:
    • branches/czetkina-9.md (score 0.62) — раздел «как добраться».
    • branches/pirogova.md (score 0.59).

Ни один чанк не упоминает метро — потому что в Перми метро нет. Ретривер вытащил то, что про транспорт ближе всего по теме.

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "В Перми метро нет, поэтому удобнее всего добираться на автобусе, троллейбусе или такси. До Клары Цеткин 9 — остановка «Площадь Восстания» (5 минут пешком), маршруты 14, 32, 36, 67. До Клиники Пирогова — остановка «Героев Хасана», маршруты 19, 27. Подсказать что-то конкретнее, если знаете, откуда поедете?",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "transit"
  }
}

Это полезный пограничный кейс: вопрос содержит ложное допущение (метро). Промпт ветки требует мягко поправить пациента, не оставляя его в недоумении, и сразу предложить рабочую альтернативу.

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "transit"
  }
}

C.2 · «Парковка у вас есть?»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/branches/**').
  • top-k: 2.
  • Достались чанки:
    • branches/czetkina-9.md (score 0.81) — раздел «парковка».
    • branches/pirogova.md (score 0.78) — раздел «парковка».

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "Да, парковка есть на обоих филиалах. На Клары Цеткин 9 — небольшая бесплатная парковка во дворе клиники, обычно есть свободные места. У Клиники Пирогова — общая платная парковка торгового центра рядом, въезд с ул. Героев Хасана. Подсказать, какой филиал вам удобнее?",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "parking"
  }
}

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "parking"
  }
}

C.3 · «Как пешком от центра дойти до Цеткина?»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/branches/**').
  • top-k: 1.
  • Достались чанки:
    • branches/czetkina-9.md (score 0.79) — раздел «как добраться» → подраздел «пешком».

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "От Комсомольского проспекта дойти можно за 12–15 минут: вниз по Сибирской до Клары Цеткин, направо до дома 9. Удобнее всего ориентироваться на Яндекс Карты — там же увидите маршрут.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "transit",
    "branch_mention": "Клары Цеткин 9"
  }
}

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "transit",
    "branch_mention": "Клары Цеткин 9"
  }
}

D · Контакты

D.1 · «Дайте телефон регистратуры»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/contacts/**').
  • top-k: 1.
  • Достались чанки:
    • contacts/registratura.md (score 0.95) — телефоны регистратуры по филиалам.

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "Регистратура на Клары Цеткин 9 — +7 (342) 204-00-00. Регистратура Клиники Пирогова — +7 (342) 204-67-16. Звонят с 8:00 до 21:00 в будни. Если удобнее, можно записаться прямо здесь — я помогу.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "contacts"
  }
}

Хвостовая фраза «можно записаться прямо здесь» — это soft-cross-sell, разрешённая в ветке general_info. Пациент свободен её проигнорировать; если откликнется — следующая реплика уйдёт в new_booking через роутера.

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "contacts"
  }
}

D.2 · «У вас WhatsApp есть?»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/contacts/**').
  • top-k: 1.
  • Достались чанки:
    • contacts/messengers.md (score 0.93) — мессенджеры, в которых отвечает клиника.

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "Да, мы отвечаем в WhatsApp по номеру +7 (342) 204-00-00 — это тот же телефон регистратуры. Также есть Telegram: @ugn_clinic. Уведомления о приёме приходят на тот номер, который вы оставите при записи.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "contacts"
  }
}

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "contacts"
  }
}

D.3 · «А сайт у вас какой?»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/contacts/**').
  • top-k: 1.
  • Достались чанки:
    • contacts/online-presence.md (score 0.91).

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "Сайт клиники: ugn-clinic.ru. Там можно посмотреть прайс, расписание врачей и записаться. Если хотите — могу записать вас прямо сейчас, без перехода на сайт.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "contacts"
  }
}

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "contacts"
  }
}

E · Документы и подготовка к приёму

E.1 · «Что нужно с собой принести?»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/preparation/**').
  • top-k: 2.
  • Достались чанки:
    • preparation/what-to-bring.md (score 0.92) — общий список.
    • preparation/лор-первичный.md (score 0.65) — для ЛОРа отдельно.

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "На приём возьмите паспорт (для пациентов с 14 лет) и, если есть, выписки или результаты предыдущих обследований по теме визита. Полис ОМС не нужен — клиника частная. Если есть конкретная специальность, к которой записываетесь, могу подсказать, что взять именно для неё.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "preparation"
  }
}

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "preparation"
  }
}

E.2 · «Полис ОМС у вас принимают?»

Решение роутера: general_info. Это ещё не вопрос про цену в чистом виде — пациент уточняет принцип, не считает деньги. Но граница тонкая, см. замечание ниже.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/preparation/**', '/wiki/services/_general/**').
  • top-k: 2.
  • Достались чанки:
    • preparation/what-to-bring.md (score 0.74) — там есть строка про ОМС.
    • services/_general/payment-options.md (score 0.81) — формы оплаты, ДМС.

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "Нет, мы частная клиника и по полису ОМС не работаем. Принимаем оплату наличными, картой и переводом. По ДМС — работаем не со всеми страховыми, нужно уточнять по конкретной компании.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "preparation"
  }
}

Замечание: следующая реплика «а с какими страховыми работаете?» — это уже жёсткий переход в price_question (там есть отдельная ветвь про ДМС-покрытие). На текущей реплике мы аккуратно касаемся темы, но не углубляемся.

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "preparation"
  }
}

E.3 · «Сыну 16, можно без меня прийти?»

Решение роутера: general_info. Вопрос про порядок приёма несовершеннолетних — это организационный, не медицинский и не запись.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/preparation/**', '/wiki/_faq/**').
  • top-k: 2.
  • Достались чанки:
    • _faq/teenagers-without-parents.md (score 0.94) — соответствует в точности.
    • preparation/what-to-bring.md (score 0.61).

Чанк teenagers-without-parents.md — это статья из раздела ЧаВо (см. источник vrachi-kliniki-...md, пункт «Если пациенту 16-17 лет»), индексированная в общий пул general_info.

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "С 15 лет подросток может прийти на приём один и сам подписать договор — нужен паспорт. Если родители смогут позже подъехать или прислать документы — это тоже подойдёт. Записать сына на удобное время?",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "preparation"
  }
}

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "preparation"
  }
}

F · Что клиника не делает

F.1 · «У вас можно сделать бронхоскопию?»

Решение роутера: general_info. По формальному признаку — пациент спрашивает про услугу. Но это не запись и не цена, это уточнение «делаете ли вы такое в принципе». Промпт роутера явно относит подобные формулировки к general_info, чтобы не запускать механизм записи на услугу, которой нет.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/services/_general/**').
  • top-k: 2.
  • Достались чанки:
    • services/_general/not-performed.md (score 0.97) — список процедур, которые НЕ проводятся.
    • services/_general/scope.md (score 0.62) — общий профиль клиники.

Чанк not-performed.md собран из соответствующего раздела вики (см. источник «Процедуры, которые в клинике НЕ проводятся» в skripty-...md). Бронхоскопия там есть в явном виде.

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "Бронхоскопию у нас не делают — это исследование с эндоскопом по слизистым трахеи и бронхов, его нужно искать в специализированных пульмонологических центрах. Если вас беспокоит кашель или дыхание — мы можем записать к пульмонологу на консультацию, он уже подскажет, куда дальше.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "scope_of_services"
  }
}

Это важный сценарий для honesty-eval'а: модель не должна выдумывать «да, конечно, запишем» в надежде, что пациент придёт и разберётся на месте. Прямой отказ + полезная альтернатива.

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "scope_of_services"
  }
}

F.2 · «Криодеструкцию миндалин делаете?»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/services/_general/**').
  • top-k: 1.
  • Достались чанки:
    • services/_general/not-performed.md (score 0.94).

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "Нет, криодеструкцию миндалин в нашей клинике не проводят. Если вас беспокоит хронический тонзиллит — можем записать на приём к ЛОР-врачу: он осмотрит, предложит варианты лечения из тех, что у нас доступны, и подскажет, куда обратиться, если понадобится именно эта процедура.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "scope_of_services"
  }
}

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "scope_of_services"
  }
}

F.3 · «Электрофорез носа можно у вас?»

Решение роутера: general_info.

Активная ветка / шаг: general_info / answer.

Ретривер:

  • Фильтр: doc_path STARTS WITH any('/wiki/services/_general/**').
  • top-k: 1.
  • Достались чанки:
    • services/_general/not-performed.md (score 0.91) — электрофорез в списке «не проводится».

Вызовы инструментов: нет.

Структурированный ответ модели:

{
  "reply": "Нет, электрофорез у нас не делают. Из физиопроцедур по ЛОР-профилю мы предлагаем промывания, орошения, лечебные ингаляции — если интересно, могу подробнее рассказать или сразу записать на приём.",
  "state_after": "done",
  "slots_updated": {
    "info_topic": "scope_of_services"
  }
}

thread_state после хода:

{
  "current_intent": "general_info",
  "current_step": "done",
  "slots": {
    "info_topic": "scope_of_services"
  }
}

Что показал этот пример

  • Одна реплика — один проход. Ветка general_info не имеет машины состояний: единственный «осмысленный» шаг answer сразу уводит в done. Это самый дешёвый путь в системе и логичная точка запуска первой версии бота.
  • Слоты почти не используются. Поле info_topic нужно ровно для того, чтобы при следующем вопросе в том же треде ретривер знал, в какую сторону смотреть в первую очередь. Опционально — branch_mention, чтобы помнить, про какой филиал шла речь.
  • Ретривер делает основную работу. Все факты в ответе должны быть из чанков, не из памяти модели. Если score лучшего чанка ниже порога — модель уходит в шаблон «уточним и перезвоним» с needs_followup=true (см. A.4).
  • Пограничные кейсы важнее, чем happy path. Метро в Перми (которого нет — C.1), услуги, которых клиника не предоставляет (F), праздничные дни без чанка (A.4) — именно на них модель ломается чаще всего и именно их полезно держать в eval-наборе с самого начала.
  • Soft cross-sell разрешён, но мягкий. Хвост «могу записать прямо сейчас» — нормальная практика для информационных ответов. Жёстко продавать запись — нет.

Что важно проверять в eval-наборе на этом примере

  • Роутер не уводит информационные вопросы в new_booking или price_question (классическая ошибка — на «сколько у вас стоит» в чистом виде это уже price_question, а на «какие у вас услуги» — general_info; границы должны быть чёткими).
  • Все факты в reply находимы в одном из чанков, попавших в контекст. Хорошая метрика — groundedness (доля утверждений, для которых есть прямое подтверждение в источниках).
  • При отсутствии релевантных чанков модель отвечает шаблоном «уточним», а не выдумывает (см. A.4 и логика needs_followup).
  • Ответы не превышают 3–4 предложений. general_info — не место для лекций.
  • Слот info_topic проставлен корректно (hours, branches, transit, parking, contacts, preparation, scope_of_services) — эта же таксономия используется в дашборде «о чём чаще всего спрашивают».
  • На пограничных кейсах (метро в Перми, услуги, которых нет, праздничные дни) ответ не выдаёт ложных утверждений — это критичный безопасный минимум для запуска.