core code helper

RAG на практиці: чанкінг, ембеддинги та векторний пошук

Як навчити LLM відповідати за вашими документами: з чого складається RAG-конвеєр, як різати тексти на чанки і чому гібридний пошук майже завжди кращий за чисто векторний.

1 імпортували Імпортувати собі

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) — навпаки.

Найкращий результат зазвичай дає гібрид:

  1. Взяти top-20 з векторного пошуку і top-20 з повнотекстового.
  2. Об'єднати списки, наприклад, через Reciprocal Rank Fusion: score = Σ 1 / (60 + rank).
  3. Пропустити 20–40 кандидатів через reranker — модель, що оцінює пару «питання — чанк» точніше за ембеддинги.
  4. Залишити 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 Інші способи входу