Skip to content

Repository files navigation

alfa-rag-demo

RAG по базе знаний службы поддержки. На вопрос пользователя система находит релевантные страницы справки, а затем решает, что с ними делать: ответить по найденному контексту, задать уточняющий вопрос или признать, что ответа в базе нет. Вырос из задачи хакатона Alfa Hack и переработан с нуля самостоятельно. Работает полностью локально: эмбеддинги через sentence-transformers, поиск через FAISS, генерация через Ollama.

Пайплайн от предметной области не зависит и запускается на любом csv с колонками web_id, title, url, text — см. «Свои данные». К домену привязан только слой guardrails.py: это словарь русских маркеров банковской поддержки, на других данных его нужно переписывать.

Пайплайн

Индексация: текст страницы склеивается с её заголовком, пробелы нормализуются, результат режется RecursiveCharacterTextSplitter на куски по 900 символов с перекрытием 150 и кодируется в 384-мерные векторы.

запрос
  ├─> dense retrieval по чанкам (multilingual-e5-small + FAISS IndexFlatIP)
  ├─> агрегация чанков в документы (max score на web_id)
  ├─> [опционально] CrossEncoder-реранк
  ├─> guardrails: разметка запроса по признакам (intent, маркеры продукта,
  │                процесса, персональных данных, overlap с найденным)
  ├─> decision layer: правила по этим признакам -> ok / need_clarify / no_answer
  └─> ветвление:
        ok            -> генерация ответа строго по найденным источникам (Ollama)
        need_clarify  -> уточняющий вопрос: ближайший пример из размеченного
                         банка уточнений, при необходимости переформулированный LLM
        no_answer     -> отказ с предложением переформулировать

Decision layer нужен, чтобы не отвечать любой ценой. Обычный RAG всегда что-то генерирует, а на клиентских обращениях это даёт уверенные ответы на вопросы, которые без уточнения ответа не имеют вовсе — вроде «у меня там оставались деньги на счёте». Отсюда три класса вместо простого порога по score.

Результаты

Данные хакатона: 1937 страниц справки, 18 965 чанков по 900 символов с перекрытием 150. Разметка — 150 обращений: 41 ok, 105 need_clarify, 4 no_answer.

Retrieval

Считается на 41 запросе с меткой ok, метрика на уровне документов. Релевантные документы выбирались из топ-20 этой же системы, поэтому числа отражают качество ранжирования внутри найденного, а не полноту поиска (см. «Ограничения»). В скобках — 95% доверительный интервал, бутстрап по запросам.

режим recall@1 recall@5 MRR@10
полный пайплайн, dense + guardrails 0.610 [0.463, 0.756] 0.878 [0.780, 0.976] 0.740 [0.634, 0.840]
чистый dense, без guardrails 0.585 [0.439, 0.732] 0.878 [0.780, 0.976] 0.725 [0.620, 0.825]
dense + CrossEncoder-реранк 0.463 0.805

Разница между первыми двумя строками — фильтрация служебных страниц в guardrails: капча, формы входа, документы без заголовка. На выборке в 41 запрос она укладывается в доверительный интервал, поэтому как прибавка не заявляется.

CrossEncoder-реранк выключен по умолчанию (USE_RERANK=0): на этих данных он снижает recall@1 с 0.585 до 0.463. Подъём страницы контактов по CALLCENTER_PIN_WEB_ID выключен там же — на метрики он не влияет.

Перебирались размер чанка, место заголовка, способ агрегации чанков в документ, четыре других энкодера, BM25 и гибрид с ним, два реранкера. 900 символов оптимальны, заголовок в начале документа даёт +0.15 к recall@1, повторять его в каждом чанке вредно. Всё, что меняет модель поиска, этой разметкой корректно не измеряется: от 28% до 59% выдачи таких систем в разметку не попадала.

Decision layer

Классы несбалансированы: 109 обращений из 150 требуют уточнения, поэтому основная метрика — macro-F1. Класс no_answer слит с need_clarify: в разметке его 4 примера и он не предсказывается ни разу.

macro-F1 accuracy правильных ok
правила 0.742 0.773 32 из 41
константа «всегда need_clarify» 0.421 0.727 0 из 41
класс precision recall f1 support
ok 0.561 0.780 0.653 41
need_clarify 0.903 0.771 0.832 109

По accuracy разрыв с константой копеечный, но константа переспрашивает всегда и не отвечает ни на один вопрос из 41. По macro-F1 разрыв двукратный.

Разметка проверялась вслепую на 74 обращениях, из них 21 расхождение пересмотрено с выдачей ретривера перед глазами: исходная метка подтвердилась в 17 случаях из 21. Итоговое согласие с разметкой около 95%.

