Files
RAG_helper/docs/architecture/GRAPH_ARCHITECTURE_v3.md
AR 15 M4 52b46bc53e feat(sprint6c+sprint7): терминология, сверка примеров с кодом, мульти-RAG (часть A)
Спринт 6c — терминология и сверка документации с реальным кодом:
- Словарь терминов в static/docs.html: «маршрутизатор» вместо «роутер»,
  «защитное условие» вместо «guard», «пошаговая ветка» вместо «многошаговая».
  Разделены концепты «намерение» (intent) и «ветка» (branch) с пометкой,
  что в коде они хранятся как одна сущность 1:1.
- Песочница: «Решение маршрутизатора» виден всегда (зелёный/жёлтый),
  счётчик переключений «N из 3» отдельной плашкой, бейджи под словарь.
- Настройки: «Условия перехода» → «Защитные условия (guards, JSON)».
- GRAPH_ARCHITECTURE_v4.md: имена полей thread_state и слоты приведены
  к реальной БД (db/models/thread_state.py) и таксономии промптов шагов
  (prompts/intents/new_booking/steps/). Ссылки на *_v2 примеры. На v3
  поставлена шапка «устарело».
- 4 примера переписаны как *_v2: реальные current_intent_code/
  current_step_code/slots_json, реальные allowed_next без двойных переходов,
  реальная таксономия слотов name/reason/specialist/preferred_time/confirmed.
  Удалены вымышленные CRM tool calls и слоты, которых нет в коде.
- static/example.html — параметризованная страница с навигацией между
  4 примерами; роут GET /api/docs/examples/{name} в main.py отдаёт
  markdown без дублирования файлов.
- Редактирование документов в Отладке: GET/PUT /documents/{id}/raw,
  textarea с переразметкой и обновлением Chroma при сохранении.

Спринт 7, часть A — мульти-RAG через подписку ветка↔документы:
- Миграция: таблица intent_documents (M:N), модель IntentDocument,
  индекс по document_id для обратного поиска.
- API: GET/PUT /intents/{code}/documents и GET/PUT /documents/{id}/intents
  с PUT-семантикой «полный список», атомарно. Сервис
  services/intent_document_service.py.
- Retrieval-фильтр в chat_service: подтягивает document_ids активной
  ветки и передаёт в vectorstore.query(). Дефолт пустой подписки —
  document_ids=[] (= 0 чанков), не «вся коллекция»: пустая подписка
  означает «ветка не настроена», подмешивать случайное хуже, чем
  ничего. vectorstore.query() различает None (нет фильтра) и [] (0).
- UI Настроек: блок «Документы базы знаний» в правом сайдбаре,
  всегда видим независимо от вкладки, сортировка по имени, счётчик
  «N из M», PUT при сохранении.
- UI Отладки: третья кнопка «привязка» рядом с «удалить» —
  раскрывашка со списком веток (галочки), быстрая привязка прямо
  на странице загрузки.
- Песочница: блок «Срез RAG» с подпиской/найдено, ворнинг при пустой
  подписке. Поле rag_subscription в QueryResponse и ChatResponse.
- Системный промпт страницы Отладки переехал в обычную ветку _debug
  («Страница отладки»). Удалён prompts/system_prompt.md и логика
  DEFAULT_SYSTEM_PROMPT в llm_client. routers/query.py подтягивает
  активный конфиг ветки _debug и её подписки. Дефолт пустой подписки
  для _debug — None (вся коллекция), не [] как для пациентских — чтобы
  Отладка работала «из коробки». На странице Отладки info-bar показывает
  активную версию и счётчик подписок, ссылка → Настройки.
- Тест-блок «Тест-вопрос» в центре Настроек: расширил /query
  параметрами intent_code (default _debug), system_prompt (override
  для теста черновика из textarea), disable_rag (для _router).
  Редактор промпта обёрнут в <details open> — можно свернуть до
  одной строки. Под ним — три колонки результата (RAG / промпт /
  ответ). Для _router показывается подсказка про отсутствие RAG.

