# Пример 04 v2 · Простые информационные запросы (general_info) > **Версия v2 · 2026-04-27.** Переписано под реальный код. Ветка `general_info` **не имеет ни машины состояний, ни своих слотов** — это обычный отвечающий промпт (`prompts/intents/general_info.md`). После её отработки `current_step_code` остаётся `null`, `slots` — пустыми. Никаких слотов `info_topic`, `branch_mention`, `needs_followup` из v1 в коде нет. Список изменений — внизу в Changelog. > > Связано с [`../architecture/GRAPH_ARCHITECTURE_v4.md`](../architecture/GRAPH_ARCHITECTURE_v4.md), разделы 1, 2. Демонстрирует **простейший случай**: одна реплика → маршрутизатор → ветка `general_info` → ретривер → ответ. Никаких шагов, никаких слотов, никаких защитных условий. ## О чём этот пример Это сборник коротких самостоятельных диалогов по одному типу запросов — общая информация (часы, адрес, проезд, телефоны, что брать с собой, чего клиника не делает). У всех у них одна и та же траектория: 1. **Маршрутизатор** классифицирует реплику как `general_info`. 2. Активируется ветка `general_info`. **Без шага** (`current_step_code = null`). 3. **Ретривер** достаёт чанки из общей коллекции `operators_wiki`. Per-intent фильтра в коде сейчас нет — это запланированный Спринт 7 (мульти-RAG). Сейчас все ветки берут топ-K по всей вики. 4. Модель формирует ответ строго по найденным чанкам. 5. `thread_state` после хода: `current_intent_code: "general_info"`, `current_step_code: null`, `slots: {}`. Ничего не добавляется. Если пациент следом задаст вопрос про запись («а тогда запишите меня…»), маршрутизатор увидит смену темы и переключит на `new_booking` (это `hard handoff`, `handoff_count` инкрементируется). У `general_info` нет своей логики «кросс-села» — это решает маршрутизатор. ## Начальное состояние Перед каждым диалогом ниже — пустой `thread_state` (свежий тред): ```json { "thread_id": 9XXX, "current_intent_code": null, "current_step": 0, "current_step_code": null, "slots": {}, "handoff_count": 0, "soft_insertion_count": 0, "suspended_intent": null, "resumable_step_code": null, "resumable_slots": null } ``` После ответа `current_intent_code = "general_info"`, остальные поля — без изменений. --- ## A · Часы работы ### A.1 · «Во сколько вы открываетесь?» **Решение маршрутизатора:** `general_info`. **Активная ветка / шаг:** `general_info` / *(нет шага)*. **Ретривер:** общая коллекция `operators_wiki`, top-K чанков. Если в загруженной вике есть документ с расписанием — он попадёт в контекст; если нет — модель ответит на основании промпта `general_info.md`, опираясь только на то, что было в системном описании. **Ответ модели** (без `STATE_JSON:` — у этой ветки нет машины состояний): ``` Здравствуйте! Клиника на Клары Цеткин, 9 работает с 8:00 до 21:00 в будни и с 9:00 до 18:00 в выходные. ``` **`thread_state` после хода:** ```json { "current_intent_code": "general_info", "current_step": 0, "current_step_code": null, "slots": {}, "handoff_count": 0, "soft_insertion_count": 0 } ``` ### A.2 · «Завтра праздник, вы работаете?» **Решение маршрутизатора:** `general_info`. **Поведение модели:** в промпте `general_info.md` явно прописано опираться **только** на выдержки из базы. Если про конкретный праздничный режим в вике ничего нет, модель должна не выдумывать, а честно сказать «уточню у администратора и сообщу». **Ответ модели:** ``` В обычные дни мы работаем с 8:00 до 21:00. Праздничный режим уточню у администратора — он свяжется с вами и подтвердит. ``` `thread_state` после хода — то же, что в A.1. > **Замечание для eval-набора.** В v1 этого примера предлагался флаг `needs_followup: true` и confidence threshold (если score лучшего чанка < 0.50, отвечать шаблоном). В коде этого механизма сейчас **нет**: ретривер просто возвращает top-K, без проверки relevance, и модель опирается на инструкцию промпта «не выдумывай — скажи, что уточнишь». Полноценный confidence-фильтр — в идеях на потом в `SPRINTS.md`. --- ## B · Адрес и проезд ### B.1 · «Где вы находитесь?» **Решение маршрутизатора:** `general_info`. **Ответ модели:** ``` У нас два адреса в Перми. Подсказать, какой ближе к вам? ``` (Точные адреса — из вики. Если их там нет, модель скажет «уточню у администратора».) ### B.2 · «На метро как до вас доехать?» **Решение маршрутизатора:** `general_info`. **Поведение модели:** вопрос содержит **ложное допущение** (в Перми метро нет). Промпт `general_info.md` обязывает модель опираться только на базу. Если в базе нет упоминания метро (а его и не будет), модель не должна сочинять станции — она мягко поправит и предложит реальные альтернативы из вики (автобус, троллейбус). **Ответ модели:** ``` В Перми метро нет, поэтому удобнее всего на автобусе или такси. Подскажу маршруты, если уточните, откуда поедете. ``` Это **важный пограничный кейс для eval'а**: модель не должна изобретать «станцию метро» только потому, что её об этом спросили. --- ## C · Услуги, которых клиника не делает ### C.1 · «У вас можно сделать бронхоскопию?» **Решение маршрутизатора:** `general_info`. Это вопрос «делаете ли вы такое в принципе», а не запись и не цена. Промпт маршрутизатора относит подобные формулировки к `general_info`, чтобы не запускать механизм записи на услугу, которой нет. **Поведение модели:** в вики (если она правильно ведётся) есть раздел «процедуры, которые не проводятся» — оттуда и должен быть ответ. Если этого раздела нет, модель должна ответить «уточню», а не «да, конечно, запишем». **Ответ модели:** ``` Бронхоскопию у нас не делают — её обычно проводят в специализированных пульмонологических центрах. Если беспокоит дыхание или кашель — могу записать на консультацию к терапевту, он подскажет, куда дальше. ``` Хвостовая фраза «могу записать на консультацию» — это **не cross-sell ветки**, а просто реплика модели. Если пациент скажет «да, запишите», следующая реплика уйдёт в `new_booking` через маршрутизатор (`hard handoff`). --- ## D · Переход в запись после справки ### D.1 · Реплика 1: «У вас по выходным открыто?» **Решение маршрутизатора:** `general_info`. **Ответ модели:** короткий ответ из вики. **`thread_state` после хода:** ```json { "current_intent_code": "general_info", "current_step_code": null, "slots": {} } ``` ### D.2 · Реплика 2: «Хорошо, тогда запишите меня к ЛОРу» **Решение маршрутизатора:** `new_booking`. Явный сигнал — пациент хочет записаться. **Шаги оркестратора:** 1. Активная ветка `general_info` ≠ решение маршрутизатора `new_booking`. И `general_info` **не пошаговая** — поэтому никакого «удержания в ветке» не делается, переключаемся прямо. 2. **`handoff_count: 0 → 1`** (это первое переключение в этом диалоге). 3. `current_intent_code` ← `new_booking`. Запускаем эту ветку с шага `intro` (если в треде уже была активна `general_info`, у `new_booking` начинаем с нуля; `suspended_intent` НЕ заполняется — `general_info` не пошаговая, восстанавливать нечего). 4. `slots` обнуляются (у `general_info` их и не было, у `new_booking` начинаем с пустого набора). **Ответ модели `new_booking`** (на шаге `intro`): ``` Да, конечно. Подскажите, как могу к вам обращаться? STATE_JSON: {"state_after": "intro", "slots_updated": {}} ``` **`thread_state` после хода:** ```json { "current_intent_code": "new_booking", "current_step": 0, "current_step_code": "intro", "slots": {}, "handoff_count": 1, "soft_insertion_count": 0 } ``` В Песочнице на этом ответе: - Бейдж **«активная ветка: new_booking»** + **«пошаговая»**. - Бейдж **«решение маршрутизатора: new_booking»** (зелёный). - Бейдж **«решение: переключили ветку»** (оранжевый `hard_handoff`). - Счётчик переключений — **«1 из 3»**. Дальше — как в `01_basic_booking_v2.md`. --- ## Что показал этот пример - **Одна реплика — один проход.** Ветка `general_info` не имеет машины состояний: `current_step_code` остаётся `null`. Это самый дешёвый путь в системе и логичная точка запуска первой версии бота. - **У `general_info` нет своих слотов.** В отличие от `new_booking`, эта ветка ничего не накапливает. Если пациент в одном треде задаст три информационных вопроса подряд — каждый пройдёт через маршрутизатор → ретривер → ответ, без какого-либо состояния между ними. - **Ретривер делает основную работу.** Все факты в ответе должны быть из чанков, не из памяти модели. Если чанков не хватило — модель уходит в шаблон «уточню у администратора», по инструкции из `general_info.md`. - **Пограничные кейсы.** Метро в Перми (которого нет — B.2), услуги, которых клиника не делает (C), праздничные дни без чанка (A.2) — именно на них модель ломается чаще всего и именно их полезно держать в eval-наборе с самого начала. - **Переход в запись — это hard handoff.** Когда после справки пациент говорит «запишите», происходит переключение ветки с инкрементом `handoff_count`. Никакого `suspended_intent` не сохраняется — у `general_info` нет состояния, восстанавливать нечего. ## Что важно проверять в eval-наборе на этом примере - Маршрутизатор **не уводит** информационные вопросы в `new_booking` или `price_question`. Граничный случай: «сколько у вас стоит» — это `price_question`, а «какие у вас услуги» — `general_info`. Границы должны быть чёткими. - Все факты в ответе находимы в одном из чанков, попавших в контекст. Хорошая метрика — `groundedness` (доля утверждений с прямым подтверждением в источниках). - При отсутствии релевантных чанков модель отвечает шаблоном «уточню у администратора», а не выдумывает. - На пограничных кейсах (метро в Перми, услуги, которых нет, праздничные дни без чанка) ответ не содержит ложных утверждений — критичный безопасный минимум для запуска. - Переход «справка → запись» инкрементит `handoff_count`. Тест: задать `general_info`-вопрос, потом «запишите меня» → проверить, что `handoff_count == 1`, `current_intent_code == "new_booking"`, `current_step_code == "intro"`. --- ## Changelog ### v2 → 2026-04-27 **Имена полей `thread_state`** приведены к реальной БД (как в `01_basic_booking_v2.md`). **Удалена несуществующая в коде машина состояний `general_info`:** - В v1 описывались шаги `answer` и `done` со слотами. Реально у `general_info` шагов нет — это обычный отвечающий промпт (`prompts/intents/general_info.md`). После хода `current_step_code` остаётся `null`. - Удалены вымышленные слоты `info_topic`, `branch_mention`, `needs_followup`, `dms_provider`. У ветки нет своих слотов вообще. **Удалена несуществующая логика confidence threshold для RAG:** - В v1 пример A.4 описывал «если score лучшего чанка < 0.50, отвечать шаблоном с `needs_followup: true`». Этого механизма в коде сейчас нет — ретривер не проверяет `relevance_score`. Поведение «не выдумывай — скажи, что уточнишь» обеспечивается **только инструкцией в промпте** `general_info.md`, не порогом. Confidence-фильтр и слот `needs_followup` остались как идея на потом в `SPRINTS.md`. **Удалены детальные блоки про per-intent RAG-фильтры по путям вики:** - В v1 показывались фильтры вида `doc_path STARTS WITH any('/wiki/hours/**')`. Этого в коде нет (используется общая коллекция `operators_wiki` без фильтра). Это запланированный Спринт 7 (мульти-RAG, вариант Б из v3). **Сокращён объём примеров:** - Было ~20 коротких диалогов разбито по 6 темам (часы, адрес, проезд, контакты, документы, услуги, которых нет). В v2 оставлены только пограничные кейсы и один сценарий перехода `general_info → new_booking`. Полный сборник можно собрать заново после прогона eval'а, когда станет понятно, какие именно граничные случаи нужны. **Добавлен раздел D · «Переход в запись после справки»** — показывает, как маршрутизатор переключает ветку с `general_info` на `new_booking`, и почему `suspended_intent` при этом **не** заполняется (нет состояния для восстановления). **Терминология:** «роутер» → «маршрутизатор», «soft cross-sell» → «реплика модели» (этот термин в v1 был мисслидингом — никакой ветки-логики там нет). **Содержательно** — то же: показать, что `general_info` это самый простой путь в системе, и пограничные кейсы важнее happy path.