Абляция признаков, разбор расхождений и трёхклассовый вариант — в docs/experiments.md.

Числа воспроизводятся, если положить в data/ файлы хакатона websites.csv и gold_labels.csv:

python scripts/make_chunks_parquet.py --inp data/websites.csv --out data/chunks_websites.parquet
python scripts/evaluate.py --chunks chunks_websites.parquet --gold data/gold_labels.csv --with-rerank

Быстрый старт

Данные хакатона в репозиторий не выкладываются. Вместо них есть синтетический демо-корпус из 16 документов вымышленного банка — на нём проект запускается сразу после клонирования, но метрики выше на нём не воспроизводятся, он нужен только для проверки работоспособности.

git clone https://github.com/belkovskyy/alfa-rag-demo.git
cd alfa-rag-demo

python -m venv .venv
.venv\Scripts\activate        # Linux/macOS: source .venv/bin/activate

pip install -e ".[app,dev]"
python scripts/make_demo_corpus.py
python scripts/run_demo.py "Где посмотреть все мои счета?"

Первый запуск скачивает intfloat/multilingual-e5-small (~470 МБ) и строит FAISS-индекс; дальше индекс берётся из кэша в папке с данными.

Веб-интерфейс — python -m streamlit run apps/streamlit_app.py, тесты — pytest.

Генерация ответов опциональна и требует локальной Ollama с любой моделью (ollama pull qwen2.5:3b). Без неё пайплайн отрабатывает полностью, но вместо сгенерированного ответа печатается список найденных источников.

Свои данные

Нужен csv с колонками web_id, title, url, text:

python scripts/make_chunks_parquet.py --inp data/mydocs.csv --out data/chunks.parquet
python scripts/run_demo.py --chunks chunks.parquet "запрос"

Для замера метрик нужна ещё разметка q_id, query, label_status, gold_web_ids, clarify_note, где label_status — одно из ok / need_clarify / no_answer. Образец формата — data/gold_labels_sample.csv.

Конфигурация

Через переменные окружения: USE_RERANK, RERANK_MODEL, E5_MODEL, OLLAMA_URL, OLLAMA_MODEL, K_CHUNKS, K_DOCS, DATA_DIR, CHUNKS_PATH, GOLD_PATH, CALLCENTER_PIN_WEB_ID.

Последняя задаёт документ, который поднимается в начало выдачи, когда запрос похож на просьбу позвонить в поддержку. По умолчанию выключено; на данных хакатона это была страница с контактами, CALLCENTER_PIN_WEB_ID=862.

Структура

src/alfa_rag/
  retrieval.py    dense-поиск по чанкам, агрегация в документы, кэш индекса
  rerank.py       CrossEncoder-реранк (выключен по умолчанию)
  guardrails.py   признаки запроса и найденной выдачи для decision layer
  decision.py     правила ok/need_clarify/no_answer и опциональный ML-gate
  clarify.py      банк уточнений и подбор ближайших примеров
  llm.py          генерация через Ollama, фолбэки при её отсутствии
  service.py      сборка пайплайна
  eval.py         метрики retrieval и прогон по размеченному набору
  labeling.py     интерактивная разметка gold-набора
scripts/
  make_demo_corpus.py    синтетический корпус для запуска без данных хакатона
  make_chunks_parquet.py нарезка своего csv на чанки
  run_demo.py            один запрос через весь пайплайн
  evaluate.py            метрики из раздела «Результаты»
  ablate_features.py     вклад каждого признака guardrails в решение
apps/streamlit_app.py    веб-интерфейс
notebooks/RAG.ipynb      исходный ноутбук хакатона
tests/                   тесты на guardrails, decision layer, чанкинг и утилиты
data/gold_labels_sample.csv  30 строк разметки как образец формата

Как читать метрики

Gold-набор размечался по выдаче системы: показывался топ-20, релевантные документы выбирались из него. Поэтому recall@1/3/5 меряют качество ранжирования внутри найденного, а не полноту поиска, а сравнивать между собой корректно только конфигурации с одной моделью (разбор — в docs/experiments.md). Выборка небольшая, 41 запрос для retrieval, поэтому доверительные интервалы приведены рядом с каждой метрикой.

Ретривер, схема ok/need_clarify/no_answer, банк уточнений и генерация по контексту переносятся на любую базу знаний; к домену привязан только словарь маркеров в guardrails.py.

Лицензия

MIT, см. LICENSE. Данные хакатона под лицензию не подпадают и в репозитории не распространяются.

About

RAG по базе знаний поддержки: FAISS, decision layer, доверительные интервалы и абляции

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages