RAG на практиці: чанкінг, ембеддинги та векторний пошук
Як навчити LLM відповідати за вашими документами: з чого складається RAG-конвеєр, як різати тексти на чанки і чому гібридний пошук майже завжди кращий за чисто векторний.
RAG (Retrieval-Augmented Generation) — це підхід, коли перед запитом до моделі ми знаходимо релевантні шматки документів і кладемо їх у контекст. Модель відповідає не «з пам'яті», а з наданих джерел. Так можна працювати з внутрішньою документацією, базою знань підтримки чи кодом — без донавчання моделі.
Конвеєр з висоти пташиного польоту
Індексація (раз на оновлення документів):
документи → очищення → чанки → ембеддинги → векторна база
Запит (щоразу):
питання → ембеддинг → пошук top-k чанків → (переранжування) → промпт з чанками → LLM → відповідь з посиланнямиЯкість RAG на 80% визначається пошуком. Якщо в контекст потрапили не ті чанки, найкраща модель не врятує.
Чанкінг: як різати документи
Ембеддинг стискає текст у вектор фіксованої довжини. Довгий шматок «розмивається» — у ньому змішано багато тем. Занадто короткий втрачає контекст. Типова відправна точка:
| Тип документів | Розмір чанка | Перекриття |
|---|---|---|
| Документація, статті | 300–800 токенів | 10–15% |
| FAQ, тікети підтримки | один запис = один чанк | — |
| Код | функція або клас | сигнатура батьківського класу як префікс |
ПорадаРіжте за структурою, а не за кількістю символів: заголовки, абзаци, функції. Додавайте до кожного чанка «хлібні крихти» — назву документа й шлях заголовків. Рядок
Документація › Деплой › Відкат релізусуттєво покращує пошук.
def chunk_markdown(doc_title: str, text: str, max_chars: int = 2500):
chunks, current, path = [], [], []
for line in text.splitlines():
if line.startswith("#"):
if current:
chunks.append(make_chunk(doc_title, path, current))
current = []
level = len(line) - len(line.lstrip("#"))
path = path[: level - 1] + [line.lstrip("# ").strip()]
current.append(line)
if sum(len(l) for l in current) > max_chars:
chunks.append(make_chunk(doc_title, path, current))
current = current[-3:] # невелике перекриття
if current:
chunks.append(make_chunk(doc_title, path, current))
return chunks
def make_chunk(title, path, lines):
header = " › ".join([title, *path])
return {"header": header, "text": header + "\n\n" + "\n".join(lines)}Ембеддинги та векторна база
Ембеддинг-модель перетворює текст на вектор (наприклад, 1024 числа). Схожі за змістом тексти мають близькі вектори — близькість зазвичай міряють косинусною відстанню.
Якщо у вас уже є PostgreSQL, окрема векторна база часто не потрібна — вистачить розширення pgvector:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE chunks (
id bigserial PRIMARY KEY,
doc_id bigint NOT NULL,
header text NOT NULL,
content text NOT NULL,
embedding vector(1024) NOT NULL
);
-- HNSW-індекс для швидкого наближеного пошуку за косинусною відстанню
CREATE INDEX ON chunks USING hnsw (embedding vector_cosine_ops);
-- top-5 найближчих чанків до вектора запиту
SELECT id, header, 1 - (embedding <=> $1) AS similarity
FROM chunks
ORDER BY embedding <=> $1
LIMIT 5;ВажливоДокументи й запити треба кодувати однією й тією самою ембеддинг-моделлю. Змінили модель — переіндексуйте все.
Гібридний пошук і переранжування
Векторний пошук добре ловить зміст («як відкотити реліз» ≈ «повернути попередню версію»), але погано — точні терміни: коди помилок, назви змінних, артикули. Повнотекстовий пошук (BM25, tsvector у PostgreSQL) — навпаки.
Найкращий результат зазвичай дає гібрид:
- Взяти top-20 з векторного пошуку і top-20 з повнотекстового.
- Об'єднати списки, наприклад, через Reciprocal Rank Fusion:
score = Σ 1 / (60 + rank). - Пропустити 20–40 кандидатів через reranker — модель, що оцінює пару «питання — чанк» точніше за ембеддинги.
- Залишити 5–8 найкращих для промпту.
Промпт для відповіді
Ти відповідаєш на питання за документацією компанії.
Використовуй ЛИШЕ фрагменти нижче. Якщо відповіді в них немає — так і скажи.
Після кожного твердження вказуй номер джерела в дужках, напр. [2].
<sources>
[1] Документація › Деплой › Відкат релізу
...текст чанка...
[2] ...
</sources>
Питання: {question}Як зрозуміти, що RAG працює
Без вимірювань будь-яка зміна — гадання на кавовій гущі. Зберіть 50–100 реальних питань з правильними відповідями і джерелами, а далі міряйте окремо:
- Recall пошуку — чи є потрібний чанк серед top-k?
- Faithfulness — чи спирається відповідь лише на надані джерела?
- Answer correctness — чи правильна відповідь по суті?
УвагаТипова пастка — «покращити» промпт, коли проблема в пошуку. Спочатку подивіться, які чанки потрапили в контекст для невдалих відповідей.
Чому пошук за кодом помилки ERR_4021 погано працює в чисто векторному RAG?
Ембеддинги кодують зміст, а не точні рядки: ERR_4021 і ERR_4012 для них майже однакові. Допомагає гібридний пошук з повнотекстовою частиною, яка знаходить точний збіг.
Збережіть статтю собі
Увійдіть, щоб імпортувати її у власні нотатки, відмічати вивчене, ставити лайки й питати спільноту. Реєстрація безкоштовна: вхід через Google займає кілька секунд.
Увійти через Google Інші способи входу