52b46bc53e
Спринт 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>
501 lines
38 KiB
HTML
501 lines
38 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Chat Agent for Patients — Документация</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f5f6f8;
|
||
--panel: #ffffff;
|
||
--border: #e1e4ea;
|
||
--muted: #6b7280;
|
||
--fg: #111827;
|
||
--accent: #2563eb;
|
||
--accent-hover: #1d4ed8;
|
||
--ok: #16a34a;
|
||
--warn: #d97706;
|
||
--err: #dc2626;
|
||
--chip-bg: #eef2ff;
|
||
--mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
html, body { height: 100%; }
|
||
body {
|
||
margin: 0;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--fg);
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
header {
|
||
background: var(--panel);
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 14px 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
header h1 { margin: 0; font-size: 16px; font-weight: 600; }
|
||
.nav { display: flex; gap: 4px; }
|
||
.nav-link {
|
||
text-decoration: none;
|
||
color: var(--muted);
|
||
padding: 6px 12px;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
}
|
||
.nav-link:hover { background: var(--chip-bg); color: var(--fg); }
|
||
.nav-link.active { background: var(--accent); color: #fff; }
|
||
|
||
main {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 32px 24px 80px 24px;
|
||
}
|
||
article {
|
||
max-width: 820px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
article h1 { font-size: 26px; font-weight: 700; margin: 0 0 6px 0; letter-spacing: -0.02em; }
|
||
article .lead {
|
||
color: var(--muted);
|
||
font-size: 15px;
|
||
margin: 0 0 32px 0;
|
||
}
|
||
article h2 {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
letter-spacing: -0.01em;
|
||
margin: 36px 0 12px 0;
|
||
padding-bottom: 6px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
article h3 {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
margin: 20px 0 8px 0;
|
||
}
|
||
article p { margin: 0 0 12px 0; }
|
||
article ul, article ol { margin: 0 0 12px 0; padding-left: 22px; }
|
||
article li { margin: 4px 0; }
|
||
article code {
|
||
background: var(--chip-bg);
|
||
color: var(--accent);
|
||
padding: 1px 6px;
|
||
border-radius: 4px;
|
||
font-family: var(--mono);
|
||
font-size: 12.5px;
|
||
}
|
||
article pre {
|
||
background: #fafbfd;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
padding: 12px 14px;
|
||
overflow-x: auto;
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
line-height: 1.55;
|
||
margin: 8px 0 16px 0;
|
||
}
|
||
article pre code { background: none; color: var(--fg); padding: 0; font-size: 12px; }
|
||
|
||
/* Карточка термина */
|
||
.term-card {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
padding: 14px 16px;
|
||
margin-bottom: 10px;
|
||
}
|
||
.term-card .term-head {
|
||
font-size: 15px;
|
||
margin-bottom: 6px;
|
||
}
|
||
.term-card .term-head strong { font-weight: 700; }
|
||
.term-card .term-en {
|
||
color: var(--muted);
|
||
font-weight: 400;
|
||
}
|
||
.term-card .term-body {
|
||
font-size: 13.5px;
|
||
color: var(--fg);
|
||
}
|
||
|
||
/* Схема пошагово */
|
||
.flow-card {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
padding: 16px 18px;
|
||
margin: 12px 0 18px 0;
|
||
}
|
||
.flow-step {
|
||
display: grid;
|
||
grid-template-columns: 32px 1fr;
|
||
gap: 10px;
|
||
padding: 10px 0;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.flow-step:last-child { border-bottom: none; }
|
||
.flow-num {
|
||
background: var(--accent);
|
||
color: #fff;
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
.flow-step b { font-size: 14px; }
|
||
.flow-step .flow-text { font-size: 13.5px; color: var(--fg); }
|
||
.flow-step .flow-text .muted { color: var(--muted); font-size: 13px; }
|
||
|
||
/* TOC */
|
||
.toc {
|
||
background: var(--panel);
|
||
border: 1px solid var(--border);
|
||
border-radius: 10px;
|
||
padding: 14px 18px;
|
||
margin-bottom: 24px;
|
||
font-size: 13.5px;
|
||
}
|
||
.toc strong { display: block; margin-bottom: 6px; font-size: 13px; }
|
||
.toc ol { margin: 0; padding-left: 22px; }
|
||
.toc a { color: var(--accent); text-decoration: none; }
|
||
.toc a:hover { text-decoration: underline; }
|
||
|
||
.callout {
|
||
background: #fffbeb;
|
||
border: 1px solid #fde68a;
|
||
color: #78350f;
|
||
border-radius: 8px;
|
||
padding: 10px 14px;
|
||
font-size: 13.5px;
|
||
margin: 10px 0 16px 0;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<h1>Chat Agent for Patients</h1>
|
||
<nav class="nav">
|
||
<a href="/" class="nav-link">Отладка</a>
|
||
<a href="/sandbox.html" class="nav-link">Песочница</a>
|
||
<a href="/settings.html" class="nav-link">Настройки</a>
|
||
<a href="/docs.html" class="nav-link active">Документация</a>
|
||
</nav>
|
||
</header>
|
||
|
||
<main>
|
||
<article>
|
||
|
||
<h1>Как работает мультиагентная система</h1>
|
||
<p class="lead">Здесь объясняется, что такое <b>намерение (intent)</b> и <b>ветка (branch)</b>, как реплика пациента доходит до ответа, и какие защитные механизмы стоят на пути «петель» и потерянного контекста. Английские термины оставлены в скобках — на них завязан код и логи.</p>
|
||
|
||
<div class="toc">
|
||
<strong>Содержание</strong>
|
||
<ol>
|
||
<li><a href="#why">Зачем это всё</a></li>
|
||
<li><a href="#terms">Главные термины</a></li>
|
||
<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>ветка (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>, <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>
|
||
|
||
<div class="term-card">
|
||
<div class="term-head"><strong>Решение маршрутизатора</strong> <span class="term-en">(router decision)</span></div>
|
||
<div class="term-body">Код намерения, который маршрутизатор вернул на конкретной реплике. Видно в Песочнице бейджем <b>«Решение маршрутизатора: <code><код></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><код></code>»</b>.</div>
|
||
</div>
|
||
|
||
<div class="term-card">
|
||
<div class="term-head"><strong>Пошаговый сценарий</strong> <span class="term-en">(state machine)</span></div>
|
||
<div class="term-body">Внутренний граф шагов внутри ветки. Сейчас есть только у <code>new_booking</code>: 6 шагов от приветствия до подтверждения записи. Модель на каждой реплике видит, на каком шаге сейчас, и какие <b>слоты</b> уже собраны.</div>
|
||
</div>
|
||
|
||
<div class="term-card">
|
||
<div class="term-head"><strong>Шаг</strong> <span class="term-en">(step)</span></div>
|
||
<div class="term-body">Узел пошагового сценария. У каждого шага свой код (<code>intro</code>, <code>qualify</code>, <code>present</code>, <code>offer_time</code>, <code>book</code>, <code>close</code>), свой кусок промпта и список <b>допустимых переходов</b>.</div>
|
||
</div>
|
||
|
||
<div class="term-card">
|
||
<div class="term-head"><strong>Допустимые переходы</strong> <span class="term-en">(allowed_next)</span></div>
|
||
<div class="term-body">Список кодов шагов, в которые можно перейти с текущего. Например, с <code>qualify</code> разрешено в <code>qualify</code> (остаться) или <code>present</code> (двигаться вперёд), но не в <code>close</code>. Если модель попытается перепрыгнуть через шаг — <b>валидатор переходов (transition validator)</b> отклонит запрос, мы останемся на шаге.</div>
|
||
</div>
|
||
|
||
<div class="term-card">
|
||
<div class="term-head"><strong>Слоты</strong> <span class="term-en">(slots)</span></div>
|
||
<div class="term-body">JSON-словарь данных, которые ветка собирает по ходу разговора. Для записи это <code>name</code>, <code>reason</code>, <code>specialist</code>, <code>preferred_time</code>, <code>confirmed</code>. Модель на каждой реплике их видит и обновляет — старые не переспрашиваются.</div>
|
||
</div>
|
||
|
||
<div class="term-card">
|
||
<div class="term-head"><strong>Условия выхода</strong> <span class="term-en">(exit conditions)</span></div>
|
||
<div class="term-body">Список ситуаций, когда ветка должна вместо обычного ответа выдать служебный сигнал <code>[INTENT_CHANGE: <код_ветки>]</code> и передать диалог другой ветке. Например, если пациент в записи упомянул хирургию — ветка <code>new_booking</code> сама вернёт <code>[INTENT_CHANGE: escalate_human]</code>.</div>
|
||
</div>
|
||
|
||
<div class="term-card">
|
||
<div class="term-head"><strong>Переключение ветки</strong> <span class="term-en">(hard handoff)</span></div>
|
||
<div class="term-body">Полная смена ветки внутри одного диалога с обнулением шага и слотов. Бывает в двух случаях: ветка сама выдала <code>[INTENT_CHANGE]</code> или маршрутизатор предложил другую ветку, которая не имеет пошагового сценария.</div>
|
||
</div>
|
||
|
||
<div class="term-card">
|
||
<div class="term-head"><strong>Удержание в ветке</strong> <span class="term-en">(sticky state machine)</span></div>
|
||
<div class="term-body">
|
||
<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> (в Песочнице — бейдж «Решение маршрутизатора: <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`.
|
||
Ты ведёшь сценарий `new_booking`.
|
||
Если пациент сменил тему — выдай [INTENT_CHANGE: general_info].
|
||
Если реплика в сценарии — зафиксируй в слот и продолжай.</code></pre>
|
||
</li>
|
||
<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>
|
||
</ul>
|
||
</li>
|
||
</ol>
|
||
<p style="margin-top:6px;">Ключевое: решение принимает <b>модель ветки за один вызов</b>, а не отдельный маршрутизатор. Подсказка маршрутизатора — просто контекст в промпте.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="term-card">
|
||
<div class="term-head"><strong>Боковой вопрос</strong> <span class="term-en">(soft insertion)</span></div>
|
||
<div class="term-body">
|
||
<p>Ситуация, когда модель пошаговой ветки отвечает на вопрос вне сценария, <b>не продвигая шаг и не меняя ветку</b>. Типичные примеры внутри <code>new_booking</code>: «а сколько стоит приём?», «у вас есть парковка?», «как долго идёт приём?».</p>
|
||
<p>Откуда система знает, что это боковой вопрос? Модель сама сообщает об этом в структурированном ответе:
|
||
<pre><code>STATE_JSON: {"state_after": "qualify", "slots_updated": {}, "soft_insertion": true}</code></pre>
|
||
Флаг <code>soft_insertion: true</code> + текущий шаг в <code>state_after</code> + пустые <code>slots_updated</code> = «ответил на отвлечение, сценарий не двинулся».
|
||
</p>
|
||
<p>Счётчик <code>soft_insertion_count</code> в состоянии диалога инкрементируется на каждом таком ответе и сбрасывается при смене шага или ветки. При трёх подряд — в системный промпт ветки добавляется жёсткое указание: «верни пациента к вопросу текущего шага».</p>
|
||
<p>В Песочнице: бейдж <b>«тип ответа: боковой вопрос»</b> (жёлтый) на ответе ассистента.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="term-card">
|
||
<div class="term-head"><strong>Структурированный ответ ветки</strong> <span class="term-en">(structured output)</span></div>
|
||
<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>
|
||
|
||
<div class="term-card">
|
||
<div class="term-head"><strong>Отложенный сценарий</strong> <span class="term-en">(suspended intent / resume)</span></div>
|
||
<div class="term-body">Если из пошаговой ветки произошёл переход в другую (например, посреди записи спросили про цены), её состояние (текущий шаг и собранные слоты) запоминается в полях <code>suspended_intent</code>, <code>resumable_step_code</code>, <code>resumable_slots</code>. Когда маршрутизатор увидит, что пациент возвращается к исходной теме («ладно, продолжаем запись»), мы автоматически восстановим шаг и слоты.</div>
|
||
</div>
|
||
|
||
<div class="term-card">
|
||
<div class="term-head"><strong>Защитное условие</strong> <span class="term-en">(guard)</span></div>
|
||
<div class="term-body">
|
||
<p>Правило, которое блокирует переход шага вперёд, пока не заполнены нужные слоты. Хранится в поле <code>guards</code> каждого шага (Настройки → Шаги → поле «Защитные условия»). Проверяется <b>после</b> того, как модель вернула корректный <code>state_after</code> — то есть даже если модель «захотела» перейти, валидатор не пустит без нужных данных.</p>
|
||
<p>Формат — JSON-объект, где каждый ключ — имя защитного условия:</p>
|
||
<pre><code>{
|
||
"require_legal_rep": {
|
||
"description": "Для записи ребёнка нужны ФИО и телефон законного представителя",
|
||
"trigger_slot": "is_child",
|
||
"trigger_value": true,
|
||
"required_slots": ["legal_rep_name", "legal_rep_phone"]
|
||
}
|
||
}</code></pre>
|
||
<p>Поля:</p>
|
||
<ul style="margin: 4px 0; padding-left: 20px; line-height: 1.8;">
|
||
<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>Сейчас защитное условие задано на шаге <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">При превышении 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><reason></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">Запись в БД (одна на диалог), хранящая активную ветку, текущий шаг (если есть), собранные слоты, счётчик переключений и поля отложенного сценария. Видно в Песочнице справа, в блоке «Состояние диалога».</div>
|
||
</div>
|
||
|
||
<h2 id="flow">Что происходит на каждой реплике</h2>
|
||
<div class="flow-card">
|
||
|
||
<div class="flow-step">
|
||
<div class="flow-num">1</div>
|
||
<div class="flow-text">
|
||
<b>Маршрутизатор классифицирует реплику.</b>
|
||
<div class="muted">Отдельный вызов модели с короткой системой и историей. Возвращает один код намерения.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flow-step">
|
||
<div class="flow-num">2</div>
|
||
<div class="flow-text">
|
||
<b>Проверяем отложенный сценарий.</b>
|
||
<div class="muted">Если в состоянии диалога есть <code>suspended_intent</code> и маршрутизатор вернул именно его — восстанавливаем шаг и слоты, очищаем поля, обнуляем счётчик переключений.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flow-step">
|
||
<div class="flow-num">3</div>
|
||
<div class="flow-text">
|
||
<b>Применяем удержание в ветке.</b>
|
||
<div class="muted">Если диалог уже идёт по пошаговой ветке и маршрутизатор предлагает другую — состояние не сбрасываем, в системный промпт ветки добавляется блок <code>[ПОДСКАЗКА РОУТЕРА]</code>.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flow-step">
|
||
<div class="flow-num">4</div>
|
||
<div class="flow-text">
|
||
<b>Если переключение всё-таки происходит — инкрементим счётчик.</b>
|
||
<div class="muted">При превышении 3 — авто-перевод на оператора, без вызова модели.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flow-step">
|
||
<div class="flow-num">5</div>
|
||
<div class="flow-text">
|
||
<b>Собираем системный промпт ветки.</b>
|
||
<div class="muted">Базовый промпт + промпт текущего шага (если есть) + блок текущего состояния (шаг, слоты, подсказка). Зовём модель.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flow-step">
|
||
<div class="flow-num">6</div>
|
||
<div class="flow-text">
|
||
<b>Парсим ответ.</b>
|
||
<div class="muted">Если есть <code>[INTENT_CHANGE]</code> — переключаемся в новую ветку (если из пошаговой ветки — запоминаем в отложенный сценарий) и зовём модель ещё раз. Если есть <code>STATE_JSON:</code> — валидируем переход, обновляем шаг и сливаем слоты.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flow-step">
|
||
<div class="flow-num">7</div>
|
||
<div class="flow-text">
|
||
<b>Сохраняем сообщение пациента, ответ модели и обновлённое состояние диалога.</b>
|
||
<div class="muted">Всё одной транзакцией. Если что-то упадёт — откатываем целиком, «диалог-призрак» в списке не появится.</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div class="callout" style="margin-top: 16px;">
|
||
<b>Почему бейджи — на ответах ассистента, а не на репликах пациента?</b><br>
|
||
Маршрутизатор обрабатывает каждую реплику пациента, но результат его решения материализуется в <em>следующем</em> ответе ассистента: именно там формируется ветка, шаг, события (sticky, боковой вопрос и т.д.). Поэтому в Песочнице бейджи стоят под ответом бота, а не над вопросом пациента — они описывают обработку предшествующей реплики.
|
||
</div>
|
||
|
||
<h2 id="guards">Защитные механизмы</h2>
|
||
<ul>
|
||
<li><b>Удержание в ветке</b> защищает от ложного сброса сценария на коротких репликах вроде «Алексей» или «болит ухо».</li>
|
||
<li><b>Валидатор переходов</b> блокирует «прыжки через шаг» — модель не сможет уйти из <code>intro</code> сразу в <code>book</code>.</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>
|
||
|
||
<h2 id="where">Где что настраивается</h2>
|
||
<ul>
|
||
<li><a href="/settings.html">Настройки</a> — список веток, активные версии промптов, поля «Системный промпт», «Правила», «Условия выхода». Для веток с пошаговым сценарием — вкладка «Шаги» с редактором каждого шага и его допустимых переходов.</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): удержание в ветке, боковые вопросы, структурированный ответ, защитные условия. Следующее: причина передачи оператору с reason (блок E), умный маршрутизатор, видящий состояние диалога (блок G).
|
||
</div>
|
||
|
||
</article>
|
||
</main>
|
||
|
||
</body>
|
||
</html>
|