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

387 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Пример 01 · Базовая запись к ЛОР-врачу (happy path)
> Связано с [`GRAPH_ARCHITECTURE_v3.md`](../architecture/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` записи ещё нет (или есть пустая):
```json
{
"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` — на приветствии модели нечего искать.
**Вызовы инструментов:** нет.
**Структурированный ответ модели:**
```json
{
"reply": "Здравствуйте! С удовольствием помогу вам записаться. Подскажите, пожалуйста, как могу к вам обращаться?",
"state_after": "intro",
"slots_updated": {
"service_mention": "ЛОР"
}
}
```
Замечание: шаг **не меняется** — мы остаёмся в `intro`, пока не получим имя пациента. Но один слот заполнили заранее: модель распарсила «к лору» и положила это в `service_mention`, чтобы потом не переспрашивать.
**`thread_state` после хода:**
```json
{
"thread_id": "T-9001",
"current_intent": "new_booking",
"current_step": "intro",
"slots": {
"service_mention": "ЛОР"
},
"handoff_count": 0
}
```
---
## Реплика 2 · «Анна Петровна»
**Решение роутера:** `new_booking`. Намерение не сменилось.
**Активная ветка / шаг:** `new_booking` / `intro`. Всё ещё ждём имя — и вот оно.
**Ретривер:** не вызывается.
**Вызовы инструментов:** нет.
**Структурированный ответ модели:**
```json
{
"reply": "Очень приятно, Анна Петровна. Записать вас на первичный приём к ЛОР-врачу? Что именно беспокоит — есть конкретные жалобы?",
"state_after": "qualify",
"slots_updated": {
"patient_name": "Анна Петровна"
}
}
```
Шаг переходит в `qualify` — имя получено, далее нужно уточнить услугу, врача, детали.
**`thread_state` после хода:**
```json
{
"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 не лезем).
**Структурированный ответ модели:**
```json
{
"reply": "Понимаю. На первичный приём ЛОР могу записать. Принимают двое: Сушков Михаил Германович и Иванов Сергей Петрович. Есть предпочтение, или подобрать ближайшее свободное время к любому?",
"state_after": "present",
"slots_updated": {
"service": "первичный_ЛОР",
"complaint": "боль в горле, неделя",
"is_child": false
}
}
```
Шаг переходит в `present` — услуга, жалоба, факт «не ребёнок» зафиксированы. Заметим: `is_child=false` модель определила сама из контекста (пациент назвался ФИО + жаловался от первого лица — взрослый), это **снимает срабатывание guard'а** для детской записи (см. `03_child_patient_guard.md`, где наоборот).
**`thread_state` после хода:**
```json
{
"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) — упоминание дополнительных процедур.
**Вызовы инструментов:** нет.
**Структурированный ответ модели:**
```json
{
"reply": "Хорошо. Первичный приём ЛОР занимает около 30 минут. Врач осмотрит, при необходимости проведёт эндоскопию (оплачивается отдельно). Готова посмотреть свободные слоты?",
"state_after": "offer_time",
"slots_updated": {
"doctor_preference": null
}
}
```
Шаг переходит в `offer_time`. Поле `doctor_preference: null` явно фиксируем — это «без разницы», а не «не уточнили».
**`thread_state` после хода:**
```json
{
"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`.
**Ретривер:** на этом шаге используется только для боковых вопросов, в основном потоке не вызывается.
**Вызовы инструментов:**
```python
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`.
**Структурированный ответ модели:**
```json
{
"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` после хода:**
```json
{
"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`.
**Ретривер:** не вызывается.
**Вызовы инструментов:**
```python
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` — финальное проговаривание).
**Структурированный ответ модели:**
```json
{
"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` после хода:**
```json
{
"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`.
**Ретривер:** не вызывается.
**Вызовы инструментов:** нет.
**Структурированный ответ модели:**
```json
{
"reply": "Отлично! Жду вас 28 апреля в 10:00. Если что-то изменится — напишите, поможем перенести. Хорошего дня!",
"state_after": "done",
"slots_updated": {
"confirmed": true
}
}
```
`done` — терминальное состояние. Тред не закрывается физически (пациент может вернуться), но активная машина состояний `new_booking` отработала.
**`thread_state` после хода:**
```json
{
"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` содержит ровно тот набор данных из скрипта вики: дата, врач, адрес, стоимость, что принести.