RAG: полный обзор

Как устроена генерация с поиском: восемь шагов, RAG в сравнении с дообучением и длинным промптом, плюсы и минусы, три шага в коде, ошибки и правила надёжной системы.

Стек и технологии Обновлено

Коротко

RAG (retrieval-augmented generation, генерация с поиском) — способ заставить языковую модель отвечать по вашим материалам, а не по своей общей памяти. Документы режутся на куски, каждый кусок получает вектор — числовой отпечаток смысла — и ложится в базу. Когда приходит вопрос, ближайшие по смыслу куски находятся и передаются модели вместе с вопросом, и модель отвечает только по ним, ссылаясь на источники. Так ИИ-ассистенты отвечают о ценах, условиях и товарах, ничего не выдумывая. Если ничего подходящего не нашлось, хорошая RAG-система так и говорит и передаёт разговор человеку.

RAG коротко

Главные факты одной таблицей: что такое RAG, из чего он состоит и где применяется.

Что это
Поиск по вашим материалам плюс ответ модели по найденному
Термин
Введён в статье Facebook AI Research 2020 года
Части
Нарезка, модель эмбеддингов, векторный индекс, языковая модель
Где живут векторы
PostgreSQL с pgvector, SQLite с расширением или отдельная векторная база
Модели
Claude, GPT или локальные модели — под задачу и под данные
Обновление
Изменённый документ переиндексируется — без переобучения модели
Главный риск
Плохой поиск: модель уверенно отвечает по не тем кускам

Как устроен RAG: 8 шагов

Первые четыре шага делаются один раз и повторяются при изменении документов; последние четыре — на каждый вопрос.

ШагЧто происходитОт чего зависит качество
1. Собрать документы, каталог, условия, ответы на частые вопросы актуальные и непротиворечивые источники
2. Нарезать куски по заголовкам и абзацам одна мысль на кусок, вместе с заголовком
3. Векторизовать каждый кусок получает вектор смысла модель эмбеддингов, которая знает язык
4. Сохранить куски и векторы ложатся в базу источник и дата рядом с каждым куском
5. Найти ближайшие куски по смыслу и по словам порог сходства, фильтры по разделам
6. Переранжировать вторая модель пересортировывает кандидатов по желанию, но повышает точность
7. Ответить модель пишет только по пронумерованным кускам строгая инструкция и ссылки
8. Записать вопрос, найденные куски, ответ, оценка разбор журнала и закрытие пробелов

RAG, дообучение или длинный промпт

Три способа передать модели ваши знания. Они решают разные задачи и часто сочетаются.

КритерийRAGДообучениеВсё в промпте
Хорош для фактов, которые меняются стиля и формата ответов небольшого неизменного набора текстов
Обновление знаний переиндексировать документ обучать заново поправить промпт
Источники в ответе да нет частично
Объём знаний практически без предела ограничен данными обучения ограничен окном контекста
Цена вопроса низкая: только найденные куски низкая высокая: весь текст каждый раз
Права доступа фильтры при поиске невозможны отдельный промпт на роль
Старт дни недели и набор данных часы

Плюсы и минусы RAG

В RAG ответ хорош ровно настолько, насколько хорош поиск. И сильные, и слабые стороны идут отсюда.

Плюсы · 5

  • Ответы по вашим данным

    Цены, условия и характеристики берутся из ваших документов, а не из памяти модели.

  • Источники можно проверить

    У каждого утверждения есть ссылка на кусок, из которого оно взято.

  • Свежесть без переобучения

    Изменённый прайс переиндексируется за секунды.

  • Права при поиске

    Клиент и менеджер получают ответы по разным наборам документов.

  • Любая модель

    Модель можно заменить, не пересобирая базу знаний.

Минусы · 4

  • Поиск решает всё

    Если нужный кусок не нашёлся, даже лучшая модель ответит плохо.

  • Мусор в базе

    Устаревшие и противоречивые документы превращаются в уверенные неверные ответы.

  • Вопросы по всей базе

    «Сколько было заказов в марте» — это запрос к базе данных, а не поиск по текстам.

  • Требует ухода

    Журнал нужно разбирать, а пробелы в знаниях — закрывать.

