Когда AI врет убедительно: почему обычный RAG - это лотерея
Вы когда-нибудь спрашивали у RAG-системы "Какая столица Франции?" и получали ответ "Лион, потому что в документах упоминался Лион 10 раз"? Я - да. И это не баг, а фича обычного RAG без гарда. LLM великолепно выдумывает, если контекст пуст или нерелевантен. Проблема в том, что модель не умеет молчать - она обязана дать ответ, даже если информации нет. В результате - уверенные галлюцинации, которые пользователь принимает за чистую монету.
В 2026 году, когда RAG-системы уже стали мейнстримом (читайте наш roadmap), галлюцинации остаются главной головной болью. Но решение есть: программная проверка источников на каждом этапе. Мы не полагаемся на "честность" LLM, а строим конвейер, который либо подтверждает наличие фактов, либо отказывается отвечать.
Стек, который не подведет: Gemma-4, BGE-M3 и pgvector в одном флаконе
Набор инструментов на июнь 2026 года выглядит так:
- Ollama - локальный рантайм для LLM, последняя версия 0.9.x. Идеально для тестов и production с GPU.
- Gemma-4 (8B и 26B) - модель от Google, доступная через Ollama. Превосходное качество в задачах RAG, хотя есть странности с кодом (подробности в нашем тесте).
- BGE-M3 (BAAI) - мультиязычная эмбеддинг-модель, поддерживает dense и sparse векторы. Актуальная версия - v1.2. Сравнение с конкурентами показало, что M3 выигрывает на русскоязычных данных.
- pgvector с HNSW-индексом - векторное расширение PostgreSQL. Версия 0.9.0+.
- bge-reranker (BAAI) - реранкер для второго прохода. Тяжелый, но точный.
- Программный гард - модуль проверки: пороги, цитирование, отказ.
💡 Зачем здесь реранкер? Первичный поиск по BGE-M3 дает 10-30 чанков. Реранкер пересчитывает релевантность с учетом запроса и каждого чанка - качество скачет с 70% до 95%. Экономия: не тащим весь контекст в LLM, а берем только топ-3-5.
Шаг за шагом: от документов к ответу с верификацией
1 Поднимаем инфраструктуру
Устанавливаем Ollama, пуллим модели и настраиваем PostgreSQL с pgvector.
# ОLLAMA
curl -fsSL https://ollama.com/install.sh | sh
ollama pull gemma4:8b # или gemma4:26b если есть 24+ GB VRAM
ollama pull bge-m3
ollama pull bge-reranker
# PostgreSQL + pgvector
docker run -d --name pgvector -e POSTGRES_PASSWORD=pass -p 5432:5432 pgvector/pgvector:0.9.0-pg16
2 Индексация документов
Разбиваем документы на чанки по 512 токенов с пересечением 64 токена. Каждый чанк превращаем в эмбеддинг через BGE-M3 и сохраняем в таблицу.
import ollama
import psycopg2
from sentence_transformers import SentenceTransformer
# Используем BGE-M3 через Ollama (или локально через sentence-transformers)
# Советую локально - быстрее.
model = SentenceTransformer('BAAI/bge-m3', device='cuda')
conn = psycopg2.connect(...)
cur = conn.cursor()
# Таблица
cur.execute("""
CREATE TABLE IF NOT EXISTS docs (
id SERIAL PRIMARY KEY,
chunk TEXT,
source TEXT,
embedding vector(1024)
);
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);
""")
chunks = split_document(text) # ваша функция
for chunk in chunks:
emb = model.encode(chunk)
cur.execute("INSERT INTO docs (chunk, source, embedding) VALUES (%s, %s, %s)",
(chunk, filename, emb.tolist()))
conn.commit()
3 Поиск + реранкинг
Пользователь задает вопрос. Получаем его эмбеддинг, делаем векторный поиск в pgvector, затем реранкинг через bge-reranker.
query = "Столица Франции - это Париж?"
q_emb = model.encode(query).tolist()
# Поиск 20 ближайших
cur.execute("""
SELECT chunk, source, 1 - (embedding <=> %s::vector) AS cosine_sim
FROM docs
ORDER BY embedding <=> %s::vector
LIMIT 20
""", (q_emb, q_emb))
candidates = cur.fetchall()
# Реранкинг
import ollama
rerank = ollama.rerank('bge-reranker', query=query, documents=[c[0] for c in candidates])
# Получаем отсортированные чанки с новыми скорами
reranked = sorted(rerank.results, key=lambda x: x.relevance_score, reverse=True)
top = [r.document for r in reranked[:5]]
4 Гард: проверка порога и отказ
Критический момент. Мы не отправляем в LLM пустой контекст. Если лучший релевантность-скор ниже 0.6 - системы отвечает "Извините, я не могу найти подтверждение этой информации в доступных источниках".
THRESHOLD = 0.6
if not top or reranked[0].relevance_score < THRESHOLD:
return {"answer": "Недостаточно данных для ответа.", "sources": []}
# Иначе формируем контекст
context = '\n---\n'.join(top)
5 Генерация ответа с цитированием
Промпт требует от Gemma-4 отвечать только на основе контекста и указывать номера источников. После генерации проверяем, что в ответе есть цитаты.
prompt = f"""Ты - ассистент, который отвечает только на основе предоставленных документов.
Если документа не хватает для ответа - напиши "Недостаточно информации".
Документы:
{context}
Вопрос: {query}
Ответ (с указанием номеров документов в квадратных скобках):"""
response = ollama.chat(model='gemma4:8b', messages=[{'role': 'user', 'content': prompt}])
answer = response['message']['content']
# Простая проверка: ответ содержит хотя бы одну ссылку вида [1]
import re
if not re.search(r'\[\d+\]', answer):
# Повторяем запрос с жесткой инструкцией или возвращаем отказ
return {"answer": "Не удалось сформировать ответ с цитатами.", "sources": []}
return {"answer": answer, "sources": [c[1] for c in top]}
Ловушки, в которые я вляпался (и вы тоже вляпаетесь)
- Слишком агрессивный порог. 0.6 может отсекать полезные чанки на специфических доменах. Лучше подбирать эмпирически: протестируйте на 100 запросах с известными ответами. Мы используем 0.55 для общих знаний и 0.7 для медицинских данных.
- Реранкер убивает latency. bge-reranker на CPU может занимать 2-3 секунды на 20 чанков. Совет: используйте GPU или ограничьте количество кандидатов до 5 и откажитесь от реранкера, если скорость критична. Но качество упадет.
- Gemma-4 игнорирует инструкции. Несмотря на явный промпт, модель иногда выдумывает источники. Методы аблитерации не помогают с игнорированием инструкций - здесь спасает только второй проход с LLM-as-judge. Мы добавляем проверку: отправляем ответ и контекст на другую LLM (например, llama3) и просим верифицировать, что все утверждения соответствуют документам.
- Модель "молчит" при недостатке данных. Если вы настроили отказ, пользователи увидят много "Не знаю". Готовьте UX: показывайте лучшие найденные фрагменты, даже если ответа нет.
Часто задаваемые вопросы (неочевидные)
Зачем BGE-M3, если есть эмбеддинги от OpenAI?
OpenAI эмбеддинги платные, а качество на русском у BGE-M3 выше - сравнение тут. К тому же вы контролируете данные.
Как быть с обновлением документов?
Добавляйте поле updated_at и при индексации удаляйте старые записи по source. Или используйте партиционирование по дате.
А что, если пользователь попросит "расскажи о себе"?
Хороший вопрос: такие запросы не пройдут проверку контекста - порог не будет достигнут, и система вежливо откажется. Это защита от промпт-инъекций.
Последний совет: не доверяйте LLM даже с гардом. Периодически запускайте автоматические тесты на наборе вопросов с эталонными ответами. Мы используем ту же Gemma-4 как judge, но с отдельным промптом. И да, визуализация эмбеддингов очень помогает понять, куда уходят ваши документы.
Построить надежную RAG-систему реально. Но только если вы перестанете надеяться на "магию" LLM и начнете проверять каждый шаг. Gemma-4, BGE-M3 и pgvector дают твердую основу. Гард от галлюцинаций - ваш последний рубеж. Не пропускайте врага.