3c71372ec8
Добавлена /docs.html — обзор мультиагентной системы для оператора. Все термины в формате «русский (english)», жирным: ветка (intent), маршрутизатор (router), пошаговый сценарий (state machine), шаг (step), допустимые переходы (allowed_next), слоты (slots), условия выхода (exit conditions), переключение ветки (hard handoff), удержание в ветке (sticky state machine), структурированный ответ (structured output), отложенный сценарий (suspended/resume), защита от петли (routing loop guard), состояние диалога (thread state). Плюс пошаговая схема обработки реплики и резюме защитных механизмов. Ссылка «Документация» добавлена в шапку всех страниц. Унификация заголовков под стиль «Версии» в правом сайдбаре Настроек: убран uppercase, переход на 13px / var(--fg) / font-weight 600 / зажатый letter-spacing. Применилось к .col-head во всех колонках, .field label в редакторе, .section-header в списке веток, заголовкам столбцов на странице Отладки и заголовкам секций RAG-результата. Бейджи (АКТИВНАЯ, система) оставлены прежними — это статусные метки, не заголовки. Переименование ветки escalate_human для согласованности с русским UI: «Эскалация на оператора» → «Перевод на оператора», описание тоже. Точечная миграция при старте (intent_service.migrate_intent_copy) обновляет существующие записи в БД, только если поле в точности совпадает со старым значением — операторские правки не затираются. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
375 lines
21 KiB
HTML
375 lines
21 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>, как реплика пациента доходит до ответа, и какие защитные механизмы стоят на пути «петель» и потерянного контекста. Английские термины оставлены в скобках — на них завязан код и логи.</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: <код_ветки>]</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">Защита от ложных переключений: если диалог идёт по пошаговому сценарию (например, <code>new_booking · qualify</code>) и маршрутизатор на короткой реплике («Алексей», «болит ухо») предлагает другую ветку — мы НЕ сбрасываем состояние, а передаём модели подсказку «маршрутизатор думает X, но ты в Y». Модель сама решает: остаться в сценарии (заполнить слот) или явно выйти через <code>[INTENT_CHANGE]</code>.</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>, обновляет состояние диалога.</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">(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>
|
|
|
|
<h2 id="guards">Защитные механизмы</h2>
|
|
<ul>
|
|
<li><b>Удержание в ветке</b> защищает от ложного сброса сценария на коротких репликах вроде «Алексей» или «болит ухо».</li>
|
|
<li><b>Валидатор переходов</b> блокирует «прыжки через шаг» — модель не сможет уйти из <code>intro</code> сразу в <code>book</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">
|
|
Документ описывает текущее состояние после Спринта 6a. Перевод на оператора с указанием причины (acute_pain / surgery / routing_loop / …), сводка для оператора и умный маршрутизатор, видящий состояние диалога — в Спринте 6b.
|
|
</div>
|
|
|
|
</article>
|
|
</main>
|
|
|
|
</body>
|
|
</html>
|