Генерация с расширенным поиском (RAG)

Firebase Genkit предоставляет абстракции, которые помогают создавать потоки генерации с расширенным поиском (RAG), а также плагины, обеспечивающие интеграцию со связанными инструментами.

Что такое РАГ?

Генерация с расширенным поиском — это метод, используемый для включения внешних источников информации в ответы LLM. Важно уметь это делать, поскольку, хотя LLM обычно обучают широкому кругу материалов, практическое использование LLM часто требует знаний в конкретной предметной области (например, вы можете использовать LLM, чтобы отвечать на вопросы клиентов о деятельности вашей компании). продукты).

Одним из решений является точная настройка модели с использованием более конкретных данных. Однако это может оказаться дорогостоящим как с точки зрения вычислительных затрат, так и с точки зрения усилий, необходимых для подготовки адекватных обучающих данных.

Напротив, RAG работает путем включения внешних источников данных в подсказку во время ее передачи в модель. Например, вы можете представить себе подсказку: «Какие отношения у Барта с Лизой?» может быть расширен («дополнен») путем добавления некоторой соответствующей информации, в результате чего появится подсказка: «Детей Гомера и Мардж зовут Барт, Лиза и Мэгги. Каковы отношения Барта с Лизой?»

Этот подход имеет ряд преимуществ:

  • Это может быть более рентабельным, поскольку вам не придется переобучать модель.
  • Вы можете постоянно обновлять свой источник данных, и LLM сможет немедленно использовать обновленную информацию.
  • Теперь у вас есть возможность цитировать ссылки в ответах вашего LLM.

С другой стороны, использование RAG, естественно, означает более длинные запросы, а некоторые службы API LLM взимают плату за каждый отправленный вами входной токен. В конечном итоге вы должны оценить соотношение затрат для ваших приложений.

RAG – это очень обширная область, и существует множество различных методов, используемых для достижения RAG наилучшего качества. Базовая структура Genkit предлагает три основные абстракции, которые помогут вам реализовать RAG:

  • Индексаторы: добавляйте документы в «индекс».
  • Embedders: преобразует документы в векторное представление.
  • Retrievers: извлекают документы из «индекса» по запросу.

Эти определения намеренно являются широкими, поскольку Genkit не имеет мнения о том, что такое «индекс» или как именно из него извлекаются документы. Genkit предоставляет только формат Document , а все остальное определяется поставщиком реализации средства извлечения или индексатора.

Индексаторы

Индекс отвечает за отслеживание ваших документов таким образом, чтобы вы могли быстро получить соответствующие документы по конкретному запросу. Чаще всего это достигается с помощью базы данных векторов, которая индексирует ваши документы с использованием многомерных векторов, называемых встраиванием. Встраивание текста (непрозрачно) представляет концепции, выраженные отрывком текста; они генерируются с использованием моделей машинного обучения специального назначения. Индексируя текст с помощью его внедрения, векторная база данных способна группировать концептуально связанный текст и извлекать документы, связанные с новой текстовой строкой (запросом).

Прежде чем вы сможете получить документы для создания, вам необходимо включить их в индекс документов. Типичный поток приема делает следующее:

  1. Разделите большие документы на более мелкие, чтобы для дополнения подсказок использовались только соответствующие части – «разбиение на части». Это необходимо, поскольку многие LLM имеют ограниченное контекстное окно, что делает непрактичным включение целых документов в подсказку.

    Genkit не предоставляет встроенных библиотек фрагментирования; однако существуют библиотеки с открытым исходным кодом, совместимые с Genkit.

  2. Сгенерируйте вложения для каждого фрагмента. В зависимости от используемой вами базы данных вы можете сделать это явно с помощью модели генерации внедрения или использовать генератор внедрения, предоставляемый базой данных.

  3. Добавьте текстовый фрагмент и его индекс в базу данных.

Вы можете запускать поток приема нечасто или только один раз, если работаете со стабильным источником данных. С другой стороны, если вы работаете с данными, которые часто меняются, вы можете постоянно запускать поток приема (например, в триггере Cloud Firestore при каждом обновлении документа).

Встраивающие устройства

Средство внедрения — это функция, которая принимает контент (текст, изображения, аудио и т. д.) и создает числовой вектор, кодирующий семантическое значение исходного контента. Как упоминалось выше, средства внедрения используются как часть процесса индексирования, однако их также можно использовать независимо для создания внедрений без индекса.

Ретриверы

Средство извлечения — это концепция, инкапсулирующая логику, связанную с поиском любого вида документа. Наиболее популярные случаи извлечения обычно включают извлечение из хранилищ векторов, однако в Genkit средством извлечения может быть любая функция, возвращающая данные.

Чтобы создать ретривер, вы можете использовать одну из предоставленных реализаций или создать свою собственную.

Поддерживаемые индексаторы, ретриверы и средства внедрения

Genkit обеспечивает поддержку индексаторов и ретриверов через свою систему плагинов. Официально поддерживаются следующие плагины:

Кроме того, Genkit поддерживает следующие векторные хранилища посредством предопределенных шаблонов кода, которые вы можете настроить в соответствии с конфигурацией и схемой вашей базы данных:

Поддержка модели внедрения обеспечивается посредством следующих плагинов:

Плагин Модели
Генеративный искусственный интеллект Google Встраивание текста Gecko
Google Вертекс ИИ Встраивание текста Gecko

Определение потока RAG

В следующих примерах показано, как можно вставить коллекцию PDF-документов меню ресторана в векторную базу данных и извлечь их для использования в потоке, который определяет, какие продукты питания доступны.

Установите зависимости для обработки PDF-файлов

npm install llm-chunk pdf-parse @genkit-ai/dev-local-vectorstore
npm i -D --save @types/pdf-parse

Добавьте локальное хранилище векторов в свою конфигурацию.

import {
  devLocalIndexerRef,
  devLocalVectorstore,
} from '@genkit-ai/dev-local-vectorstore';
import { textEmbedding004, vertexAI } from '@genkit-ai/vertexai';
import { z, genkit } from 'genkit';

const ai = genkit({
  plugins: [
    // vertexAI provides the textEmbedding004 embedder
    vertexAI(),

    // the local vector store requires an embedder to translate from text to vector
    devLocalVectorstore([
      {
        indexName: 'menuQA',
        embedder: textEmbedding004,
      },
    ]),
  ],
});

Определить индексатор

В следующем примере показано, как создать индексатор для приема коллекции PDF-документов и сохранения ее в локальной базе данных векторов.

Он использует локальный инструмент поиска сходства векторов на основе файлов, который Genkit предоставляет «из коробки» для простого тестирования и прототипирования ( не используйте в производстве ).

Создайте индексатор

export const menuPdfIndexer = devLocalIndexerRef('menuQA');

Создать конфигурацию фрагментации

В этом примере используется библиотека llm-chunk , которая предоставляет простой разделитель текста для разбиения документов на сегменты, которые можно векторизовать.

Следующее определение настраивает функцию фрагментирования так, чтобы гарантировать сегмент документа длиной от 1000 до 2000 символов с разрывом в конце предложения и перекрытием между фрагментами по 100 символов.

const chunkingConfig = {
  minLength: 1000,
  maxLength: 2000,
  splitter: 'sentence',
  overlap: 100,
  delimiters: '',
} as any;

Дополнительные параметры фрагментации для этой библиотеки можно найти в документации llm-chunk .

Определите поток индексатора

import { Document } from 'genkit/retriever';
import { chunk } from 'llm-chunk';
import { readFile } from 'fs/promises';
import path from 'path';
import pdf from 'pdf-parse';

async function extractTextFromPdf(filePath: string) {
  const pdfFile = path.resolve(filePath);
  const dataBuffer = await readFile(pdfFile);
  const data = await pdf(dataBuffer);
  return data.text;
}

export const indexMenu = ai.defineFlow(
  {
    name: 'indexMenu',
    inputSchema: z.string().describe('PDF file path'),
    outputSchema: z.void(),
  },
  async (filePath: string) => {
    filePath = path.resolve(filePath);

    // Read the pdf.
    const pdfTxt = await run('extract-text', () =>
      extractTextFromPdf(filePath)
    );

    // Divide the pdf text into segments.
    const chunks = await run('chunk-it', async () =>
      chunk(pdfTxt, chunkingConfig)
    );

    // Convert chunks of text into documents to store in the index.
    const documents = chunks.map((text) => {
      return Document.fromText(text, { filePath });
    });

    // Add documents to the index.
    await ai.index({
      indexer: menuPdfIndexer,
      documents,
    });
  }
);

Запуск потока индексатора

genkit flow:run indexMenu "'menu.pdf'"

После запуска потока indexMenu база данных векторов будет заполнена документами и готова к использованию в потоках Genkit с этапами извлечения.

Определить поток с извлечением

В следующем примере показано, как можно использовать средство извлечения в потоке RAG. Как и в примере с индексатором, в этом примере используется средство извлечения векторов на основе файлов Genkit, которое не следует использовать в рабочей среде.

import { devLocalRetrieverRef } from '@genkit-ai/dev-local-vectorstore';

// Define the retriever reference
export const menuRetriever = devLocalRetrieverRef('menuQA');

export const menuQAFlow = ai.defineFlow(
  { name: 'menuQA', inputSchema: z.string(), outputSchema: z.string() },
  async (input: string) => {
    // retrieve relevant documents
    const docs = await ai.retrieve({
      retriever: menuRetriever,
      query: input,
      options: { k: 3 },
    });

    // generate a response
   const { text } = await ai.generate({
      prompt: `
You are acting as a helpful AI assistant that can answer 
questions about the food available on the menu at Genkit Grub Pub.

Use only the context provided to answer the question.
If you don't know, do not make up an answer.
Do not add or change items on the menu.

Question: ${input}`,
      docs,
    });

    return text;
  }
);

Напишите свои собственные индексаторы и ретриверы

