Files
RAG_helper/docs/examples/03_child_patient_guard.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

22 KiB
Raw Permalink Blame History

Пример 03 · Запись ребёнка — защитное условие в шаге уточнения

⚠️ Эта версия устарела. Актуальная — 03_child_patient_guard_v2.md. v1 описывал три защитных условия (для ребёнка, для сурдолога, юридическая оговорка на close) — в коде реально есть только одно (require_legal_rep), остальные — инструкции в промптах. Также имена полей thread_state и слоты приведены к реальной БД и таксономии. Список изменений — внизу v2 в Changelog.

Связано с 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.

Ретривер: срез шага qualifywiki_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 / presentoffer_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 слота, мама выбирает один, идём в bookclose.)


Реплика 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 сделал своё дело).