В базе знаний был точный ответ про ошибку E1047. Семантический поиск отдавал общую статью про авторизацию: смысл похож, кода ошибки в ней нет. Полнотекстовый поиск находил E1047 сразу. После объединения результатов нужный фрагмент стал первым. Так обычно и выясняется, что embeddings не заменяют поиск.
От документа до кандидатов
Чанк должен сохранять законченную мысль и адрес в исходнике. Резать каждые 500 токенов удобно только загрузчику. Для Markdown разумнее идти по заголовкам, затем делить слишком длинные секции с небольшим перекрытием. Таблицу, подпись и условия применения нельзя разнести по разным кускам.
from dataclasses import dataclass
@dataclass
class Chunk:
document_id: str
heading: str
text: str
ordinal: int
version: str
# tokenizer зависит от embedding-модели
def split_section(tokens, size=450, overlap=60):
step = size - overlap
for start in range(0, len(tokens), step):
yield tokens[start:start + size]Embedding должен строиться одной зафиксированной моделью. При смене размерности или модели создают новую колонку либо индекс и переиндексируют корпус. Смешивать в одном пространстве векторы разных моделей нельзя.
Hybrid search
Векторный поиск ловит перефразирование, лексический сохраняет номера договоров, имена методов и артикулы. Результаты удобно объединять Reciprocal Rank Fusion, которому не требуется сравнивать несопоставимые score:
def rrf(rankings: list[list[str]], k: int = 60) -> list[str]:
scores = {}
for ranking in rankings:
for rank, item_id in enumerate(ranking, start=1):
scores[item_id] = scores.get(item_id, 0) + 1 / (k + rank)
return sorted(scores, key=scores.get, reverse=True)После фильтров и fusion остаётся 20–50 кандидатов. Reranker оценивает пару «вопрос, фрагмент» точнее embedding-поиска и возвращает несколько источников в prompt. Это отдельная задержка, поэтому его не запускают на всём корпусе.
Failure modes
Слишком мелкие чанки теряют условия и исключения. Слишком крупные дают высокий шум и дорогой prompt. Большое overlap плодит дубликаты. ANN-индекс с агрессивными параметрами пропускает редкий точный ответ. Reranker меняет порядок, но не может вернуть документ, который retrieval не нашёл. Метаданные после обновления документа расходятся с текстом, если индексация не атомарна.
Проверять надо на именованных запросах: точный код, разговорная формулировка, вопрос с двумя условиями, документ без ответа. Среднее впечатление от пяти демонстраций ничего не говорит о recall.
Правило главы
Chunking задаёт единицу доказательства, hybrid search собирает кандидатов, reranker выбирает пригодные. Настраивать их следует по отдельным метрикам, сохраняя путь до исходного документа.
Практикум
Подготовьте 40 вопросов и отметьте релевантные секции. Сравните фиксированную нарезку с нарезкой по заголовкам, BM25 с vector search и их RRF. Посчитайте recall@20 до reranker и nDCG@5 после него. Разберите пять промахов вручную и меняйте только один компонент за итерацию.