10 промтов для RAG систем: индексация, поиск и генерация без боли

Введение

RAG (Retrieval-Augmented Generation) — это не просто модный термин, а рабочий инструмент для создания точных ответов на основе собственных данных. Однако на практике разработчики сталкиваются с тремя узкими местами: как правильно разбить документы на чанки, как улучшить поиск с помощью гибридных методов и как заставить генератор не выдумывать факты. Я собрал 10 проверенных промтов, которые помогают решать эти задачи. Каждый промт — это готовый шаблон с примером кода и пояснением. Вы можете адаптировать их под свои проекты, будь то чат-бот для техподдержки или аналитическая система.

Подборка промтов

1. Чанкинг с перекрытием для контекстной связности

Проблема: При разбиении текста на фиксированные куски теряется смысл на стыках. Решение — использовать перекрытие (overlap). Этот промт задаёт стратегию чанкования с настраиваемым размером и перекрытием, чтобы модель не теряла контекст.

Пример использования: Вы обрабатываете техническую документацию. Без перекрытия предложения «Отключите питание» и «Затем нажмите кнопку Reset» могут попасть в разные чанки, и модель не поймёт логику.

Код:

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=100,
    length_function=len,
    separators=["\n\n", "\n", ".", " "]
)
chunks = text_splitter.split_text(document)

2. Эмбеддинги с нормализацией для косинусного сходства

Проблема: Векторные представления (эмбеддинги) имеют разную длину, что искажает косинусное сходство. Промт задаёт нормализацию векторов перед поиском.

Пример: При использовании модели all-MiniLM-L6-v2 без нормализации результаты ранжирования могут быть нестабильными. Нормализация фиксирует это.

Код:

from sentence_transformers import SentenceTransformer
import numpy as np

model = SentenceTransformer('all-MiniLM-L6-v2')
embeddings = model.encode(chunks)
normalized = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)

3. Гибридный поиск: BM25 + векторный поиск

Проблема: Векторный поиск плохо находит точные совпадения (например, артикулы товаров), а BM25 — семантически близкие фразы. Гибрид объединяет оба подхода.

Пример: Поиск «iPhone 15»: BM25 найдёт точное совпадение, а векторный — «смартфон Apple 2024 года». Результаты объединяются через взвешенную сумму.

Код:

from rank_bm25 import BM25Okapi
import numpy as np

bm25 = BM25Okapi([chunk.split() for chunk in chunks])
vector_results = vector_search(query, top_k=10)
bm25_results = bm25.get_top_n(query.split(), chunks, n=10)
# Комбинируем с весами 0.5 и 0.5
combined = combine_results(vector_results, bm25_results, alpha=0.5)

4. Переранжирование с помощью кросс-энкодера

Проблема: Векторный поиск возвращает много релевантных, но не точных результатов. Кросс-энкодер (например, cross-encoder/ms-marco-MiniLM-L-6-v2) переранжирует топ-N.

Пример: После первого этапа у вас 20 чанков. Кросс-энкодер оценивает пару «запрос-чанк» и выдаёт скор, что повышает точность с 70% до 90%.

Код:

from sentence_transformers import CrossEncoder

model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
scores = model.predict([(query, chunk) for chunk in top_chunks])
reranked = [chunk for _, chunk in sorted(zip(scores, top_chunks), reverse=True)]

5. Генерация с проверкой фактов (grounding)

Проблема: LLM может выдумывать факты (галлюцинации). Промт заставляет модель опираться только на переданные чанки и указывать источник.

Пример использования: В чат-боте по продукту модель пишет «Наш сервер поддерживает 1000 запросов в секунду», хотя в документации указано 500. Промт предотвращает это.

Код:

prompt = f"""Ответь на вопрос, используя ТОЛЬКО следующий контекст. Если контекст не содержит ответа, скажи 'Не знаю'. Укажи номер чанка-источника.
Контекст: {context}
Вопрос: {query}
Ответ:"""
response = llm.generate(prompt)

6. Динамический чанкинг по семантическим границам

Проблема: Фиксированный размер чанка не учитывает логику текста (например, начало и конец раздела). Используйте NLTK или spaCy для разбивки по предложениям.

Пример: Документ с параграфами: чанкинг по границам предложений сохраняет целостность абзацев.

Код:

import nltk
nltk.download('punkt')
sentences = nltk.sent_tokenize(document)
chunks = []
current = []
for sent in sentences:
    if len(' '.join(current)) + len(sent) > 500:
        chunks.append(' '.join(current))
        current = [sent]
    else:
        current.append(sent)

7. Фильтрация нерелевантных чанков по порогу сходства

Проблема: Векторный поиск возвращает чанки с низкой релевантностью. Установите порог (например, 0.7), чтобы отсечь шум.

Пример: При запросе «цветы» система может вернуть чанк про «садовые инструменты» с косинусным сходством 0.4. Порог удаляет его.

Код:

threshold = 0.7
filtered = [(chunk, score) for chunk, score in results if score >= threshold]

8. Аугментация запроса: переформулировка для улучшения поиска

Проблема: Пользовательский запрос короткий («цена»). LLM генерирует несколько вариантов (стоимость, тариф) и ищет по каждому.

Пример: Запрос «отзывы» -> варианты: «рецензии», «мнения пользователей», «оценки». Поиск становится полнее.

Код:

queries = llm.generate(f"Сгенерируй 5 синонимичных вариантов запроса '{query}':")
results = [vector_search(q) for q in queries]
aggregated = merge_results(results)

9. Хранение метаданных в векторной БД для фильтрации

Проблема: Поиск по всем документам без фильтрации по дате или категории замедляется и снижает точность. Добавьте метаданные (дата, тег) в индекс.

Пример: Вопрос «новости за июль 2026» — фильтр по метаданным даты ускоряет поиск в 10 раз.

Код:

# Пример для ChromaDB
collection.add(
    documents=chunks,
    metadatas=[{"date": "2026-07-01", "category": "tech"}],
    ids=["id1", "id2"]
)
results = collection.query(query_texts=[query], where={"date": {"$gte": "2026-07-01"}})

10. Оценка качества RAG через метрики (hit rate, MRR)

Проблема: Невозможно улучшить то, что не измеряешь. Промт вычисляет hit rate (доля запросов, где ответ найден в топ-1) и MRR (средняя обратная позиция).

Пример: На 100 запросах hit rate = 0.75 означает, что 75 ответов — в первом чанке. Это baseline для улучшения.

Код:

from sklearn.metrics import ndcg_score

hit_rate = sum(1 for q in queries if q['relevant_chunk'] in q['top_chunks'][:1]) / len(queries)
mrr = sum(1 / (q['top_chunks'].index(q['relevant_chunk']) + 1) for q in queries if q['relevant_chunk'] in q['top_chunks']) / len(queries)
print(f"Hit Rate: {hit_rate:.2f}, MRR: {mrr:.2f}")

Заключение

Эти 10 промтов — база для построения RAG-системы, которая работает стабильно. Начните с чанкинга и эмбеддингов, затем подключите гибридный поиск и переранжирование. Главное — тестируйте каждое изменение на своих данных. В следующей статье разберём, как автоматизировать оценку качества с помощью этих метрик. Если у вас есть вопросы — пишите в комментариях, обсудим.

← Все статьи

Комментарии