- 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>
21 KiB
Пример 03 · Запись ребёнка — защитное условие в шаге уточнения
Связано с
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 — валидатор перехода в коде это проверит и отклонит ход, если переход неправильный.
Начальное состояние
{
"thread_id": "T-9003",
"current_intent": null,
"current_step": null,
"slots": {},
"handoff_count": 0
}
Реплика 1 · «Здравствуйте, хочу записать ребёнка к лору»
Решение роутера: new_booking. Упоминание «ребёнка» само по себе не сигнал эскалации — это норма для записи.
Активная ветка / шаг: new_booking / intro.
Ретривер: не вызывается.
Вызовы инструментов: нет.
Структурированный ответ модели:
{
"reply": "Здравствуйте! Помогу записать ребёнка. Подскажите, как могу к вам обращаться, и как зовут ребёнка?",
"state_after": "intro",
"slots_updated": {
"service_mention": "ЛОР",
"is_child": true
}
}
Заметим: is_child=true модель устанавливает уже здесь, на основании слова «ребёнок». Это раннее срабатывание флага — и оно правильное, потому что определяет всю дальнейшую логику. Шаг при этом не меняется — ждём имена.
thread_state после хода:
{
"current_intent": "new_booking",
"current_step": "intro",
"slots": {
"service_mention": "ЛОР",
"is_child": true
}
}
Реплика 2 · «Меня зовут Мария, ребёнка зовут Матвей, ему 8 лет»
Решение роутера: new_booking.
Активная ветка / шаг: new_booking / intro.
Ретривер: не вызывается.
Структурированный ответ модели:
{
"reply": "Очень приятно, Мария. Записать Матвея на первичный приём к ЛОР-врачу? Что именно беспокоит?",
"state_after": "qualify",
"slots_updated": {
"patient_name": "Матвей",
"patient_age": 8,
"parent_first_name": "Мария"
}
}
Шаг переходит в qualify. Имя ребёнка — patient_name (это пациент, на чьё имя пойдёт запись), имя мамы временно фиксируем как parent_first_name — на следующем под-шаге его дополним до полного ФИО.
thread_state после хода:
{
"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).
Вызовы инструментов: нет.
Структурированный ответ модели:
{
"reply": "Поняла, посмотрим. Прежде чем подобрать слот — для записи ребёнка нам по правилам клиники нужно зафиксировать данные законного представителя. Уточните, пожалуйста, ваше полное ФИО и контактный телефон.",
"state_after": "qualify.legal_rep",
"slots_updated": {
"complaint": "боль в ушах, ухудшение слуха на одно ухо"
}
}
Шаг переходит в под-состояние qualify.legal_rep — guard активен. Жалоба зафиксирована в слотах, но переход в present пока заблокирован.
thread_state после хода:
{
"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 уже в контексте.
Структурированный ответ модели:
{
"reply": "Это стандартная практика — нужна для безопасности ребёнка и для того, чтобы мы могли с вами связаться, если в ходе приёма потребуется ваше согласие на процедуру. Согласно ФЗ № 323-ФЗ мы должны иметь возможность связаться с законным представителем в любой момент. Подскажите, пожалуйста, ваше полное ФИО и телефон.",
"state_after": "qualify.legal_rep",
"slots_updated": {}
}
Шаг и слоты не меняются — guard всё ещё блокирует выход.
Реплика 5 · «Хорошо. Иванова Мария Сергеевна, +79991234567»
Решение роутера: new_booking.
Активная ветка / шаг: new_booking / qualify.legal_rep.
Структурированный ответ модели:
{
"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 после хода:
{
"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).
Структурированный ответ модели:
{
"reply": "С учётом того, что есть жалоба на ухудшение слуха на одно ухо, я бы предложила сначала записать Матвея к нашему сурдологу — он проведёт обследование слуха, и уже с его заключением ЛОР сможет дать более точное лечение. Если вы согласны — подберу слот к сурдологу. Или, если хотите, всё равно идём сразу к ЛОРу?",
"state_after": "present",
"slots_updated": {
"guard_surdologist_suggested": true
}
}
Шаг не меняется — ждём решение мамы. Флаг guard_surdologist_suggested нужен, чтобы при следующей реплике не предлагать сурдолога повторно, если она откажется.
Реплика 7 · «Да, давайте сначала к сурдологу»
Решение роутера: new_booking.
Активная ветка / шаг: new_booking / present → offer_time.
Структурированный ответ модели:
{
"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) — что взять с собой.
Структурированный ответ модели:
{
"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сделал своё дело).