Перейти к содержимому

Что делает ассистент

Ассистент отвечает на вопросы клиентской поддержки по русскоязычной базе знаний конкретного тенанта. Задача — не давать ответов про коды ошибок, гарантию или биллинг компании, для которых в базе нет подтверждающих пассажей: каждый ответ опирается на найденные фрагменты, и до десяти извлечённых утверждений перепроверяются по ним до того, как ответ покинет граф, — если включена проверка фактов и контекст поиска непустой.

Первый скриншот — чат-UI ассистента с живым ответом на E20 (тот же кейс, который разобран ниже в «Реальный вопрос, end-to-end»). Это статический UI-shell static/chat.html, отрендеренный через production-функцию addMessage на том самом JSON, что вернул реальный прогон графа /api/ask (sources + citations + badges Качество/Маршрут), снятый на изолированном демо-тенанте, чтобы не задеть корпус владельца. Layout, citation-pill, disclosure «Источники» — реальный DOM, заполненный этим живым ответом. Остальные три скриншота — с самого docs-хаба: лэндинг, секция «Measured baseline» и автогенерируемая схема LangGraph.

Чат-UI RAG Support Assistant: вопрос «Как исправить E20?» от пользователя и трёхшаговый ответ ассистента с citation-pill [1], бейджами Качество: 95 и Маршрут: auto, развёрнутым блоком Источники со ссылкой на errors_e10_e30.md.
Чат-UI — E20 walkthrough через production-функцию addMessage на живом ответе /api/ask (external-mistral, снят 2026-06-16; citation, бейджи, sources disclosure — реальный DOM).
Лэндинг docs-хаба RAG Support Assistant: заголовок, intro-абзац, кнопки See the example / Reproduce E20 / GitHub и начало карточек Proof points.
Лэндинг — заголовок, три кнопки навигации, секция Proof points.
Страница оценки — секция Measured baseline с таблицей агрегатов (faithfulness 0.042, answer_relevancy 0.549, context_precision 0.190, context_recall 0.250) и per-category разрезом.
Страница оценки — измеренные агрегаты + per-category числа из benchmark_offline_with_docs.py.
Схема LangGraph state-машины inline: classify_complexity, transform_query, retrieve, grade_docs, generate, verify_facts, evaluate, route_or_retry с ветками suggest_questions / rewrite_query / handle_error.
Архитектура / LangGraph — flowchart Mermaid, сгенерированный из agent/graph.py.

В репозитории лежит набор из 12 фикстурных вопросов evaluation/test_cases.json по шести темам: коды ошибок, сброс пароля, гарантия, установка, биллинг, общее. Демо-загрузчик (demo/seed_docs.py) кладёт исходные файлы в каталог для последующей индексации обычным каналом загрузки в базу знаний; сам загрузчик векторный индекс не строит. Ниже один из этих вопросов прогнан через полный конвейер LangGraph на этом корпусе после индексации.

  • Демо-корпус: гарантия, возвраты и ошибки E10-E30 (errors_e10_e30.md).

Как исправить E20?

Конвейер — это конечный автомат LangGraph. Он живёт в agent/graph.py; полный flowchart со всеми узлами и условными переходами генерируется автоматически на странице LangGraph state machine. Разбор ниже называет каждый узел в том порядке, в котором их реально проходит граф.

  1. Retrieve 1-3: classify_complexity — классифицирует вопрос как simple, complex или unknown. Метка управляет выбором модели (быстрая для простых, сильная для сложных), а не путём через граф: любой вопрос дальше уходит в transform_query.

  2. Retrieve 1-3: transform_query — переписывает вопрос в одну строку search_query. Если есть история переписки, уточняющие вопросы сначала переписываются в самодостаточный запрос. Если включён HyDE (hypothetical-document embedding — модель пишет правдоподобный ответ-черновик, который потом эмбеддится и идёт как пробный запрос для поиска), сгенерированный гипотетический ответ сохраняется как hyde_query, и при удачном HyDE узел retrieve берёт его вместо search_query.

  3. Retrieve 1-3: retrieve — гибридный поиск по коллекции ChromaDB, выделенной под тенанта. Изоляция тенантов сделана через разные коллекции: tenant_id берётся из JWT на входе в API и используется для выбора отдельной Chroma-коллекции ({prefix}_{sanitize(tenant_id)} — см. vectordb/manager.py). Санитайзер приводит запрещённые в имени коллекции символы к _ и обрезает строку по лимиту 63 символа, так что изоляция держится, пока сырые tenant_id остаются различимыми после нормализации. Запросы к Postgres фильтруются по tenant_id на уровне ORM.

  4. Generate 4-5: grade_docs (Corrective RAG — модель сама пере-оценивает каждый найденный фрагмент по схеме YES/NO на фактическую релевантность, а не доверяет ранжированию по векторному сходству) — фрагменты с меткой NO отбрасываются до генерации, при этом топ-1 векторное совпадение сохраняется как запасной вариант, если хоть что-то ещё прошло. Это формирует то, что увидит generate, но само по себе повтор не запускает — решение о повторе принимает route_or_retry уже по оценкам само-проверки.

  5. Generate 4-5: generate — QA-промпт получает только отграждированные фрагменты плюс вопрос, каждый фрагмент подаётся в виде [Документ N | source=<filename>]. Модель выбирается активным профилем маршрутизации (по умолчанию gracekelly-primary, запасной путь на Ollama при сбое провайдера — см. Provider routing).

  6. Verify & Decide 6-8: verify_facts — когда проверка фактов включена и контекст поиска непустой, из черновика ответа извлекаются до десяти утверждений, и каждое перепроверяется против найденных фрагментов. Каждое проверенное утверждение помечается supported / unsupported в трейсе, а в factuality_score пишется процент подтверждённых утверждений; на пропущенных путях / без утверждений пишется 100. Эта разметка независима от само-оценки следующего шага.

  7. Verify & Decide 6-8: evaluate — запускает само-оценку моделью против контекста поиска и выдаёт quality_score (1–100) и relevance_score (выводится как quality_score / 100, округлённый до трёх знаков — обычно 0.01–1.0, потому что quality_score зажат в 1–100). Эти оценки читает дальше route_or_retry.

  8. Verify & Decide 6-8: route_or_retry — выставляет вердикт из двух оценок и оставшегося бюджета итераций. Для auto должны пройти ОБА порога:

    • auto (quality ≥ min_quality и relevance ≥ min_relevance, по умолчанию 80 и 0.8) → suggest_questionslog
    • retry (любой из порогов не пройден, итерации ещё есть) → rewrite_queryretrieve → … (Self-RAG — цикл, который переписывает запрос и снова ищет, когда само-оценка слишком низкая; ограничен max_iterations)
    • human (любой из порогов не пройден, итерации исчерпаны) → log напрямую, без подсказок
    • error (состояние несёт ошибку из более раннего узла) → handle_error → END
  9. Finalize 9-10: suggest_questions — генерирует три уточняющих вопроса из контекста ответа, чтобы засеять следующий шаг диалога. Запускается только при вердикте auto.

  10. Finalize 9-10: log — хранилище трейсов сохраняет порядок узлов, снимки состояния, ссылки на найденные документы и цитаты, поставщика и модель, расход токенов и оценку стоимости. Длительность каждого узла и интервалы LLM-вызовов уносят Langfuse и OpenTelemetry, когда соответствующие экспортёры настроены.

QA-промпт подаёт каждый найденный фрагмент с тегом source=<filename> внутренне (см. agent/prompts.py) и просит модель ставить нумерованную цитату [N], когда опирается на конкретный факт. Имена источников, на которые эти цитаты ссылаются, возвращаются отдельно в полях sources и citations JSON-ответа /api/ask. Ответ с опорой на источники для комплектного кейса E20 выглядит примерно так:

Ошибка E20 связана с проблемой слива воды. Возможные причины:
засорённый сливной фильтр, перегиб сливного шланга или неисправность
сливного насоса [1].
Возможно, вас также интересует:
• Что делать, если фильтр чистый, а ошибка остаётся?
• Где найти инструкцию по разборке сливного узла?
• Какие коды ошибок ещё связаны с водой?
{
"sources": [
{ "source": "errors_e10_e30.md", "page_content": "E20 — проблема со сливом воды …" }
],
"citations": [
{ "index": 1, "doc_id": "errors_e10_e30.md", "title": "errors_e10_e30.md", "excerpt": "клапан слива / фильтр …" }
]
}

