RAG: как передать модели свои данные
Знания LLM замёрзли на отсечке обучения; ваши документы меняются еженедельно. Retrieval-augmented generation (RAG) их оттаивает: в момент запроса достать релевантные куски ваших данных, вставить их в промпт и дать модели ответить по доказательствам с цитатами. Модель делает рассуждение; индекс делает память.
Пайплайн, один разбор
Возьмите мануал на 12,000 токенов и пройдите его от начала до конца:
- Чанки. Режьте сначала по заголовкам (секции выживают целыми), затем перережьте любую секцию длиннее ~400 токенов, держа ~60 токенов перекрытия (10–15%). Разумные размеры чанков — 200–800 токенов. Результат: примерно 35 чанков.
- Эмбеддинг. Каждый чанк становится одним вектором — bge-m3 выдаёт 1,024 float на чанк. На CPU это занимает секунды на весь мануал.
- Индекс. Храните векторы + текст + метаданные (файл-источник, путь заголовков) в векторном хранилище.
- Ретривал. Эмбеддингьте вопрос, достаньте топ k = 5 чанков по косинусной близости (4–10 — здравый диапазон).
- Генерация. Соберите промпт: инструкции, 5 чанков с ID-тегами, вопрос.
Чанкование по смысловым границам бьёт фиксированные окна. Таблица, отрезанная от строки заголовка, или нумерованная процедура, разрезанная на шаге 4, — недоставаемый мусор, как бы хорош ни был эмбеддер.
Две добавки, окупающиеся сразу: храните путь заголовков (manual › §4 › Bolts) как метаданные, чтобы фильтровать и цитировать точно, и эмбеддьте запрос ровно как набрал пользователь — «очистительная» предобработка обычно делает ретривал хуже, а не лучше.
Эмбеддинги: мультиязычное требование
Эмбеддер решает, что значит «похоже». Для корейских и смешанных корейско-английских корпусов выбор узок — англо-моно-эмбеддеры молча проваливаются на корейских запросах, возвращая правдоподобные-но-неверные чанки:
| Модель | Размерности | Заметки |
|---|---|---|
| bge-m3 | 1024 | мультиязычная, сильный корейский — безопасный локальный дефолт |
| nomic-embed-text-v1.5 | 768 | крепкий английский, длинный контекст |
| all-MiniLM-L6-v2 | 384 | быстрая, англо-центричная, слаба на корейском |
Модели эмбеддингов крошечные (0.1–0.6B параметров); ядро CPU прожёвывает тысячи чанков — инференс на CPU. Эмбеддьте корпус один раз, запросы — в момент вопроса. Держите эмбеддер замороженным на индекс: смена модели посреди индекса делает векторы из разных пространств, и оценки близости становятся шумом.
Векторные хранилища
| Хранилище | Когда |
|---|---|
| sqlite-vec | один пользователь, до ~1M векторов, ноль ops — это файл |
| ChromaDB | локально-первое прототипирование с фильтрами по метаданным |
| Qdrant | продакшен: фильтрация, гибридный поиск, масштаб |
Все три работают локально. Начните с sqlite-vec; переезжайте, когда реально упрётесь в его стену, а не раньше.
Сборка промпта
Answer only from the excerpts below. Cite every claim as [n].
If the excerpts do not contain the answer, reply "not in the docs".
[1] (manual §4.2) Torque the M8 bolt to 12 Nm after lubrication...
[2] (manual §7.1) Do not re-use stretch bolts...
Question: what torque spec applies to the M8 bolt?
Цитаты позволяют пользователям проверять, а не доверять слепо. Выход «нет в доках» важен: без него модель выдумает ответ вместо провала формата.
Гибридный поиск и реранкинг
Чисто векторный поиск промахивается по точным строкам — коды ошибок, имена функций, номера деталей, — по которым пригвождает ключевой поиск. Гибрид = BM25 + векторы, слитые reciprocal rank fusion; это почти бесплатно и стабильно лучше любого по отдельности.
Ради качества добавьте реранкер: достаньте 20–50 кандидатов, переранжируйте до топ-5 кросс-энкодером (семейство bge-reranker — та же мультиязычная история, что bge-m3). Реранкер читает чанк и запрос вместе, чего не может одновекторный эмбеддер. Дешёвый рецепт, покрывающий большинство нужд: гибридно достать 25, переранжировать до 5.
Диагностика: промах ретривала или генерации?
Прежде что-то тюнить, ответьте на один вопрос: был ли доставлен правильный чанк?
- Напечатайте топ-k чанков для проваливающегося запроса, со счетами.
- Если чанк с ответом не в списке — проблема ретривала: чините чанкование, эмбеддинги или поднимайте k.
- Если он в списке, но ответ неверный — проблема генерации: модель побольше, промпт получше, реранк, чтобы вытолкнуть дистракторы.
Этот один тест экономит часы тюнинга не той половины пайплайна.
Режимы отказа
| Симптом | Причина | Фикс |
|---|---|---|
| Найдены верные документы, ответ неверный | дистракторы в топ-k | реранк; сузить k |
| Ответ есть в доках, но не всплывает | голод топ-k или плохие границы | поднять k; чанковать по заголовкам |
| Уверенные, но устаревшие ответы | индекс старше доков | переиндексировать при изменениях |
| Корейские запросы не находят английские доки | моноязычные эмбеддинги | bge-m3 |
| Цитаты указывают не на тот чанк | чанк потерял метаданные источника | хранить источник + заголовок на чанк |
Мерьте, а не вайбите
Держите золотой набор из 20–50 реальных вопросов с известными ответами. Оценивайте два числа раздельно: hit rate ретривала (правильный чанк в топ-k?) и корректность ответа. Отслеживайте их после каждого изменения размера чанка, эмбеддера или k. RAG-работа без золотого набора — астрология.
Когда лучше дообучить
Дообучение меняет поведение — формат, стиль, терминологию. Это плохой способ добавлять факты: модель не может цитировать то, на чём тюнилась, а свежие документы означают свежий обучающий прогон. Правило: факты → RAG, поведение → LoRA, и то и другое — только когда исчерпан промптинг: дообучение.
Локальные стеки, от меньшего
Open WebUI поставляется с загрузкой документов и встроенным гибридным ретривалом — начните там, и, возможно, никогда не напишете кода пайплайна. Честное ядро RAG — ~50 строк; напишите его раз, чтобы знать, что делают фреймворки:
chunks = chunk_by_header(read("manual.md"), max_tokens=400, overlap=60)
index = [(embed(c), c) for c in chunks] # делается один раз
def answer(question):
hits = topk_by_cosine(embed(question), index, k=5)
ctx = "\n\n".join("[{}] {}".format(i, c) for i, (v, c) in enumerate(hits))
prompt = TEMPLATE.replace("{ctx}", ctx).replace("{q}", question)
return chat(prompt) # маленькой локальной модели хватает
Тянитесь к LlamaIndex или LangChain, когда нужны права доступа, обновление по расписанию или каркасы оценки — не ради цикла выше. Для корейских документов конкретно мультиязычные эмбеддинги (bge-m3) не опциональны, а чанкование по заголовкам должно уважать структуру корейских документов (заголовки 장/절) так же, как оно уважает заголовки markdown ##.
Связанные разделы: дообучение · промптинг · структурированный вывод · инференс на CPU