Также возможно создать свой собственный ретривер. Это полезно, если ваши документы хранятся в хранилище документов, которое не поддерживается Genkit (например, MySQL, Google Drive и т. д.). Genkit SDK предоставляет гибкие методы, позволяющие предоставлять собственный код для получения документов. Вы также можете определить собственные средства извлечения, которые будут созданы на основе существующих средств извлечения в Genkit, и применить поверх них расширенные методы RAG (такие как изменение ранжирования или расширения подсказок).

Простые ретриверы

Простые ретриверы позволяют легко преобразовать существующий код в ретриверы:

import { z } from "genkit";
import { searchEmails } from "./db";

ai.defineSimpleRetriever(
  {
    name: "myDatabase",
    configSchema: z
      .object({
        limit: z.number().optional(),
      })
      .optional(),
    // we'll extract "message" from the returned email item
    content: "message",
    // and several keys to use as metadata
    metadata: ["from", "to", "subject"],
  },
  async (query, config) => {
    const result = await searchEmails(query.text, { limit: config.limit });
    return result.data.emails;
  }
);

Пользовательские ретриверы

import {
  CommonRetrieverOptionsSchema,
} from 'genkit/retriever';
import { z } from 'genkit';

export const menuRetriever = devLocalRetrieverRef('menuQA');

const advancedMenuRetrieverOptionsSchema = CommonRetrieverOptionsSchema.extend({
  preRerankK: z.number().max(1000),
});

const advancedMenuRetriever = ai.defineRetriever(
  {
    name: `custom/advancedMenuRetriever`,
    configSchema: advancedMenuRetrieverOptionsSchema,
  },
  async (input, options) => {
    const extendedPrompt = await extendPrompt(input);
    const docs = await ai.retrieve({
      retriever: menuRetriever,
      query: extendedPrompt,
      options: { k: options.preRerankK || 10 },
    });
    const rerankedDocs = await rerank(docs);
    return rerankedDocs.slice(0, options.k || 3);
  }
);

( extendPrompt и rerank — это то, что вам придется реализовать самостоятельно, это не предусмотрено фреймворком)

И тогда вы можете просто поменять свой ретривер:

const docs = await ai.retrieve({
  retriever: advancedRetriever,
  query: input,
  options: { preRerankK: 7, k: 3 },
});

Реранкеры и двухэтапный поиск

Модель реранжирования, также известная как кросс-кодировщик, представляет собой тип модели, которая на основе запроса и документа выводит оценку сходства. Мы используем эту оценку, чтобы переупорядочить документы по их релевантности нашему запросу. API-интерфейсы Reranker принимают список документов (например, выходные данные средства извлечения) и меняют порядок документов в зависимости от их релевантности запросу. Этот шаг может быть полезен для точной настройки результатов и обеспечения использования наиболее актуальной информации в подсказке, предоставляемой генеративной модели.

Пример реранкера

Реранкер в Genkit определяется синтаксисом, аналогичным синтаксису ретриверов и индексаторов. Вот пример использования реранкера в Genkit. Этот поток изменяет ранжирование набора документов на основе их релевантности предоставленному запросу с использованием предопределенного средства изменения ранжирования Vertex AI.

const FAKE_DOCUMENT_CONTENT = [
  'pythagorean theorem',
  'e=mc^2',
  'pi',
  'dinosaurs',
  'quantum mechanics',
  'pizza',
  'harry potter',
];

export const rerankFlow = ai.defineFlow(
  {
    name: 'rerankFlow',
    inputSchema: z.object({ query: z.string() }),
    outputSchema: z.array(
      z.object({
        text: z.string(),
        score: z.number(),
      })
    ),
  },
  async ({ query }) => {
    const documents = FAKE_DOCUMENT_CONTENT.map((text) =>
       ({ content: text })
    );

    const rerankedDocuments = await ai.rerank({
      reranker: 'vertexai/semantic-ranker-512',
      query:  ({ content: query }),
      documents,
    });

    return rerankedDocuments.map((doc) => ({
      text: doc.content,
      score: doc.metadata.score,
    }));
  }
);

Этот реранкер использует плагин Vertex AI genkit с semantic-ranker-512 для оценки и ранжирования документов. Чем выше балл, тем более релевантным является документ для запроса.

Пользовательские реранкеры

Вы также можете определить собственные средства изменения рейтинга в соответствии с вашим конкретным вариантом использования. Это полезно, когда вам нужно изменить ранжирование документов, используя собственную логику или модель. Вот простой пример определения пользовательского реранкера:

export const customReranker = ai.defineReranker(
  {
    name: 'custom/reranker',
    configSchema: z.object({
      k: z.number().optional(),
    }),
  },
  async (query, documents, options) => {
    // Your custom reranking logic here
    const rerankedDocs = documents.map((doc) => {
      const score = Math.random(); // Assign random scores for demonstration
      return {
        ...doc,
        metadata: { ...doc.metadata, score },
      };
    });

    return rerankedDocs.sort((a, b) => b.metadata.score - a.metadata.score).slice(0, options.k || 3);
  }
);

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