Точные значения полей меняются от запуска к запуску, но форма закреплена: sources — список объектов {source, page_content}, citations — список объектов {index, doc_id, title, excerpt} — см. Pydantic-модели AskResponse, SourceInfo и Citation в api/routers/conversation.py. Тело ответа ограничено тем, что реально написано в errors_e10_e30.md про E20 (засорённый фильтр, перегиб сливного шланга, неисправность сливного насоса).

expected_keywords этого кейса (ошибка, проверьте, устройство) НЕ сверяются с текстом ответа — benchmark_runner подаёт их в context_recall, которое проверяет, появляются ли ключевые слова в контексте, найденном на этапе retrieve. Поле expected_answer потребляется отдельно как фикстура для офлайн-метрик качества ответа.

Любой вопрос проходит один и тот же скелет: classify → transform → retrieve → grade → generate → verify → evaluate → route_or_retry. Различия между темами — в том, какие оценки обычно выдаёт evaluate, и какой вердикт из-за этого выберет route_or_retry. Таблица ниже называет, что чаще всего может запустить повтор по теме; это качественная оценка по комплектному набору фикстур, не измеренное число.

КатегорияПримерЧто может запустить повтор
error_codesЧто означает ошибка E401?Вопрос про код, которого нет в индексе (E401 в наборе фикстур есть, но в errors_e10_e30.md лежат только E10/E20/E25/E30) — поиск возвращает нерелевантные фрагменты, evaluate падает, route_or_retry переписывает запрос. Разбор выше (E20) в базе есть и приземляется чисто.
reset_passwordКак сбросить пароль?Короткий фактический; в демо-базе нет документа про сброс пароля, поэтому поиск вернёт пусто или нерелевантное, и evaluate упадёт.
warrantyСколько длится гарантия?Короткий фактический; документ про гарантию в демо-базе есть, поиск приземляется чисто.
installationКак установить приложение?В демо-базе нет документа про установку — поиск вернёт пусто или нерелевантное, evaluate упадёт, route_or_retry уйдёт в retry или human.
billingКак отменить подписку?В демо-базе нет документа про биллинг — тот же путь, что у installation.
generalКак связаться с поддержкой?Короткий фактический; если ответ выводится из комплектных документов — evaluate пройдёт, иначе тот же путь с пустым результатом поиска.

Ответы с опорой на источники

Из финального ответа извлекается до десяти утверждений; каждое сверяется с найденными фрагментами в verify_facts, который ставит каждому supported / unsupported и пишет factuality_score как процент подтверждённых. evaluate отдельно прогоняет само-оценку против найденного контекста, и решение «вернуть ответ или сделать повтор» принимает route_or_retry именно по этой оценке, а не по меткам verify_facts.

Самокорректирующийся поиск

Corrective RAG (grade_docs) выкидывает нерелевантные фрагменты до генерации. Цикл Self-RAG (route_or_retryrewrite_queryretrieve → …) запускается оценками evaluate, а не количеством фрагментов, и ограничен max_iterations.

Мульти-тенант по построению

tenant_id берётся из JWT на входе в API и адресует каждый запрос к векторной базе в выделенную под тенанта коллекцию ChromaDB ({prefix}_{sanitize(tenant_id)}). Запросы к Postgres фильтруются по tenant_id на уровне ORM. Тенанты попадают в разные коллекции, пока их сырые id остаются различимыми после санитизации и обрезки по 63-символьному лимиту имени коллекции в Chroma.

Прозрачность от начала до конца

Хранилище трейсов пишет порядок узлов, снимки состояния, ссылки на найденные документы, поставщика и модель, расход токенов и стоимость — любой ответ из прода можно воспроизвести офлайн. Длительность LLM-вызовов и спаны несут Langfuse и OpenTelemetry, когда настроены.

Окно терминала
docker compose up -d # postgres, redis, chroma, api
python -m demo.seed_docs # пишет файлы демо-базы в demo/docs/
# (потом надо отдельно проиндексировать
# через ваш обычный канал загрузки в
# базу знаний — seed_docs только
# создаёт исходники)
curl -X POST http://localhost:8000/api/ask \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $JWT" \
-d '{"question": "Как исправить E20?"}'
  • LangGraph state machine — автогенерируемый flowchart всех упомянутых выше узлов.
  • Обзор архитектуры — жизненный цикл запроса, модули, хранилища, сквозные аспекты.
  • Provider routing — какая модель отвечает в каком профиле и за сколько.
  • API routes catalog — поверхность, к которой подключается клиент.