Files
RAG_helper/static/docs.html
T
AR 15 M4 c3b874dc37 docs: карточка Guard + упоминание в защитных механизмах
Добавлена карточка «Guard (условие перехода)» в раздел терминов:
формат JSON, описание полей, пример require_legal_rep из new_booking.
Guard добавлен в список защитных механизмов. Обновлён callout статуса.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 18:46:12 +05:00

440 lines
29 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>, как реплика пациента доходит до ответа, и какие защитные механизмы стоят на пути «петель» и потерянного контекста. Английские термины оставлены в скобках — на них завязан код и логи.</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>
</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>
<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>
<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">(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: &lt;код_ветки&gt;]</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> (в Песочнице — бейдж «роутер предложил: general_info»).</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">Каждая sm-ветка возвращает не только текст пациенту, но и служебный 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>Guard (условие перехода)</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>
<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> — слот, при значении которого guard активируется. Если опущен — guard активен всегда.</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>
</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>
<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>
<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">Если диалог уже идёт по sm-ветке и маршрутизатор предлагает другую — состояние не сбрасываем, в системный промпт ветки добавляется блок <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> — переключаемся в новую ветку (если из sm-ветки — запоминаем в отложенный сценарий) и зовём модель ещё раз. Если есть <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>Guard (условие перехода)</b> блокирует переход вперёд до заполнения обязательных слотов. Настраивается в поле Guards каждого шага. Пример: при записи ребёнка нельзя уйти с шага <code>qualify</code>, пока не указаны ФИО и телефон родителя.</li>
<li><b>Защита от петли</b> ограничивает число переключений ветки за диалог. После 3-го — авто-перевод на оператора.</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>
<div class="callout">
Документ описывает текущее состояние после Спринта 6b (блоки D + F): удержание в ветке, боковые вопросы, структурированный ответ, guards. Следующее: причина эскалации с reason (блок E), умный маршрутизатор, видящий состояние диалога (блок G).
</div>
</article>
</main>
</body>
</html>