Как RAG выглядит в коде: 3 шага

Нарезка, индексация и ответ на TypeScript с PostgreSQL и pgvector. Цепочка прогнана целиком на тестовой базе знаний мебельного магазина.

Нарезка на куски

Куски идут по заголовкам и абзацам: в одном куске одна мысль, и он знает, откуда взят.

chunk.ts
// Разрезать документ на куски до ~800 символов по границам абзацев.
// Каждый кусок помнит источник и заголовок — ответ будет на них ссылаться.
export type Chunk = { source: string; heading: string; text: string };

export function chunk(source: string, markdown: string, maxChars = 800): Chunk[] {
  const chunks: Chunk[] = [];
  let heading = '';
  let buffer: string[] = [];

  const flush = () => {
    const text = buffer.join('\n\n').trim();
    if (text) chunks.push({ source, heading, text });
    buffer = [];
  };

  for (const block of markdown.split(/\n{2,}/)) {
    if (block.startsWith('#')) {
      flush(); // новый заголовок всегда начинает новый кусок
      heading = block.replace(/^#+\s*/, '');
      continue;
    }
    if (buffer.join('\n\n').length + block.length > maxChars) flush();
    buffer.push(block);
  }
  flush();
  return chunks;
}

Индексация

Векторы — от любого OpenAI-совместимого API; повторная индексация заменяет куски документа одним запросом, без дублей.

index.ts
// Индексация: каждый кусок получает вектор и ложится в PostgreSQL с pgvector.
// CREATE TABLE chunks (id bigserial PRIMARY KEY, source text, heading text,
//                      text text, embedding vector(384));
import pg from 'pg';
import { chunk } from './chunk.ts';

const db = new pg.Pool({ connectionString: process.env.DATABASE_URL });

// Любой OpenAI-совместимый API эмбеддингов — облачный провайдер или локальная модель
export async function embed(texts: string[]): Promise<number[][]> {
  const res = await fetch(`${process.env.EMBEDDINGS_URL}/v1/embeddings`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.EMBEDDINGS_KEY}`,
    },
    body: JSON.stringify({ model: process.env.EMBEDDINGS_MODEL, input: texts }),
  });
  if (!res.ok) throw new Error(`embeddings: HTTP ${res.status}`);
  const { data } = (await res.json()) as { data: { embedding: number[] }[] };
  return data.map((d) => d.embedding);
}

export async function indexDocument(source: string, markdown: string): Promise<number> {
  const pieces = chunk(source, markdown);
  const vectors = await embed(pieces.map((p) => `${p.heading}\n${p.text}`));
  // Один запрос: старые куски документа заменяются атомарно
  await db.query(
    `WITH old AS (DELETE FROM chunks WHERE source = $1)
     INSERT INTO chunks (source, heading, text, embedding)
     SELECT $1, h, t, e::vector FROM unnest($2::text[], $3::text[], $4::text[]) AS u(h, t, e)`,
    [source, pieces.map((p) => p.heading), pieces.map((p) => p.text), vectors.map((v) => JSON.stringify(v))],
  );
  return pieces.length;
}

Ответ с источниками

В проверке вопросы о доставке, возврате и гарантии нашли нужный кусок со сходством 0,52–0,60, а вопрос о погоде набрал 0,14 и до модели не дошёл.

ask.ts
// Ответ: найти ближайшие куски, отдать их модели с номерами, потребовать ссылки —
// и вовсе не вызывать модель, если ничего не нашлось
import pg from 'pg';
import { embed } from './index.ts';

const db = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const API = process.env.ANTHROPIC_BASE_URL ?? 'https://api.anthropic.com';

export async function ask(question: string) {
  const [q] = await embed([question]);
  const { rows } = await db.query(
    `SELECT source, heading, text, 1 - (embedding <=> $1) AS score
       FROM chunks ORDER BY embedding <=> $1 LIMIT 4`,
    [JSON.stringify(q)],
  );
  // Порог подбирается на ваших вопросах; ниже него — не ответ
  const found = rows.filter((r) => r.score > 0.5);
  if (found.length === 0) return { answer: null, sources: [] }; // передать человеку

  const context = found.map((r, i) => `[${i + 1}] ${r.heading}\n${r.text}`).join('\n\n');
  const res = await fetch(`${API}/v1/messages`, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'x-api-key': process.env.ANTHROPIC_API_KEY ?? '',
      'anthropic-version': '2023-06-01',
    },
    body: JSON.stringify({
      model: 'claude-sonnet-5',
      max_tokens: 600,
      system:
        'Отвечай только по источникам ниже и ссылайся на них как [1], [2]. ' +
        'Если в источниках нет ответа, так и скажи.\n\n' + context,
      messages: [{ role: 'user', content: question }],
    }),
  });
  if (!res.ok) throw new Error(`model: HTTP ${res.status}`);
  const data = (await res.json()) as { content: { type: string; text: string }[] };
  return { answer: data.content[0].text, sources: found.map((r) => r.source) };
}

Частые ошибки в RAG-проектах

  1. Куски фиксированной длины

    Нарезка каждые 500 символов рвёт правило пополам, и ни одна половина не отвечает на вопрос.

  2. Нет порога

    Ближайший кусок находится всегда, даже на вопрос о погоде, — и модель строит ответ на нём.

  3. Только векторный поиск

    Артикулы, названия моделей и коды лучше ищутся по словам — два поиска сочетают.

  4. Модель эмбеддингов только для английского

    Вопросы на русском ничего не находят; модель должна знать языки ваших клиентов.

  5. Проиндексировать и забыть

    Прайс поменялся, а база нет — ассистент называет старые цены.

  6. Нет набора проверочных вопросов

    Без 30–50 настоящих вопросов с ожидаемыми ответами любая правка — гадание.

7 правил надёжного RAG

  1. 01

    Сначала вычистить источники

    Убрать устаревшие версии и противоречия до индексации.

  2. 02

    Куски по смыслу

    Заголовки и абзацы как границы, заголовок — внутри каждого куска.

  3. 03

    Два поиска вместе

    По смыслу и по словам; результаты объединяются.

  4. 04

    Порог и честное «не знаю»

    Ниже порога модель не вызывается — вопрос уходит человеку.

  5. 05

    Ссылки обязательны

    Ответ без источника не показывается.

  6. 06

    Права — в запросе

    Фильтры по ролям применяются при поиске, а не в инструкции модели.

  7. 07

    Мерить на настоящих вопросах

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

Вопросы о RAG

Что такое RAG простыми словами?

Сначала найти ответ в ваших материалах, потом дать модели его сформулировать — со ссылкой на источник.

Убирает ли RAG выдумки модели?

Резко сокращает, если есть порог, строгая инструкция и ссылки; без них — нет.

Нужна ли векторная база?

Отдельная обычно нет: pgvector в PostgreSQL держит миллионы кусков рядом с остальными данными.

Используются ли мои данные для обучения модели?

Нет при деловом доступе к Claude или GPT через API; для самых строгих случаев есть локальные модели.

Какие документы подходят?

Тексты, таблицы, PDF, страницы сайта, каталог; сканы сначала распознаются в текст.

Как измерить качество?

На наборе настоящих вопросов: нашёлся ли нужный кусок, верен ли ответ и подтверждён ли источником.

Как RAG связан с MCP?

RAG ищет по текстам, MCP даёт модели инструменты — заказы, остатки, CRM. Ассистенты часто используют оба.

Форма

Ассистент
по вашим данным

Делаю ИИ-ассистентов, которые отвечают по вашим документам, каталогу и правилам — со ссылками на источники и передачей человеку, когда ответа нет. Расскажите о задаче — отвечу в течение рабочего дня.

Или пишите на [email protected]