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>
This commit is contained in:
AR 15 M4
2026-04-26 21:50:16 +05:00
parent 82bba34937
commit f348570b1b
9 changed files with 2427 additions and 3 deletions
+332
View File
@@ -0,0 +1,332 @@
# Пример 03 · Запись ребёнка — защитное условие в шаге уточнения
> Связано с [`GRAPH_ARCHITECTURE_v3.md`](../architecture/GRAPH_ARCHITECTURE_v3.md), разделы 3.2, 3.3. Демонстрирует **защитное условие** (guard) внутри шага `qualify` ветки `new_booking`. Когда модель определяет, что пациент — ребёнок (`is_child=true`), линейный путь машины состояний прерывается: переход `qualify → present` блокируется до тех пор, пока не будут собраны данные **законного представителя** (legal representative). Также показано, как эта особенность отражается на финальном шаге `close` — там добавляются юридические оговорки из вики.
## О чём этот пример
Мама записывает 8-летнего сына к ЛОР-врачу. Ситуация юридически регулируется: согласно ФЗ № 323-ФЗ «Об основах охраны здоровья граждан в РФ» приём ребёнка возможен только с участием законного представителя, и клиника обязана зафиксировать его данные на этапе записи. Скрипт записи в вике формулирует это как обязательный сбор ФИО и контактного телефона законного представителя.
В терминах архитектуры это означает: внутри шага `qualify` есть **под-состояние** (sub-state) `qualify.legal_rep`, активирующееся при `is_child=true`. Модель не может вернуть `state_after: "present"`, пока в слотах не заполнены `legal_rep_full_name` и `legal_rep_phone` — валидатор перехода в коде это проверит и отклонит ход, если переход неправильный.
## Начальное состояние
```json
{
"thread_id": "T-9003",
"current_intent": null,
"current_step": null,
"slots": {},
"handoff_count": 0
}
```
---
## Реплика 1 · «Здравствуйте, хочу записать ребёнка к лору»
**Решение роутера:** `new_booking`. Упоминание «ребёнка» само по себе не сигнал эскалации — это норма для записи.
**Активная ветка / шаг:** `new_booking` / `intro`.
**Ретривер:** не вызывается.
**Вызовы инструментов:** нет.
**Структурированный ответ модели:**
```json
{
"reply": "Здравствуйте! Помогу записать ребёнка. Подскажите, как могу к вам обращаться, и как зовут ребёнка?",
"state_after": "intro",
"slots_updated": {
"service_mention": "ЛОР",
"is_child": true
}
}
```
Заметим: `is_child=true` модель устанавливает уже здесь, на основании слова «ребёнок». Это **раннее срабатывание флага** — и оно правильное, потому что определяет всю дальнейшую логику. Шаг при этом не меняется — ждём имена.
**`thread_state` после хода:**
```json
{
"current_intent": "new_booking",
"current_step": "intro",
"slots": {
"service_mention": "ЛОР",
"is_child": true
}
}
```
---
## Реплика 2 · «Меня зовут Мария, ребёнка зовут Матвей, ему 8 лет»
**Решение роутера:** `new_booking`.
**Активная ветка / шаг:** `new_booking` / `intro`.
**Ретривер:** не вызывается.
**Структурированный ответ модели:**
```json
{
"reply": "Очень приятно, Мария. Записать Матвея на первичный приём к ЛОР-врачу? Что именно беспокоит?",
"state_after": "qualify",
"slots_updated": {
"patient_name": "Матвей",
"patient_age": 8,
"parent_first_name": "Мария"
}
}
```
Шаг переходит в `qualify`. Имя ребёнка — `patient_name` (это пациент, на чьё имя пойдёт запись), имя мамы временно фиксируем как `parent_first_name` — на следующем под-шаге его дополним до полного ФИО.
**`thread_state` после хода:**
```json
{
"current_intent": "new_booking",
"current_step": "qualify",
"slots": {
"service_mention": "ЛОР",
"is_child": true,
"patient_name": "Матвей",
"patient_age": 8,
"parent_first_name": "Мария"
}
}
```
---
## Реплика 3 · «У него уши болят и плохо слышит на одно ухо»
**Решение роутера:** `new_booking`. Жалоба упомянута, но в контексте записи — не самостоятельный медицинский вопрос.
**Активная ветка / шаг:** `new_booking` / `qualify`.
**Срабатывает guard** — при `is_child=true` обычный переход `qualify → present` заблокирован. Машина состояний ждёт под-состояние `qualify.legal_rep`. В промпте шага явно написано: «если `is_child=true` и не заполнены `legal_rep_full_name` + `legal_rep_phone` — соблюдай скрипт сбора данных представителя из вики».
**Дополнительно срабатывает второй guard** — упоминание «плохо слышит на одно ухо» это сигнал на сурдолога. Из v3 раздел 3.2: «жалоба на слух без обследования у сурдолога → на шаге `present` предложить сурдолога». Этот guard сработает позже, когда дойдём до `present`.
**Ретривер:** срез шага `qualify``wiki_sources: ['/wiki/services/**', '/wiki/doctors/**', '/wiki/legal/**']` (последний раздел добавлен для шага под-состояния `qualify.legal_rep`).
- top-k: 3.
- Достались чанки:
- `legal/child-patient-rep-script.md` (score 0.93) — скрипт сбора данных законного представителя из вики.
- `legal/fz-323-rep.md` (score 0.78) — выдержка из ФЗ-323 для случаев «зачем вам это нужно».
- `services/первичный-приём-лор.md` (score 0.62).
**Вызовы инструментов:** нет.
**Структурированный ответ модели:**
```json
{
"reply": "Поняла, посмотрим. Прежде чем подобрать слот — для записи ребёнка нам по правилам клиники нужно зафиксировать данные законного представителя. Уточните, пожалуйста, ваше полное ФИО и контактный телефон.",
"state_after": "qualify.legal_rep",
"slots_updated": {
"complaint": "боль в ушах, ухудшение слуха на одно ухо"
}
}
```
Шаг переходит в под-состояние `qualify.legal_rep` — guard активен. Жалоба зафиксирована в слотах, но переход в `present` пока заблокирован.
**`thread_state` после хода:**
```json
{
"current_intent": "new_booking",
"current_step": "qualify.legal_rep",
"slots": {
"service_mention": "ЛОР",
"is_child": true,
"patient_name": "Матвей",
"patient_age": 8,
"parent_first_name": "Мария",
"complaint": "боль в ушах, ухудшение слуха на одно ухо"
}
}
```
---
## Реплика 4 · «А зачем вам это нужно?»
Возражение пациентки. Из вики: «Если возникает вопрос — А зачем вам это нужно? — вы можете объяснить, что это стандартная практика для обеспечения безопасности и контроля доступа к медицинским услугам ребенка...».
**Решение роутера:** `new_booking`.
**Активная ветка / шаг:** `new_booking` / `qualify.legal_rep`.
**Ретривер:** тот же срез, чанк `legal/fz-323-rep.md` уже в контексте.
**Структурированный ответ модели:**
```json
{
"reply": "Это стандартная практика — нужна для безопасности ребёнка и для того, чтобы мы могли с вами связаться, если в ходе приёма потребуется ваше согласие на процедуру. Согласно ФЗ № 323-ФЗ мы должны иметь возможность связаться с законным представителем в любой момент. Подскажите, пожалуйста, ваше полное ФИО и телефон.",
"state_after": "qualify.legal_rep",
"slots_updated": {}
}
```
Шаг и слоты не меняются — guard всё ещё блокирует выход.
---
## Реплика 5 · «Хорошо. Иванова Мария Сергеевна, +79991234567»
**Решение роутера:** `new_booking`.
**Активная ветка / шаг:** `new_booking` / `qualify.legal_rep`.
**Структурированный ответ модели:**
```json
{
"reply": "Спасибо, Мария Сергеевна. Записываю Матвея на первичный приём к ЛОР-врачу. Подберу ближайший свободный слот?",
"state_after": "present",
"slots_updated": {
"legal_rep_full_name": "Иванова Мария Сергеевна",
"legal_rep_phone": "+79991234567"
}
}
```
Guard выполнен — оба обязательных слота заполнены. Валидатор переходов в коде проверяет: переход `qualify.legal_rep → present` допустим **только если** `legal_rep_full_name` и `legal_rep_phone` заполнены. Условие выполняется, переход разрешён.
**`thread_state` после хода:**
```json
{
"current_intent": "new_booking",
"current_step": "present",
"slots": {
"service_mention": "ЛОР",
"is_child": true,
"patient_name": "Матвей",
"patient_age": 8,
"parent_first_name": "Мария",
"complaint": "боль в ушах, ухудшение слуха на одно ухо",
"legal_rep_full_name": "Иванова Мария Сергеевна",
"legal_rep_phone": "+79991234567"
}
}
```
---
## Реплика 6 · «Да, давайте посмотрим, что есть»
**Решение роутера:** `new_booking`.
**Активная ветка / шаг:** `new_booking` / `present`.
**Срабатывает второй guard** — на основании слота `complaint` (упоминание ухудшения слуха) промпт шага `present` должен предложить **сурдолога перед ЛОРом**. Это правило из вики и v3 раздел 3.2.
**Ретривер:** срез шага — `wiki_sources: ['/wiki/services/**', '/wiki/doctors/**', '/wiki/preparation/**']`.
- top-k: 3.
- Достались чанки:
- `services/приём-сурдолога.md` (score 0.88) — описание услуги.
- `services/первичный-приём-лор.md` (score 0.74).
- `doctors/общая-информация-сурдолог.md` (score 0.65).
**Структурированный ответ модели:**
```json
{
"reply": "С учётом того, что есть жалоба на ухудшение слуха на одно ухо, я бы предложила сначала записать Матвея к нашему сурдологу — он проведёт обследование слуха, и уже с его заключением ЛОР сможет дать более точное лечение. Если вы согласны — подберу слот к сурдологу. Или, если хотите, всё равно идём сразу к ЛОРу?",
"state_after": "present",
"slots_updated": {
"guard_surdologist_suggested": true
}
}
```
Шаг не меняется — ждём решение мамы. Флаг `guard_surdologist_suggested` нужен, чтобы при следующей реплике не предлагать сурдолога повторно, если она откажется.
---
## Реплика 7 · «Да, давайте сначала к сурдологу»
**Решение роутера:** `new_booking`.
**Активная ветка / шаг:** `new_booking` / `present``offer_time`.
**Структурированный ответ модели:**
```json
{
"reply": "Хорошо. Подбираю свободные слоты к сурдологу.",
"state_after": "offer_time",
"slots_updated": {
"service": "приём_сурдолога",
"doctor_preference": null
}
}
```
Шаг переходит в `offer_time`. Услуга в слотах теперь — приём сурдолога, не ЛОР.
---
## Реплика 8 · `crm.get_slots()` + предложение слотов
(Аналогично `01_basic_booking.md` репликам 5-6, без особенностей. Получаем 2 слота, мама выбирает один, идём в `book``close`.)
---
## Реплика 9 · Закрытие — финальная сводка с детскими оговорками
**Активная ветка / шаг:** `new_booking` / `close`.
**Срабатывает guard на шаге `close`:** при `is_child=true` промпт обязан включить специальный блок с юридическими оговорками из вики. Берётся из `legal/child-patient-final-script.md`.
**Ретривер:** срез шага — `wiki_sources: ['/wiki/contacts/**', '/wiki/preparation/**', '/wiki/legal/**']`.
- Достались чанки:
- `legal/child-patient-final-script.md` (score 0.95) — обязательный текст про сопровождение, паспорт + свидетельство о рождении, доверенность для других сопровождающих.
- `contacts/branch-klary-tsetkin-9.md` (score 0.81) — адрес, как доехать.
- `preparation/ребёнок-первичный.md` (score 0.71) — что взять с собой.
**Структурированный ответ модели:**
```json
{
"reply": "Записала Матвея на четверг 30 апреля, 11:00, к сурдологу Кузнецовой Елене Игоревне, ул. Газеты Звезда, 31-А. Стоимость приёма — 2 100 рублей. Если в ходе приёма понадобится дополнительная диагностика, она оплачивается отдельно. Подходите за 5 минут до назначенного времени, при себе паспорт одного из родителей и свидетельство о рождении ребёнка. \n\nОбращаю внимание: на приём вместе с ребёнком должен прийти его законный представитель. Если в качестве сопровождающего лица будет кто-то другой (бабушка, дедушка), у него обязательно должен быть паспорт, доверенность от законного представителя, копия свидетельства о рождении и копия паспорта представителя. Форма доверенности — на сайте клиники.\n\nПодтверждаете?",
"state_after": "close",
"slots_updated": {
"time_chosen": "2026-04-30 11:00",
"doctor": "Кузнецова Е. И.",
"branch": "ул. Газеты Звезда, 31-А",
"booking_id": "B-1236"
}
}
```
Длинная финальная реплика с юридической оговоркой из ФЗ-323 — ровно так, как написано в скрипте записи в вике для детских пациентов.
---
## Что показал этот пример
- **Guard как изменение допустимых переходов.** Шаг `qualify` имеет два пути: обычный `qualify → present` (для взрослых, `is_child=false`) и через под-шаг `qualify.legal_rep → present` (для детей). Валидатор переходов в коде должен это учитывать, иначе модель сможет «прыгнуть» через `legal_rep` и переход состоится с пустыми слотами представителя.
- **Раннее срабатывание флага `is_child`.** Модель установила его уже на реплике 1, до получения имени. Это важно: если бы `is_child` появился только на реплике 2, сбор данных пошёл бы в неправильном порядке (сначала жалоба, потом данные представителя — пациентка успела бы рассказать симптомы, прежде чем мы попросили бы документы).
- **Несколько guard'ов в одной ветке.** На шаге `present` сработал второй guard (предложить сурдолога), на шаге `close` — третий (юридический текст для детей). Они независимы и могут срабатывать в одном диалоге.
- **Ретривер на каждом шаге достаёт релевантный для guard'а контент.** На `qualify.legal_rep` — раздел `/wiki/legal/`, на `close` — финальные юридические оговорки. Без правильного `wiki_sources` на уровне шага модель должна была бы «помнить» юридический текст из системного промпта — это плохо масштабируется.
- **Возражение «а зачем вам это нужно?» обрабатывается в-line, без выхода из guard'а.** На реплике 4 модель пояснила, оставаясь в `qualify.legal_rep`. Это похоже на мягкую вставку из примера 02, но внутри одного и того же шага, не между ветками.
## Что важно проверять в eval-наборе на этом примере
- **Без legal_rep слотов переход `qualify.legal_rep → present` не должен срабатывать.** Тест: подать в ветку модельный ответ с `state_after: "present"` при пустых `legal_rep_full_name` или `legal_rep_phone` → валидатор должен отклонить переход, состояние остаётся `qualify.legal_rep`, в логе предупреждение.
- **`is_child=true` устанавливается рано.** Тест: фраза «запишите ребёнка», без других слов → проверить, что `is_child=true` появляется в `slots_updated` уже на первой реплике.
- **На шаге `close` для ребёнка ответ обязан содержать упоминание свидетельства о рождении и доверенности.** Тест: прогнать сценарий с `is_child=true` → проверить, что финальный `reply` содержит подстроки «свидетельство о рождении» и «доверенность». Это простая проверка подстрокой, не нужен LLM-as-judge.
- **Guard сурдолога не срабатывает повторно.** Тест: после реплики, в которой бот предложил сурдолога, мама отказалась → проверить, что на следующей реплике бот не предлагает сурдолога снова (флаг `guard_surdologist_suggested` сделал своё дело).