Узнайте, как исправить главную проблему RAG-систем — ложные ответы. Четыре шага: реранжинг, порог релевантности, цитирование и гибридный поиск. Попробуйте прямо сейчас!
Вы когда-нибудь спрашивали RAG-систему о политике, которой нет в документах, и получали уверенный, но полностью вымышленный ответ? Это не баг, а архитектурная проблема: поиск Top-K гарантирует только «ближайшие» векторы, а не релевантные. В этой статье вы узнаете, как внедрить четыре ключевых улучшения, чтобы ваша RAG-система говорила «я не знаю» вместо того, чтобы галлюцинировать.
Векторный поиск быстр, но приблизителен. Он может выдать кусок про календарь праздников по запросу о сроке уведомления при увольнении, просто из-за случайной близости в пространстве эмбеддингов. Решение — двухэтапный подход.
Сначала дешёвый и широкий поиск (Top-20 через HNSW-индекс), затем дорогой и точный реранжинг (Top-5). Реранжер — это cross-encoder, который смотрит на запрос и каждый кандидат вместе, а не по отдельности.
# Устанавливаем библиотеку для Cohere Rerank
# pip install cohere
import cohere
co = cohere.Client('YOUR_API_KEY')
def rerank_chunks(query, candidates, top_n=5):
"""
candidates: список словарей с ключом 'chunk_text'
Возвращает отреранжированные куски с оценками
"""
response = co.rerank(
model='rerank-english-v3.0',
query=query,
documents=[c['chunk_text'] for c in candidates],
top_n=top_n
)
reranked = []
for result in response.results:
idx = result.index
reranked.append({
**candidates[idx],
'rerank_score': result.relevance_score
})
return reranked
# Пример использования:
# top20 = vector_search(query_embedding, limit=20)
# top5 = rerank_chunks(user_question, top20)
Почему именно 20, а не 5? Если истинно лучший кусок оказался на 8-м месте в приблизительном поиске, вы даёте реранжеру шанс его найти.
Реранжинг не решает проблему отсутствия данных. Если в базе нет ничего по теме, система всё равно вернёт Top-5 «лучших из того, что есть». Нужен минимальный порог.
RELEVANCE_THRESHOLD = 0.5 # настраивается под ваши данные
def retrieve_with_abstention(query):
top20 = vector_search(query, limit=20)
reranked = rerank_chunks(query, top20, 5)
best_score = reranked[0]['rerank_score'] if reranked else 0
if best_score < RELEVANCE_THRESHOLD:
return {'has_relevant_context': False, 'chunks': []}
return {'has_relevant_context': True, 'chunks': reranked}
def answer_question(query):
result = retrieve_with_abstention(query)
if not result['has_relevant_context']:
return "У меня нет информации об этом в доступных документах. Обратитесь в HR или уточните запрос."
# далее сборка промпта с цитатами
Как выбрать порог? Прогоните набор известных вопросов (должны быть отвечены) и заведомо неотвечаемых. Посмотрите распределение оценок реранжера и выберите значение на стыке групп.
Даже с хорошими кусками важно, как вы их передаёте LLM. Просто вставить контекст — плохо: ответ невозможно проверить. Используйте нумерованные источники.
def build_grounded_prompt(query, chunks):
context_block = '\n\n'.join(
f'[{i+1}] (Источник: {c["metadata"]["source_file"]}, {c["metadata"]["department"]})\n{c["chunk_text"]}'
for i, c in enumerate(chunks)
)
prompt = f'''Вы отвечаете на вопросы, используя ТОЛЬКО нумерованные источники ниже.
{context_block}
Инструкции:
- Отвечайте, используя ТОЛЬКО информацию из источников.
- После каждого утверждения указывайте номер источника в скобках, например [1].
- Если источники не дают полного ответа, скажите об этом — не додумывайте.
Вопрос: {query}
Ответ:'''
return prompt
Пример ответа LLM: «Срок уведомления составляет 30 дней с даты подачи заявления [1][2]. Для увольнения также требуется одобрение руководителя [3].»
Теперь каждый источник можно сделать кликабельной ссылкой на оригинальный документ.
Векторный поиск плохо справляется с точными идентификаторами: кодами ошибок, номерами заказов. Например, запрос «ERR_4521» не имеет семантического смысла для эмбеддинга. Решение — добавить лексический поиск (BM25).
from rank_bm25 import BM25Okapi
tokenized_corpus = [chunk['chunk_text'].split() for chunk in all_chunks]
bm25 = BM25Okapi(tokenized_corpus)
def hybrid_search(query, vector_hits, alpha=0.5):
"""
Комбинируем оценки векторного и лексического поиска.
alpha — вес векторного поиска (0..1).
"""
tokenized_query = query.split()
bm25_scores = bm25.get_scores(tokenized_query)
# Нормализуем обе оценки
combined = []
for i, chunk in enumerate(all_chunks):
vec_score = vector_hits.get(i, 0) # предполагаем, что vector_hits — dict {index: score}
lex_score = bm25_scores[i]
combined_score = alpha * vec_score + (1 - alpha) * lex_score
combined.append((i, combined_score))
combined.sort(key=lambda x: x[1], reverse=True)
return [all_chunks[idx] for idx, _ in combined[:20]]
Гибридный поиск особенно полезен для кодов ошибок, названий продуктов и других точных строк.
Эти четыре шага превратят вашу RAG-систему из «уверенного лжеца» в надёжного помощника, который либо даёт проверяемый ответ, либо честно говорит, что не знает.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →