Files
RAG_helper/docs/examples/01_basic_booking.md
T
AR 15 M4 f348570b1b docs: переезд в docs/ — SPRINTS, architecture (v1/v2/v3), examples
- SPRINTS.md → docs/SPRINTS.md
- GRAPH_ARCHITECTURE.md → docs/architecture/GRAPH_ARCHITECTURE_v1.md
- GRAPH_ARCHITECTURE_v2.md → docs/architecture/GRAPH_ARCHITECTURE_v2.md
- Новый docs/architecture/GRAPH_ARCHITECTURE_v3.md (билингв. термины + ссылки на примеры)
- Новые docs/examples/: 01 базовая запись, 02 цена во время записи (soft vs hard),
  03 запись ребёнка (guard), 04 простой general_info
- README обновлён: ссылки на новые пути + раздел «Документация»

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:50:16 +05:00

18 KiB
Raw Blame History

Пример 01 · Базовая запись к ЛОР-врачу (happy path)

Связано с GRAPH_ARCHITECTURE_v3.md, разделы 1, 2, 3. Демонстрирует линейный проход машины состояний (state machine) ветки new_booking: контакт → уточнение → презентация → слоты → запись → закрытие. Никаких guard'ов, никаких боковых вопросов, никаких переходов в другие ветки. Самый простой случай, на котором удобно увидеть базовое поведение системы.

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

Взрослый пациент звонит/пишет в чат, хочет записаться к ЛОР-врачу с жалобой на горло. Не уточняет конкретного врача, согласен на ближайший слот. Семь реплик, после которых запись создана в CRM.

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

  • решение роутера (router) — какое намерение распознано;
  • активный шаг (current_step) — где мы сейчас в скрипте;
  • работу поисковика-ретривера (retriever) — какие чанки достаются и из каких разделов вики;
  • вызовы инструментов (tool calls) к CRM, если они есть;
  • структурированный ответ модели (structured output) — reply, state_after, slots_updated;
  • итоговое состояние треда (thread_state) — что записалось в БД.

Начальное состояние

Тред только что создан. В таблице thread_state записи ещё нет (или есть пустая):

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

Реплика 1 · «Здравствуйте, хочу записаться к лору»

Решение роутера: new_booking. Однозначное упоминание записи + специальности.

Активная ветка / шаг: new_booking / intro. Шаг по умолчанию для свежей ветки.

Ретривер: не вызывается. Шаг intro имеет пустой wiki_sources — на приветствии модели нечего искать.

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

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

{
  "reply": "Здравствуйте! С удовольствием помогу вам записаться. Подскажите, пожалуйста, как могу к вам обращаться?",
  "state_after": "intro",
  "slots_updated": {
    "service_mention": "ЛОР"
  }
}

Замечание: шаг не меняется — мы остаёмся в intro, пока не получим имя пациента. Но один слот заполнили заранее: модель распарсила «к лору» и положила это в service_mention, чтобы потом не переспрашивать.

thread_state после хода:

{
  "thread_id": "T-9001",
  "current_intent": "new_booking",
  "current_step": "intro",
  "slots": {
    "service_mention": "ЛОР"
  },
  "handoff_count": 0
}

Реплика 2 · «Анна Петровна»

Решение роутера: new_booking. Намерение не сменилось.

Активная ветка / шаг: new_booking / intro. Всё ещё ждём имя — и вот оно.

Ретривер: не вызывается.

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

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

{
  "reply": "Очень приятно, Анна Петровна. Записать вас на первичный приём к ЛОР-врачу? Что именно беспокоит — есть конкретные жалобы?",
  "state_after": "qualify",
  "slots_updated": {
    "patient_name": "Анна Петровна"
  }
}

Шаг переходит в qualify — имя получено, далее нужно уточнить услугу, врача, детали.

thread_state после хода:

{
  "current_intent": "new_booking",
  "current_step": "qualify",
  "slots": {
    "service_mention": "ЛОР",
    "patient_name": "Анна Петровна"
  }
}

Реплика 3 · «Да, болит горло уже неделю»

Решение роутера: new_booking. Жалоба упомянута, но в контексте записи — не самостоятельный медицинский вопрос. Роутер не сворачивает в medical_question, потому что пациент уже на маршруте записи.

Активная ветка / шаг: new_booking / qualify.

Ретривер: срез на уровне шага — wiki_sources: ['/wiki/services/**', '/wiki/doctors/**'].

  • top-k: 3.
  • Достались чанки:
    • services/первичный-приём-лор.md (score 0.87) — описание услуги, длительность, что входит.
    • doctors/сушков-михаил-германович.md (score 0.71) — карточка врача.
    • doctors/иванов-сергей-петрович.md (score 0.69) — карточка второго ЛОР.

Вызовы инструментов: нет (на этом шаге слоты ещё собираем, в CRM не лезем).

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

{
  "reply": "Понимаю. На первичный приём ЛОР могу записать. Принимают двое: Сушков Михаил Германович и Иванов Сергей Петрович. Есть предпочтение, или подобрать ближайшее свободное время к любому?",
  "state_after": "present",
  "slots_updated": {
    "service": "первичный_ЛОР",
    "complaint": "боль в горле, неделя",
    "is_child": false
  }
}

Шаг переходит в present — услуга, жалоба, факт «не ребёнок» зафиксированы. Заметим: is_child=false модель определила сама из контекста (пациент назвался ФИО + жаловался от первого лица — взрослый), это снимает срабатывание guard'а для детской записи (см. 03_child_patient_guard.md, где наоборот).

thread_state после хода:

{
  "current_intent": "new_booking",
  "current_step": "present",
  "slots": {
    "service_mention": "ЛОР",
    "patient_name": "Анна Петровна",
    "service": "первичный_ЛОР",
    "complaint": "боль в горле, неделя",
    "is_child": false
  }
}

Реплика 4 · «Без разницы, что есть»

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

Активная ветка / шаг: new_booking / present.

Ретривер: срез шага — ['/wiki/services/**', '/wiki/doctors/**', '/wiki/preparation/**'].

  • top-k: 3.
  • Достались чанки:
    • services/первичный-приём-лор.md — длительность, что входит, что оплачивается отдельно.
    • preparation/лор-первичный.md (score 0.74) — что принести, как подготовиться.
    • services/эндоскопия-лор.md (score 0.63) — упоминание дополнительных процедур.

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

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

{
  "reply": "Хорошо. Первичный приём ЛОР занимает около 30 минут. Врач осмотрит, при необходимости проведёт эндоскопию (оплачивается отдельно). Готова посмотреть свободные слоты?",
  "state_after": "offer_time",
  "slots_updated": {
    "doctor_preference": null
  }
}

Шаг переходит в offer_time. Поле doctor_preference: null явно фиксируем — это «без разницы», а не «не уточнили».

thread_state после хода:

{
  "current_intent": "new_booking",
  "current_step": "offer_time",
  "slots": {
    "patient_name": "Анна Петровна",
    "service": "первичный_ЛОР",
    "complaint": "боль в горле, неделя",
    "is_child": false,
    "doctor_preference": null
  }
}

Реплика 5 · «Да, посмотрите»

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

Активная ветка / шаг: new_booking / offer_time.

Ретривер: на этом шаге используется только для боковых вопросов, в основном потоке не вызывается.

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

crm.get_slots(
    service_code="primary_lor",
    doctor=None,
    days_ahead=7
)
# → [
#   {"datetime": "2026-04-28 10:00", "doctor": "Сушков М. Г.", "branch": "ул. Клары Цеткин, 9"},
#   {"datetime": "2026-04-28 15:00", "doctor": "Сушков М. Г.", "branch": "ул. Клары Цеткин, 9"}
# ]

Согласно скрипту записи в вике — предлагаем ровно два слота, не больше. Это правило зашито в промпт шага offer_time.

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

{
  "reply": "Есть свободно вторник 28 апреля: 10:00 или 15:00 — оба слота к Сушкову Михаилу Германовичу, ул. Клары Цеткин, 9. Какое удобнее?",
  "state_after": "offer_time",
  "slots_updated": {
    "time_candidates": ["2026-04-28 10:00", "2026-04-28 15:00"],
    "doctor": "Сушков М. Г.",
    "branch": "ул. Клары Цеткин, 9"
  }
}

Шаг не меняется — слоты предложили, но выбора пока нет, ждём.

thread_state после хода:

{
  "current_intent": "new_booking",
  "current_step": "offer_time",
  "slots": {
    "patient_name": "Анна Петровна",
    "service": "первичный_ЛОР",
    "complaint": "боль в горле, неделя",
    "is_child": false,
    "doctor_preference": null,
    "time_candidates": ["2026-04-28 10:00", "2026-04-28 15:00"],
    "doctor": "Сушков М. Г.",
    "branch": "ул. Клары Цеткин, 9"
  }
}

Реплика 6 · «10 утра, давайте»

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

Активная ветка / шаг: new_booking / offer_time. Получили выбор времени → допустим переход offer_time → book.

Ретривер: не вызывается.

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

crm.create_booking(
    patient_name="Анна Петровна",
    service_code="primary_lor",
    doctor="Сушков М. Г.",
    datetime="2026-04-28 10:00",
    branch="ул. Клары Цеткин, 9"
)
# → {"booking_id": "B-1234", "status": "confirmed"}

Модель в одном ходе проходит book, и сразу за ним — close (это легальный двойной переход, потому что book — это «вызови инструмент» без отдельной реплики пациенту, а close — финальное проговаривание).

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

{
  "reply": "Записала вас на вторник 28 апреля, 10:00, к ЛОР-врачу Сушкову Михаилу Германовичу, ул. Клары Цеткин, 9. Стоимость первичного приёма — 2 300 рублей. Эндоскопия, лечебные процедуры, забор анализов оплачиваются дополнительно. Подходите за 5 минут до приёма, при себе иметь паспорт. Подтверждаете?",
  "state_after": "close",
  "slots_updated": {
    "time_chosen": "2026-04-28 10:00",
    "booking_id": "B-1234"
  }
}

Шаг переходит в close. На этом шаге ретривер уже доставал бы wiki_sources ['/wiki/contacts/**', '/wiki/preparation/**'], но в этом конкретном ответе модель использовала чанки, оставшиеся в контексте с предыдущих шагов (стоимость уже была в карточке услуги, требования к документам — в preparation).

thread_state после хода:

{
  "current_intent": "new_booking",
  "current_step": "close",
  "slots": {
    "patient_name": "Анна Петровна",
    "service": "первичный_ЛОР",
    "complaint": "боль в горле, неделя",
    "is_child": false,
    "doctor_preference": null,
    "time_candidates": ["2026-04-28 10:00", "2026-04-28 15:00"],
    "doctor": "Сушков М. Г.",
    "branch": "ул. Клары Цеткин, 9",
    "time_chosen": "2026-04-28 10:00",
    "booking_id": "B-1234"
  }
}

Реплика 7 · «Да, подтверждаю. Спасибо!»

Решение роутера: new_booking (или general_info — реплика короткая, но контекст ясен).

Активная ветка / шаг: new_booking / close.

Ретривер: не вызывается.

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

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

{
  "reply": "Отлично! Жду вас 28 апреля в 10:00. Если что-то изменится — напишите, поможем перенести. Хорошего дня!",
  "state_after": "done",
  "slots_updated": {
    "confirmed": true
  }
}

done — терминальное состояние. Тред не закрывается физически (пациент может вернуться), но активная машина состояний new_booking отработала.

thread_state после хода:

{
  "current_intent": "new_booking",
  "current_step": "done",
  "slots": {
    "...все поля выше...": "...",
    "confirmed": true
  }
}

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

  • Линейный проход машины состояний. Шаги шли строго в порядке intro → qualify → present → offer_time → book → close → done. Двойной переход в реплике 6 (offer_time → book → close) — допустимый случай, потому что book — технический шаг с вызовом инструмента, не требующий отдельной реплики пациенту.
  • Слоты накапливаются. На каждом ходе slots_updated содержит только новые/изменённые поля, а в thread_state.slots они мерджатся с предыдущим состоянием. Старые значения не теряются.
  • Ретривер использует срез шага. Один и тот же тред на разных шагах достаёт разные документы вики: на qualify — каталог услуг и врачей, на close — контакты и подготовку.
  • Намерение остаётся одним. Роутер на каждой реплике подтверждал new_booking. В этом примере смены намерения нет — для случаев, где она происходит, см. 02_price_during_booking.md.
  • Guard'ы не сработали. Пациент — взрослый (is_child=false), врача не выбирал — ушли по основному пути. Случай со срабатыванием guard'а — в 03_child_patient_guard.md.

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

  • Все шаги машины состояний были пройдены в правильном порядке (логи state_after на каждом ходе).
  • В slots к моменту шага book были заполнены: patient_name, service, doctor, time_chosen. Без любого из этих полей crm.create_booking не должен вызываться (валидатор перехода).
  • Ответ на шаге close содержит ровно тот набор данных из скрипта вики: дата, врач, адрес, стоимость, что принести.