Документы:
- data/datasets/*.md — наработки по 6 веткам (рабочие материалы оператора).
- docs/BRANCH_MAP_AND_PROMPTS_v1.md, docs/OPTIMIZATION_CONVERSION_v1.md,
  docs/guides/state_machine_and_slots.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:00:44 +05:00

43 KiB
Raw Permalink Blame History

Графовая архитектура: роутер намерений + изолированные ветки

⚠️ Эта версия устарела. Актуальная — GRAPH_ARCHITECTURE_v4.md. Архитектурные решения те же, но имена полей thread_state и иллюстративные слоты приведены к тому, как они реально называются в БД и в промптах шагов. Список изменений — в Changelog v4.

Версия 3 · 2026-04-26. По содержанию — то же, что v2, но переписано в стиле «русское объяснение + английский термин в скобках» для всех понятий, которые будут встречаться в коде, в промптах или в названиях полей БД. В конце документа — разделы со ссылками на разобранные примеры (см. файлы в ../examples/). Изменения относительно v2 — в разделе Changelog.

Документ фиксирует направление, в которое двигается проект после пилота Спринтов 1–3. Перепланировка спринтов сделана в SPRINTS.md — здесь только сама архитектура и почему она нам нужна.


Соглашение о терминах

В документе встречаются понятия, которые одновременно:

  • являются обычными русскими словами в обиходе,
  • и одновременно — идентификаторами полей БД, переменных в коде, ключей в JSON-структурах.

Чтобы не было путаницы, при первом упоминании в разделе мы пишем русский термин и сразу даём английский эквивалент (в том виде, в котором он живёт в коде). Например: намерение (intent), машина состояний (state machine), счётчик переключений (handoff_count). Ниже по тексту того же раздела используется тот вариант, который удобнее по контексту.


Проблема, с которой сталкиваемся

Текущая реализация — это «мега-промпт»: в один системный промпт положен весь скрипт поведения агента, плюс правила, плюс инструкции по всем возможным темам (запись, перенос, цены, подготовка к приёму, ДМС, детский приём и т. д.).

На MVP это работает. Но как только добавим реальные бизнес-процессы с несколькими этапами (например, запись с перехватом инициативы в 6 шагов) — модель начнёт «плыть»:

  • Забывать начало инструкций в конце длинного промпта.
  • Перескакивать этапы мини-интервью.
  • Пытаться применять правила не к месту — например, запустить скрипт записи, когда пациент просто спросил, как доехать.
  • Путать ветки между собой, потому что они все лежат в одном контексте.

Это классическая ловушка production-ready ассистентов. Дело не в мощности модели (DeepSeek более чем достаточно), а в архитектуре: один промпт не должен знать про всё одновременно.


Архитектура, к которой идём

Паттерн называется маршрутизация на основе графа (graph-based routing) или мультиагентная система (multi-agent system). Идея проста:

  1. Входная реплика пациента идёт не сразу в отвечающего агента, а в роутер (router).
  2. Роутер определяет намерение (intent) пациента и передаёт диалог в конкретную изолированную ветку (branch).
  3. Каждая ветка — это отдельный узкий промпт, который умеет делать одну вещь хорошо.
  4. Ветки не замкнуты: в любой момент агент может вернуть управление роутеру, если контекст изменился.
┌─────────────┐
│  Пациент    │
└──────┬──────┘
       │
┌──────▼──────────────────────────┐
│  Роутер (LLM-классификатор)     │
│  определяет намерение           │
└──────┬──────────────────────────┘
       │
       ├──→ Ветка «Новая запись»  (new_booking, машина состояний, 6 шагов + guard'ы)
       ├──→ Ветка «Перенос / отмена»  (reschedule)
       ├──→ Ветка «Цены и ДМС»  (price_question)
       ├──→ Ветка «Медицинский вопрос»  (medical_question, канонический ответ)
       ├──→ Ветка «Общая справка»  (general_info, адрес, часы, проезд)
       └──→ Ветка «Эскалация»  (escalate_human, reason: surgery |
                                                       acute_pain |
                                                       angry |
                                                       explicit_request |
                                                       routing_loop)

Шесть веток — то же количество, что сидится при первом запуске Спринта 4. Хирургия и острая боль не отдельные ветки, а значение поля причина эскалации (reason) внутри escalate_human — так решили на развороте 2026-04-23.


1. Роутер — входной узел

Отдельный, быстрый и дешёвый вызов языковой модели (LLM, large language model). Сам пациенту не отвечает — только классифицирует.

Задача роутера:

  • Проанализировать последнюю реплику пациента + краткую историю диалога.
  • Вернуть код намерения (intent code) — одну из заранее заданных категорий.
  • Если детектирован острый случай (боль, кровотечение, упоминание операции) — маршрутизировать в escalate_human с соответствующим reason.

Пример промпта роутера:

Определи намерение пользователя. Варианты:

  1. new_booking — новая запись.
  2. reschedule — перенос или отмена существующей записи.
  3. price_question — цены, ДМС, оплата.
  4. medical_question — симптомы, диагноз, лечение (немедленная эскалация не требуется).
  5. general_info — как доехать, часы работы, контакты.
  6. escalate_human — пациент явно просит оператора, злится, описывает острое состояние, упоминает операцию.

Верни только код намерения. Для escalate_human дополнительно верни reason из списка: acute_pain, surgery, angry, explicit_request.

Роутер продолжает незримо присутствовать в диалоге — его вызывают на каждой реплике, не один раз при входе. Это двойная защита: если ветка не поймала условие выхода (exit condition) сама, роутер увидит изменение намерения (intent'а) и инициирует переход в другую ветвь (handoff).


2. Узкоспециализированные ветки (sub-agents)

Каждая ветка — отдельный промпт, который ничего не знает про другие ветки. Он видит:

  • Свой системный промпт (узкий, под одну задачу).
  • Свой срез базы знаний (см. раздел 6).
  • Историю диалога (чтобы не переспрашивать имя/симптомы).
  • Текущий шаг машины состояний — если она в этой ветке есть.

Примеры:

Ветка «Новая запись» (new_booking). 6-этапный промпт-продавец с условными ветвлениями. Перехват инициативы, мини-интервью по услуге и врачу, презентация приёма, два слота + «настоять на записи», бронирование, закрытие с проговариванием даты/врача/адреса/стоимости. Подробно — в разделе 3 и в 01_basic_booking.md.

Ветка «Перенос / отмена» (reschedule). Другой промпт: извиниться, уточнить текущую запись, сверить с календарём, предложить варианты. RAG не используется — работа через вызовы инструментов (tool calls) к CRM.

Ветка «Медицинский вопрос» (medical_question). Канонический ответ: «не могу консультировать, это к врачу. Записать вас к профильному специалисту?» — с мягким переходом в new_booking. Никакого RAG по медицинским темам намеренно (юридический риск).

Ветка «Эскалация» (escalate_human). Короткая: извиниться, передать оператору. Перед передачей формируется саммари с reason, историей и собранными слотами.


3. Машина состояний внутри ветки

Для сложных скриптов (вроде записи) недостаточно иметь один промпт — нужна ещё память о том, на каком шаге мы сейчас находимся (current_step) и какие данные мы уже собрали (slots).

3.1 Базовая линейная цепочка

Пример состояния треда (thread state) для new_booking:

{
  "intent": "new_booking",
  "step": "offer_time",
  "slots": {
    "patient_name": "Анна",
    "is_child": false,
    "service": "первичный ЛОР",
    "doctor": "Сушков М. Г.",
    "time_candidates": ["2026-04-24 10:00", "2026-04-24 15:00"],
    "time_chosen": null
  }
}

Модель на каждом ходе видит: «Я на шаге offer_time, слот time_candidates заполнен, значит следующим сообщением я должна получить выбор времени, а не представляться заново». Это убирает «перескоки» и «забывания».

Состояние треда хранится в отдельной таблице thread_state с JSON-колонкой под слоты (см. раздел «Что это меняет в данных»). Полный пример заполнения слотов реплика за репликой — в 01_basic_booking.md.

3.2 Защитные условия (guards) и ветвления внутри скрипта

Линейная цепочка из шести шагов — идеальный случай. В реальном скрипте записи (см. вики клиники) есть как минимум три защитных условия (guards), которые ломают линейность:

  • Пациент — ребёнок. На шаге qualify обязательно собрать ФИО и телефон законного представителя. Блокирует переход в present, пока слоты не заполнены. Юридическое требование, не косметика.
  • Запрос конкретного врача (например, Ворончихиной). Вместо шага offer_time диалог уходит в рукав «лист ожидания» (waitlist): запись в очередь вместо предложения слотов.
  • Жалоба на слух без обследования у сурдолога. На шаге present модель должна предложить записаться сначала к сурдологу, и только потом — к отоневрологу.

Моделировать guard'ы можно двумя способами:

Условные переходы (conditional transitions). Шаг qualify имеет два возможных next-step'а: present (обычно) или collect_legal_rep (если is_child=true), и только после заполнения переходит дальше.

Под-состояния (sub-states). Внутри qualify есть qualify.base и qualify.legal_rep, последнее активируется при is_child=true.

Рекомендуем первый вариант — он проще и легче тестируется. Разбор guard'а с ребёнком на конкретном диалоге — в 03_child_patient_guard.md.

3.3 Структурированный выход модели + валидатор переходов

Чисто управляемые моделью (LLM-driven) переходы — где в промпте написано «если слот заполнен, переходи к следующему шагу» — фрагильны. Модель периодически «не замечает» заполнение слота и застревает или, наоборот, прыгает через шаг.

Гибридный подход надёжнее. Модель возвращает структурированный ответ (structured output):

{
  "reply": "Записала вас на четверг, 10:00...",
  "state_after": "close",
  "slots_updated": {
    "time_chosen": "2026-04-24 10:00"
  }
}

Код:

  1. Валидирует легальность переходаoffer_time → close допустим, intro → book нет.
  2. Сохраняет слоты строго — что модель обновила, то и попало в thread_state.
  3. Логирует несоответствия — если модель вернула несуществующее state_after, состояние остаётся прежним, в лог пишется предупреждение.

Модель рассуждает содержательно, код защищает механически. Прибавка — около 50 строк валидатора, снижение нестабильности — заметное.

3.4 RAG-срез на уровне шага, а не только ветки

Разным шагам одной ветки нужны разные куски вики. Для new_booking:

Шаг (step) Срез базы знаний (wiki_sources) Инструмент (tool)
intro
qualify /wiki/services/**, /wiki/doctors/**
present /wiki/services/**, /wiki/doctors/**, /wiki/preparation/**
offer_time /wiki/services/** (для боковых вопросов) crm.get_slots
book crm.create_booking
close /wiki/contacts/**, /wiki/preparation/**

Поле «источники базы знаний» (wiki_sources) имеет смысл определять на уровне шага, а не только ветки. Ветка задаёт значения по умолчанию, шаг может их сузить или расширить.


4. Условия выхода: динамическая маршрутизация

4.1 Жёсткий переход в другую ветвь (hard handoff)

Каждая ветка знает не только как вести разговор, но и когда из него выйти. В системный промпт ветки зашивается блок «условий выхода» (exit conditions):

Если в любой момент пациент упоминает операцию, наркоз, стационар, удаление гланд, септопластику, стапедопластику — прекрати скрипт записи и выдай служебный сигнал: [INTENT_CHANGE: escalate_human] с reason=surgery.

Когда оркестратор видит такой сигнал в ответе модели:

  1. Останавливает текущую ветку.
  2. Сохраняет текущее состояние как suspended_intent + resumable_step + resumable_slots (см. 4.4).
  3. Передаёт всю историю в роутер.
  4. Запускает новую ветку — бесшовно для пользователя.

Полный разбор жёсткого перехода с возвратом — в 02_price_during_booking.md.

4.2 Мягкая вставка (soft insertion) — боковой вопрос без выхода из ветки

Не каждое отклонение от темы — это переход в другую ветвь. Частый случай: пациент посреди записи спрашивает «а сколько это стоит?» или «где вы находитесь?». Это не смена темы, это короткий параллельный вопрос, после которого нужно продолжить скрипт записи с того же шага.

Различение:

  • Мягкая вставка (soft insertion) — на вопрос можно ответить одной репликой без запуска собственной машины состояний. Цена услуги, адрес, длительность приёма, требования к документам. Ветка отвечает сама, поле current_step не меняется.
  • Жёсткий переход (hard handoff) — вопрос сам по себе требует процесса (перенос существующей записи, запись другого человека, хирургия). Полный выход к роутеру.

Практически: ветка new_booking имеет read-only доступ к RAG-срезам price и info, и в её промпте прописано правило: «короткие боковые вопросы отвечай сам, не покидая шаг». Модели этого обычно достаточно; если правило проскакивает — двойной прогон роутера на следующей реплике поймает ошибку.

Сравнение мягкой вставки и жёсткого перехода на одном и том же сценарии — в 02_price_during_booking.md.

4.3 Защита от петель: handoff_count

Без ограничения легко получить цикл маршрутизации (routing loop) — «booking → price → booking → price → ...» на несогласованных промптах. Поэтому в thread_state заводится счётчик:

  • handoff_count инкрементится при каждом жёстком переходе.
  • Кап — 2–3 переключения за сессию.
  • При превышении — автоматическая маршрутизация в escalate_human с reason=routing_loop.

Это дешёвая страховка, которая окупается на первом же багованном промпте.

4.4 Возобновление после перехода: suspended_intent + resumable_state

Если ветка вышла по soft-handoff'у для короткого ответа — ок, через мгновение продолжает. Если произошёл жёсткий переход и боковая (detour) ветка закрылась — пациент часто возвращается к исходной задаче. Пример:

  • Пациент в new_booking на шаге offer_time.
  • Переспросил про цену — ушли в price_question.
  • Получил ответ, говорит «ок, тогда бронируем на четверг».
  • Должен вернуться в new_booking на шаг offer_time, не в intro.

Для этого при выходе из ветки в thread_state сохраняются:

{
  "current_intent": "price_question",
  "current_step": null,
  "suspended_intent": "new_booking",
  "resumable_step": "offer_time",
  "resumable_slots": { "...копия слотов new_booking..." }
}

Роутер, приняв решение о возврате, восстанавливает current_intent из suspended_intent, current_step из resumable_step, слоты — из resumable_slots. Полный диалог с разбором изменений thread_state на каждом ходе — в 02_price_during_booking.md.


5. Передача человеку (escalation)

Часть сценариев не заканчивается в боте — агент маршрутизирует пациента в контакт-центр. Важное отличие от «просто сбросить диалог» — система отдаёт оператору полный контекст (full handoff context):

  • Полную историю переписки.
  • Распознанное намерение + причину эскалации (reason из списка acute_pain / surgery / angry / explicit_request / routing_loop).
  • Собранные слоты, если они уже есть (ФИО, телефон, услуга, предпочитаемый врач).
  • Флаг suspended_intent, если эскалация прервала другую ветку.

Это превращает ассистента не в «фильтр перед оператором», а в инструмент квалификации лида (lead qualification). Дальнейшая маршрутизация (какому именно оператору, в какую очередь) — задача смежного разработчика при подключении каналов.


6. RAG: коллекции на ветку или подписка ветки на разделы вики?

Здесь два технически рабочих подхода с очень разными эксплуатационными свойствами.

Вариант А — отдельная коллекция на ветку

(как описано в v1 и как было запланировано в Спринте 6.)

  • Каждая ветка имеет собственную векторную коллекцию (vector collection) в Chroma.
  • Загрузка документа требует выбора ветки.
  • Поле collection_name в intents.
  • Плюсы: жёсткая изоляция по умолчанию, простой query-путь.
  • Минусы: дублирование (одна статья вики часто нужна нескольким веткам); лишнее решение на каждый upload; сложнее поддерживать при росте вики.

Вариант Б — одна коллекция + подписка ветки на разделы

  • Одна общая Chroma-коллекция clinic_wiki.
  • В таблице intents поле «источники» (wiki_sources: list[str]) — список префиксов путей или набор идентификаторов документов (document ids).
  • Поисковик-ретривер (retriever) применяет фильтр по метаданным (metadata filter, where-filter): doc_path STARTS WITH any(...).
  • Один документ, нужный нескольким веткам, перечисляется в wiki_sources нескольких веток — физического дублирования нет.
  • Плюсы: структура вики = единый источник истины (single source of truth); новая страница в /wiki/pricing/ автоматически попадает в price_question без правок конфига; операторы и так ведут вики — не добавляется отдельный процесс тегирования.
  • Минусы: требует дисциплины в структуре папок вики.

Рекомендация для проекта — Вариант Б. Причина: вики у клиники уже атомарная, регулярно обновляемая, с осмысленной структурой. Добавлять поверх неё тегирование чанков или физическую фрагментацию по коллекциям — это второй слой, который будет расходиться с первым. При Варианте Б «источник правды» один — сама вика.

Дополнительно: wiki_sources на уровне шага

Внутри ветки new_booking разным шагам нужны разные срезы (см. 3.4). Это решается тем, что поле wiki_sources существует на двух уровнях:

  • на intents — значения по умолчанию для ветки;
  • на шаге машины состояний — уточнение/сужение для конкретного состояния.

Что это меняет в данных

Сейчас в БД:

  • threads, messages — диалоги (Спринт 2).
  • agent_configs — один активный системный промпт на всё (Спринт 3).
  • intents — справочник веток (Спринт 4).

После полного перехода на графовую архитектуру понадобится:

  • intents — добавить поле wiki_sources: list[str] для Варианта Б мульти-RAG.
  • agent_configs — привязан к intent_id, у каждой ветки свой активный промпт и свои условия выхода (уже заложено в Спринте 4).
  • thread_state — текущее состояние треда:
    • thread_id (PK, FK)
    • current_intent
    • current_step
    • slots (JSON)
    • handoff_count (int, default 0) — счётчик переключений, защита от петель.
    • suspended_intent (nullable) — отложенное намерение, ветка, из которой вышли по жёсткому переходу.
    • resumable_step (nullable) — шаг в suspended_intent, куда возвращаться.
    • resumable_slots (JSON, nullable) — слоты той ветки.
    • updated_at.
  • Машина состояний на ветке — для new_booking справочник шагов + допустимых переходов (может быть в коде или в БД, на старте достаточно в коде).
  • routing_log (опционально) — лог решений роутера: намерение, срабатывание условия выхода, инкремент handoff_count. Нужен для отладки и тюнинга.

Что это меняет в UI

  • «Настройки агента» — настройки веток: слева список веток, справа редактор промпта и условий выхода для выбранной ветки. Для веток с машиной состояний — дополнительная вкладка со списком шагов и их промптами.
  • В «Песочнице» отладочная панель показывает: текущее намерение (current_intent), шаг машины состояний (current_step), собранные слоты (slots), счётчик переключений (handoff_count), отложенное намерение (suspended_intent), если есть, и историю переходов между ветками (handoff history) в рамках треда.
  • «Сценарии» (Спринт 7) прогоняют не только диалог, но и проверяют: правильно ли роутер классифицировал намерение на каждой реплике, корректно ли сработали условия выхода, восстановилось ли состояние после боковой ветки.

7. Eval-набор нужен до Спринта 5

В плане Спринт 7 — полноценная подсистема сценариев. Это правильная цель, но реализация bouncing'а в Спринте 5 требует минимального набора оценочных кейсов (eval set, evaluation set) уже на входе. Иначе реализуем переход «на глазок», без способа понять, стало лучше или хуже после правки промпта.

Минимум:

  • Eval роутера. 20–30 фраз на каждую ветку: типичные, пограничные (ловушечные), злые (опечатки, короткие, эмоциональные). Формат: CSV фраза, ожидаемый_intent.
  • Eval перехода. 5–10 многошаговых мини-диалогов: намерение на реплике 1 → пациент сменил тему на реплике 2 → на реплике 3 проверяем, что ветка ушла на роутер и роутер правильно переключил.
  • Eval возобновления. 3–5 сценариев: detour → возврат. Проверяем, что current_step восстановился из resumable_step.

Реализация — короткий скрипт, прогоняющий набор через /chat и сравнивающий решения. Будет заменён полноценной подсистемой Спринта 7, но до этого закроет ~80% регрессий.


Открытые вопросы

Часть вопросов из v1 закрылась на развороте 2026-04-23. Актуальный список:

  1. Фреймворк оркестровки — решено: пишем вручную на Python. LangGraph/n8n не берём.
  2. Роутер — отдельная модель — отложено: пока DeepSeek через отдельный RouterClient, чтобы сменить модель в одном месте. Пересмотрим, когда вызовов станет много.
  3. Формат условий выхода — текстом в промпте ветки + независимый прогон роутера на каждой реплике. Если реальный прогон покажет, что свободный текст пропускает случаи смены темы — добавим структурированный список триггеров (trigger list, keyword-match).
  4. Уверенность роутера (confidence score) — не на первом спринте. После живого прогона посмотрим на реальные ошибки, и если их много — добавим уточняющий вопрос (clarifying question) при низкой уверенности.

Новые вопросы после v2:

  1. Момент обновления current_step. Сразу после парсинга state_after из ответа модели, или после того как ответ успешно показан пациенту? При ошибке доставки состояние может разъехаться с тем, что пациент видел.
  2. Кап на мягкие вставки. Если пациент крутит ассистенту пять побочных вопросов подряд, не продвигаясь по записи — это нормально или это сигнал «пациент не хочет записываться, эскалировать»? Нужен ли кап на число инлайн-ответов до возврата к шагу скрипта.
  3. Шаги записи — из вики или из головы. Шесть шагов new_booking формализованы нами, но скрипт в вике формулирует их слегка иначе («контакт → уточнение → презентация приёма → 2 слота → запись → закрытие»). До реализации Спринта 5 — свериться с вики по конкретной первой целевой специальности (ЛОР?) и принять официальный список шагов.

Вопрос из v1 про границу «бот vs. оператор по хирургии» — исключён из архитектурных открытых: это продуктовое решение (как у клиники устроен контакт-центр), не архитектурное, и на код не влияет — хирургия просто эскалируется с reason=surgery, а дальше смежный разработчик маршрутизирует в нужную очередь.


Ориентир на следующие спринты

Логичный порядок (согласован с SPRINTS.md, Спринты 47):

  1. Разделить «один промпт» на несколько → сделано (Спринт 4).
  2. Добавить роутер → сделано (Спринт 4).
  3. Машина состояний + условия выхода → Спринт 5.
  4. Мульти-RAG → Спринт 6. С учётом v3: дизайн пересмотреть в сторону Варианта Б (подписка на разделы вики).
  5. Сценарии и экспорт → Спринт 7. С учётом v3: минимальный eval-набор сделать до Спринта 5, полный Спринт 7 реализовать позже.

Рекомендация v3 по Спринту 5: разделить на 5a (переходы между ветками: условия выхода, двойной прогон роутера, handoff_count, suspended_intent) и 5b (машина состояний внутри new_booking с guard'ами, структурированный ответ модели, валидатор переходов, wiki_sources на уровне шага). Объём работ неравномерный: 5a — несколько дней, 5b — неделя+. Если слить, велик шанс получить «наполовину сделано, протестировать нечем».


Разобранные примеры

Эти документы показывают архитектуру в работе на конкретных диалогах — реплика за репликой, с фиксацией того, что в этот момент происходит в thread_state, какое решение принял роутер, какой шаг машины состояний активен, что вернула модель в state_after и slots_updated.

  • 01_basic_booking.md — happy path записи к ЛОР-врачу. Базовый случай, в котором всё идёт по линейному скрипту: контакт → уточнение → презентация → слоты → запись → закрытие. Показывает, как заполняются слоты, как меняется current_step, что видит модель на каждой реплике.

  • 02_price_during_booking.md — пациент в середине записи спрашивает про цену. Один и тот же сценарий разобран в двух вариантах: мягкая вставка (без выхода из ветки) и жёсткий переход с возвратом (через suspended_intent + resumable_state). Лучший пример для понимания различий между этими двумя механизмами.

  • 03_child_patient_guard.md — запись ребёнка к врачу. Показывает срабатывание guard'а в шаге qualify, нелинейный путь по машине состояний, сбор данных законного представителя, юридические оговорки в шаге close.

  • 04_general_info_simple.md — простые информационные запросы (часы, адрес, проезд, контакты, документы, не-предоставляемые услуги). Самый дешёвый путь в системе: одна реплика, одна ветка general_info, один шаг answer, прямой ретривер → ответ → done. Логичная стартовая точка для запуска первой версии бота.


Changelog

v3 → 2026-04-26

Стиль:

  • Все технические понятия, которые встречаются в коде, в промптах или в названиях полей БД, оформлены по схеме «русское объяснение + английский термин в скобках» при первом упоминании в разделе. Это позволяет читать документ без догадок: что русское слово, что код, что переменная.
  • Добавлен раздел «Соглашение о терминах» в начале.

Ссылки на примеры:

  • В разделах 3, 4 и в новом разделе «Разобранные примеры» добавлены ссылки на четыре документа с пошаговыми разборами диалогов: 01_basic_booking.md, 02_price_during_booking.md, 03_child_patient_guard.md, 04_general_info_simple.md (последний — для простых одношаговых запросов общей информации, добавлен 2026-04-26 как стартовая точка для запуска).

Содержательно:

  • Без изменений. Все архитектурные решения, открытые вопросы, рекомендации по спринтам — те же, что в v2.

v2 → 2026-04-24

Добавлено:

  • Раздел 3.2: guards внутри ветки new_booking (ребёнок, Ворончихина, сурдолог) — из анализа скрипта записи в вике.
  • Раздел 3.3: структурированный выход модели {reply, state_after, slots_updated} и валидатор переходов в коде.
  • Раздел 3.4: wiki_sources на уровне шага, а не только ветки.
  • Раздел 4.2: мягкая вставка (soft-insertion) для боковых вопросов без выхода из ветки.
  • Раздел 4.3: handoff_count с капом и автоматическим уходом в escalate_human с reason=routing_loop.
  • Раздел 4.4: suspended_intent + resumable_step + resumable_slots для возврата в исходную ветку после detour'а.
  • Раздел 6 (новый): две схемы мульти-RAG. Рекомендация — Вариант Б (одна коллекция + подписка ветки на разделы вики через wiki_sources).
  • Раздел 7 (новый): eval-набор (20–30 фраз на ветку + handoff-сценарии) нужен до Спринта 5, а не в Спринте 7.
  • Рекомендация разделить Спринт 5 на 5a/5b.

Исправлено:

  • Убрана «Хирургия» как отдельная ветка (была в v1). Актуальная модель (решение разворота 2026-04-23) — одна ветка escalate_human с полем reason: acute_pain | surgery | angry | explicit_request | routing_loop.
  • Пример условия выхода переписан с [INTENT_CHANGE: surgery] на [INTENT_CHANGE: escalate_human] + reason=surgery.
  • Список веток в разделе 1 приведён к шести (как в сиде Спринта 4).
  • Открытый вопрос #4 из v1 (граница бота и оператора по хирургии) исключён из архитектурных — это продуктовый вопрос, на код не влияет.