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>
This commit is contained in:
AR 15 M4
2026-04-27 20:00:44 +05:00
parent f348570b1b
commit 52b46bc53e
43 changed files with 5914 additions and 105 deletions
+86 -25
View File
@@ -201,7 +201,7 @@
<article>
<h1>Как работает мультиагентная система</h1>
<p class="lead">Здесь объясняется, что такое <b>ветка (intent)</b>, как реплика пациента доходит до ответа, и какие защитные механизмы стоят на пути «петель» и потерянного контекста. Английские термины оставлены в скобках — на них завязан код и логи.</p>
<p class="lead">Здесь объясняется, что такое <b>намерение (intent)</b> и <b>ветка (branch)</b>, как реплика пациента доходит до ответа, и какие защитные механизмы стоят на пути «петель» и потерянного контекста. Английские термины оставлены в скобках — на них завязан код и логи.</p>
<div class="toc">
<strong>Содержание</strong>
@@ -211,23 +211,42 @@
<li><a href="#flow">Что происходит на каждой реплике</a></li>
<li><a href="#guards">Защитные механизмы</a></li>
<li><a href="#where">Где что настраивается</a></li>
<li><a href="#examples">Разобранные примеры</a></li>
</ol>
</div>
<h2 id="why">Зачем это всё</h2>
<p>На пилоте у нас был «один большой системный промпт» — модель пыталась одновременно записывать на приём, отвечать на вопросы по ценам и эскалировать острые случаи. По мере усложнения скрипта запись начинала «плыть»: модель забывала шаги, путала ветки, перескакивала через мини-интервью.</p>
<p>Мы перешли на <b>графовую архитектуру (graph-based routing)</b>: реплика пациента сначала идёт в <b>маршрутизатор (router)</b>, который определяет тему, а потом — в <b>ветку (intent)</b>, отвечающую только за свой узкий сценарий. У сложных веток внутри есть собственный <b>пошаговый сценарий (state machine)</b>.</p>
<p>Мы перешли на <b>графовую архитектуру (graph-based routing)</b>: реплика пациента сначала идёт в <b>маршрутизатор (router)</b>, который определяет <b>намерение (intent)</b> — категорию темы. По коду намерения подбирается <b>ветка (branch)</b>, отвечающая только за свой узкий сценарий. У сложных веток внутри есть собственный <b>пошаговый сценарий (state machine)</b>.</p>
<h2 id="terms">Главные термины</h2>
<div class="term-card">
<div class="term-head"><strong>Ветка</strong> <span class="term-en">(intent)</span></div>
<div class="term-body">Изолированный «под-агент» с собственным системным промптом. Отвечает за одну тему: запись, перенос, цены, медицинский вопрос, общая справка, перевод на оператора. У каждой ветки — свой код (<code>new_booking</code>, <code>price_question</code> и т. п.) и активная версия настроек.</div>
<div class="term-head"><strong>Намерение</strong> <span class="term-en">(intent)</span></div>
<div class="term-body">Категория темы, которую распознал маршрутизатор: <code>new_booking</code>, <code>price_question</code>, <code>general_info</code>, <code>medical_question</code>, <code>reschedule</code>, <code>escalate_human</code>. У нас намерение жёстко связано с <b>веткой</b> 1:1 — код намерения совпадает с кодом ветки.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Ветка</strong> <span class="term-en">(branch)</span></div>
<div class="term-body">
<p>Изолированный «под-агент» с собственным системным промптом. Отвечает за одно намерение: запись, перенос, цены, медицинский вопрос, общая справка, перевод на оператора. У каждой ветки — свой код, активная версия настроек, опционально — пошаговый сценарий и защитные условия.</p>
<p style="font-size:12.5px; color:var(--muted); margin-top:8px;"><em>В коде и в БД ветка хранится в таблице <code>intents</code> — исторически. Концептуально это branch, но из-за связи 1:1 с намерением мы пока не разделяли их в коде.</em></p>
</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Маршрутизатор</strong> <span class="term-en">(router)</span></div>
<div class="term-body">Системная ветка <code>_router</code>: отдельный, дешёвый вызов модели, который по последней реплике пациента возвращает один из кодов веток. Не отвечает пациенту напрямую — только классифицирует. Вызывается на КАЖДОЙ реплике, не один раз при входе.</div>
<div class="term-body">Системная ветка <code>_router</code>: отдельный, дешёвый вызов модели, который по последней реплике пациента возвращает один из кодов намерений. Не отвечает пациенту напрямую — только классифицирует. Вызывается на КАЖДОЙ реплике, не один раз при входе.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Решение маршрутизатора</strong> <span class="term-en">(router decision)</span></div>
<div class="term-body">Код намерения, который маршрутизатор вернул на конкретной реплике. Видно в Песочнице бейджем <b>«Решение маршрутизатора: <code>&lt;код&gt;</code>»</b> под ответом ассистента. Если решение совпало с активной веткой — мы остались в ней; если разошлось — сработало либо переключение, либо удержание в ветке.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Активная ветка</strong> <span class="term-en">(active branch)</span></div>
<div class="term-body">Ветка, которая реально сформировала ответ ассистента на этой реплике. Может совпадать с решением маршрутизатора (норма), а может расходиться (если сработало удержание в ветке или возврат из отложенного сценария). Видно в Песочнице бейджем <b>«Активная ветка: <code>&lt;код&gt;</code>»</b>.</div>
</div>
<div class="term-card">
@@ -266,11 +285,11 @@
<p>Защита от ложных переключений внутри пошагового сценария. Это <b>не второй вызов маршрутизатора</b> — один вызов, но с расширенным промптом. Работает пошагово:</p>
<ol style="margin: 8px 0; padding-left: 20px; line-height: 1.8;">
<li>Пациент пишет что-то вроде <em>«а сколько стоит приём?»</em> внутри записи.</li>
<li>Маршрутизатор анализирует реплику и возвращает: <code>general_info</code> (в Песочнице — бейдж «роутер предложил: general_info»).</li>
<li>Система видит: тред уже идёт по многошаговой ветке <code>new_booking</code>, шаг <code>qualify</code>. Переключать опасно — потеряем контекст записи.</li>
<li>Маршрутизатор анализирует реплику и возвращает: <code>general_info</code> (в Песочнице — бейдж «Решение маршрутизатора: <code>general_info</code>»).</li>
<li>Система видит: тред уже идёт по пошаговой ветке <code>new_booking</code>, шаг <code>qualify</code>. Переключать опасно — потеряем контекст записи.</li>
<li>Вместо переключения: <code>effective_code</code> остаётся <code>new_booking</code>, в системный промпт ветки добавляется блок:
<pre><code>[ПОДСКАЗКА РОУТЕРА]
Роутер счёл, что тема — `general_info`.
Маршрутизатор счёл, что тема — `general_info`.
Ты ведёшь сценарий `new_booking`.
Если пациент сменил тему — выдай [INTENT_CHANGE: general_info].
Если реплика в сценарии — зафиксируй в слот и продолжай.</code></pre>
@@ -278,11 +297,11 @@
<li>Модель <code>new_booking</code> получает весь этот контекст и сама решает:
<ul style="margin: 4px 0; padding-left: 18px;">
<li>Ответить на вопрос и остаться → вернёт <code>{"state_after": "qualify", "soft_insertion": true}</code> → бейдж «удержались в ветке» + «боковой вопрос».</li>
<li>Решить, что тема реально сменилась → вернёт <code>[INTENT_CHANGE: general_info]</code> жёсткое переключение.</li>
<li>Решить, что тема реально сменилась → вернёт <code>[INTENT_CHANGE: general_info]</code> → переключение ветки.</li>
</ul>
</li>
</ol>
<p style="margin-top:6px;">Ключевое: решение принимает <b>модель ветки за один вызов</b>, а не отдельный роутер. Подсказка роутера — просто контекст в промпте.</p>
<p style="margin-top:6px;">Ключевое: решение принимает <b>модель ветки за один вызов</b>, а не отдельный маршрутизатор. Подсказка маршрутизатора — просто контекст в промпте.</p>
</div>
</div>
@@ -301,7 +320,7 @@
<div class="term-card">
<div class="term-head"><strong>Структурированный ответ ветки</strong> <span class="term-en">(structured output)</span></div>
<div class="term-body">Каждая sm-ветка возвращает не только текст пациенту, но и служебный JSON-блок в хвосте ответа:
<div class="term-body">Каждая пошаговая ветка возвращает не только текст пациенту, но и служебный JSON-блок в хвосте ответа:
<pre><code>STATE_JSON: {"state_after": "qualify", "slots_updated": {"name": "Алексей"}}</code></pre>
Парсер вырезает этот блок (пациент его не видит), валидатор проверяет легальность <code>state_after</code>, обновляет состояние диалога. Необязательный флаг <code>soft_insertion: true</code> сигнализирует, что это был боковой ответ без продвижения сценария.</div>
</div>
@@ -312,10 +331,10 @@
</div>
<div class="term-card">
<div class="term-head"><strong>Guard (условие перехода)</strong> <span class="term-en">(guard)</span></div>
<div class="term-head"><strong>Защитное условие</strong> <span class="term-en">(guard)</span></div>
<div class="term-body">
<p>Правило, которое блокирует переход шага вперёд, пока не заполнены нужные слоты. Хранится в поле <code>guards</code> каждого шага (Настройки → Шаги → поле Guards). Проверяется <b>после</b> того, как модель вернула корректный <code>state_after</code> — то есть даже если модель «захотела» перейти, валидатор не пустит без нужных данных.</p>
<p>Формат — JSON-объект, где каждый ключ — имя guard'а:</p>
<p>Правило, которое блокирует переход шага вперёд, пока не заполнены нужные слоты. Хранится в поле <code>guards</code> каждого шага (Настройки → Шаги → поле «Защитные условия»). Проверяется <b>после</b> того, как модель вернула корректный <code>state_after</code> — то есть даже если модель «захотела» перейти, валидатор не пустит без нужных данных.</p>
<p>Формат — JSON-объект, где каждый ключ — имя защитного условия:</p>
<pre><code>{
"require_legal_rep": {
"description": "Для записи ребёнка нужны ФИО и телефон законного представителя",
@@ -326,23 +345,43 @@
}</code></pre>
<p>Поля:</p>
<ul style="margin: 4px 0; padding-left: 20px; line-height: 1.8;">
<li><code>trigger_slot</code> — слот, при значении которого guard активируется. Если опущен — guard активен всегда.</li>
<li><code>trigger_slot</code> — слот, при значении которого защитное условие активируется. Если опущен — условие активно всегда.</li>
<li><code>trigger_value</code> — значение, которое должен иметь <code>trigger_slot</code> для активации (например, <code>true</code>).</li>
<li><code>required_slots</code> — список слотов, которые должны быть заполнены для разрешения перехода.</li>
<li><code>description</code> — пояснение для операторов; показывается в «Состоянии диалога» в Песочнице при срабатывании.</li>
</ul>
<p>Сейчас guard задан на шаге <code>qualify</code> ветки <code>new_booking</code>: при <code>is_child: true</code> нельзя перейти в <code>present</code>, пока не заполнены <code>legal_rep_name</code> и <code>legal_rep_phone</code>. В Песочнице при срабатывании появляется красный блок: <b>«🔒 guard require_legal_rep не пройден — ждём: legal_rep_name, legal_rep_phone»</b>.</p>
<p>Сейчас защитное условие задано на шаге <code>qualify</code> ветки <code>new_booking</code>: при <code>is_child: true</code> нельзя перейти в <code>present</code>, пока не заполнены <code>legal_rep_name</code> и <code>legal_rep_phone</code>. В Песочнице при срабатывании появляется красный блок: <b>«🔒 защитное условие <code>require_legal_rep</code> не пройдено — ждём: <code>legal_rep_name</code>, <code>legal_rep_phone</code>»</b>.</p>
</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Счётчик переключений</strong> <span class="term-en">(handoff_count)</span></div>
<div class="term-body">Сколько раз в этом диалоге произошло переключение ветки. Растёт при каждом переключении и сбрасывается в 0 при возврате из отложенного сценария. В Песочнице показан как <b>«Переключений: N из 3»</b> в карточке состояния диалога. По достижении кап-значения срабатывает защита от петли.</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Защита от петли</strong> <span class="term-en">(routing loop guard)</span></div>
<div class="term-body">Счётчик <code>handoff_count</code> в состоянии диалога считает все переключения ветки. При превышении 3 переключений за диалог следующее переключение блокируется: диалог автоматически уходит в <code>escalate_human</code> с шаблонным ответом «Уточню детали с администратором клиники, свяжемся с вами в течение ближайшего часа». Это страховка от циклов вроде «запись ↔ цены ↔ запись ↔ цены».</div>
<div class="term-body">При превышении 3 переключений за диалог следующее переключение блокируется: диалог автоматически уходит в <code>escalate_human</code> с причиной <code>routing_loop</code> и шаблонным ответом «Уточню детали с администратором клиники, свяжемся с вами в течение ближайшего часа». Это страховка от циклов вроде «запись ↔ цены ↔ запись ↔ цены».</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Причина передачи оператору</strong> <span class="term-en">(escalation_reason)</span></div>
<div class="term-body">
<p>Когда диалог уходит в ветку <code>escalate_human</code>, фиксируется причина — для статистики и для оператора, который примет диалог. Возможные значения:</p>
<ul style="margin: 4px 0; padding-left: 20px; line-height: 1.8;">
<li><code>acute_pain</code> — пациент описывает острое состояние.</li>
<li><code>surgery</code> — упоминание операции, наркоза, стационара.</li>
<li><code>angry</code> — агрессивный или раздражённый тон.</li>
<li><code>explicit_request</code> — пациент явно просит оператора («дайте человека»).</li>
<li><code>routing_loop</code> — сработала защита от петли (3+ переключений ветки).</li>
</ul>
<p>В Песочнице видно в карточке ответа: <b>«Передача оператору · причина: <code>&lt;reason&gt;</code>»</b>. Само значение модель возвращает в служебном сигнале <code>[INTENT_CHANGE: escalate_human|<em>reason</em>]</code> либо его подставляет код (например, <code>routing_loop</code> при срабатывании защиты).</p>
</div>
</div>
<div class="term-card">
<div class="term-head"><strong>Состояние диалога</strong> <span class="term-en">(thread state)</span></div>
<div class="term-body">Запись в БД (одна на диалог), хранящая текущую ветку, текущий шаг (если есть), собранные слоты, <code>handoff_count</code> и поля отложенного сценария. Видно в Песочнице справа, в блоке «Состояние диалога».</div>
<div class="term-body">Запись в БД (одна на диалог), хранящая активную ветку, текущий шаг (если есть), собранные слоты, счётчик переключений и поля отложенного сценария. Видно в Песочнице справа, в блоке «Состояние диалога».</div>
</div>
<h2 id="flow">Что происходит на каждой реплике</h2>
@@ -352,7 +391,7 @@
<div class="flow-num">1</div>
<div class="flow-text">
<b>Маршрутизатор классифицирует реплику.</b>
<div class="muted">Отдельный вызов модели с короткой системой и историей. Возвращает один код ветки.</div>
<div class="muted">Отдельный вызов модели с короткой системой и историей. Возвращает один код намерения.</div>
</div>
</div>
@@ -368,7 +407,7 @@
<div class="flow-num">3</div>
<div class="flow-text">
<b>Применяем удержание в ветке.</b>
<div class="muted">Если диалог уже идёт по sm-ветке и маршрутизатор предлагает другую — состояние не сбрасываем, в системный промпт ветки добавляется блок <code>[ПОДСКАЗКА РОУТЕРА]</code>.</div>
<div class="muted">Если диалог уже идёт по пошаговой ветке и маршрутизатор предлагает другую — состояние не сбрасываем, в системный промпт ветки добавляется блок <code>[ПОДСКАЗКА РОУТЕРА]</code>.</div>
</div>
</div>
@@ -392,7 +431,7 @@
<div class="flow-num">6</div>
<div class="flow-text">
<b>Парсим ответ.</b>
<div class="muted">Если есть <code>[INTENT_CHANGE]</code> — переключаемся в новую ветку (если из sm-ветки — запоминаем в отложенный сценарий) и зовём модель ещё раз. Если есть <code>STATE_JSON:</code> — валидируем переход, обновляем шаг и сливаем слоты.</div>
<div class="muted">Если есть <code>[INTENT_CHANGE]</code> — переключаемся в новую ветку (если из пошаговой ветки — запоминаем в отложенный сценарий) и зовём модель ещё раз. Если есть <code>STATE_JSON:</code> — валидируем переход, обновляем шаг и сливаем слоты.</div>
</div>
</div>
@@ -415,8 +454,8 @@
<ul>
<li><b>Удержание в ветке</b> защищает от ложного сброса сценария на коротких репликах вроде «Алексей» или «болит ухо».</li>
<li><b>Валидатор переходов</b> блокирует «прыжки через шаг» — модель не сможет уйти из <code>intro</code> сразу в <code>book</code>.</li>
<li><b>Guard (условие перехода)</b> блокирует переход вперёд до заполнения обязательных слотов. Настраивается в поле Guards каждого шага. Пример: при записи ребёнка нельзя уйти с шага <code>qualify</code>, пока не указаны ФИО и телефон родителя.</li>
<li><b>Защита от петли</b> ограничивает число переключений ветки за диалог. После 3-го — авто-перевод на оператора.</li>
<li><b>Защитное условие</b> блокирует переход вперёд до заполнения обязательных слотов. Настраивается в поле «Защитные условия» каждого шага. Пример: при записи ребёнка нельзя уйти с шага <code>qualify</code>, пока не указаны ФИО и телефон родителя.</li>
<li><b>Защита от петли</b> ограничивает число переключений ветки за диалог. После 3-го — авто-перевод на оператора с причиной <code>routing_loop</code>.</li>
<li><b>Отложенный сценарий</b> возвращает прерванный сценарий с теми же слотами и шагом — пациент не должен повторять имя или повод.</li>
<li><b>Ретрай LLM</b>: и маршрутизатор, и ветка делают один повтор при сетевом сбое DeepSeek. При полном падении — откат транзакции и понятный ответ «модель временно недоступна».</li>
</ul>
@@ -424,12 +463,34 @@
<h2 id="where">Где что настраивается</h2>
<ul>
<li><a href="/settings.html">Настройки</a> — список веток, активные версии промптов, поля «Системный промпт», «Правила», «Условия выхода». Для веток с пошаговым сценарием — вкладка «Шаги» с редактором каждого шага и его допустимых переходов.</li>
<li><a href="/sandbox.html">Песочница</a> — живые диалоги от лица пациента. В правой панели видны: состояние диалога (ветка, шаг, слоты, счётчик переключений, отложенный сценарий), решение маршрутизатора, RAG-фрагменты и собранный системный промпт.</li>
<li><a href="/sandbox.html">Песочница</a> — живые диалоги от лица пациента. В правой панели видны: состояние диалога (активная ветка, шаг, слоты, счётчик переключений, отложенный сценарий), решение маршрутизатора, RAG-фрагменты и собранный системный промпт.</li>
<li><a href="/">Отладка</a> — база знаний (загрузка / переразметка документов), одиночные тестовые вопросы без памяти диалога.</li>
</ul>
<h2 id="examples">Разобранные примеры</h2>
<p>Четыре пошаговых разбора реальных сценариев: что делает маршрутизатор, какая ветка отвечает, на каком шаге, какие слоты заполняются и что происходит в состоянии диалога после каждой реплики. Полезно для понимания архитектуры в работе и как ориентир для будущего eval-набора.</p>
<div class="flow-card" style="padding:0;">
<a href="/example.html?id=01_basic_booking_v2" style="display:block;padding:14px 18px;border-bottom:1px solid var(--border);text-decoration:none;color:var(--fg);">
<div style="font-size:14px;font-weight:600;color:var(--accent);margin-bottom:2px;">Пример 01 · Базовая запись к ЛОР-врачу</div>
<div style="font-size:13px;color:var(--muted);">Линейный happy path: <code>intro → qualify → present → offer_time → book → close</code>. Никаких защитных условий, никаких боковых вопросов, никаких переключений. Самый простой случай — на нём удобно увидеть базовое поведение системы.</div>
</a>
<a href="/example.html?id=02_price_during_booking_v2" style="display:block;padding:14px 18px;border-bottom:1px solid var(--border);text-decoration:none;color:var(--fg);">
<div style="font-size:14px;font-weight:600;color:var(--accent);margin-bottom:2px;">Пример 02 · Вопрос про цену в середине записи</div>
<div style="font-size:13px;color:var(--muted);">Один сценарий разобран в двух вариантах: <b>боковой вопрос</b> (без выхода из ветки) и <b>переключение ветки с возвратом</b> через отложенный сценарий. Лучший пример для понимания различий между этими двумя механизмами.</div>
</a>
<a href="/example.html?id=03_child_patient_guard_v2" style="display:block;padding:14px 18px;border-bottom:1px solid var(--border);text-decoration:none;color:var(--fg);">
<div style="font-size:14px;font-weight:600;color:var(--accent);margin-bottom:2px;">Пример 03 · Запись ребёнка — защитное условие</div>
<div style="font-size:13px;color:var(--muted);">Срабатывание единственного реального защитного условия <code>require_legal_rep</code> на шаге <code>qualify</code>: при <code>is_child=true</code> диалог не уходит в <code>present</code>, пока не собраны <code>legal_rep_name</code> и <code>legal_rep_phone</code>.</div>
</a>
<a href="/example.html?id=04_general_info_simple_v2" style="display:block;padding:14px 18px;text-decoration:none;color:var(--fg);">
<div style="font-size:14px;font-weight:600;color:var(--accent);margin-bottom:2px;">Пример 04 · Простые информационные запросы</div>
<div style="font-size:13px;color:var(--muted);">Короткие диалоги про часы, адрес, проезд, услуги, которых клиника не делает, и переход «справка → запись». Самый дешёвый путь: одна реплика → ветка <code>general_info</code> без машины состояний и без слотов → ответ.</div>
</a>
</div>
<div class="callout">
Документ описывает текущее состояние после Спринта 6b (блоки D + F): удержание в ветке, боковые вопросы, структурированный ответ, guards. Следующее: причина эскалации с reason (блок E), умный маршрутизатор, видящий состояние диалога (блок G).
Документ описывает текущее состояние после Спринта 6b (блоки D + F): удержание в ветке, боковые вопросы, структурированный ответ, защитные условия. Следующее: причина передачи оператору с reason (блок E), умный маршрутизатор, видящий состояние диалога (блок G).
</div>
</article>