Generowanie rozszerzone przez wyszukiwanie w zapisanych informacjach (RAG)

Firebase Genkit udostępnia abstrakcje, które ułatwiają tworzenie przepływów generowania wzbogaconego o wyszukiwanie (RAG), oraz wtyczek umożliwiających integrację z powiązanymi narzędziami.

Co to jest RAG?

Generowanie z użyciem techniki wzbogacania za pomocą wyszukiwania to technika służąca do włączania zewnętrznych źródeł informacji do odpowiedzi LLM. Jest to ważne, ponieważ LLM są zwykle trenowane na podstawie szerokiego zbioru materiałów, ale ich praktyczne zastosowanie często wymaga znajomości konkretnej dziedziny (np. możesz chcieć użyć LLM, aby odpowiadać na pytania klientów dotyczące produktów Twojej firmy).

Jednym z rozwiązań jest dostrojenie modelu za pomocą bardziej szczegółowych danych. Może to jednak być kosztowne, zarówno pod względem kosztów obliczeń, jak i wysiłku potrzebnego do przygotowania odpowiednich danych treningowych.

W tym przypadku zewnętrzne źródła danych są uwzględniane w promptach w momencie ich przekazywania do modelu. Wyobraźmy sobie na przykład prompt „Jaki jest związek Barta z Lisą?”. Może on zostać rozszerzony („wzbogacony”) o odpowiednie informacje, dzięki czemu prompt będzie brzmiał: „Dzieci Homera i Marge to Bart, Lisa i Maggie. „Jaki jest związek Barta z Lisą?”

Takie podejście ma kilka zalet:

  • Może to być bardziej opłacalne, ponieważ nie musisz ponownie trenować modelu.
  • Możesz stale aktualizować źródło danych, a LLM może natychmiast korzystać z zaktualizowanych informacji.
  • Możesz teraz cytować źródła w odpowiedziach na pytania dotyczące LLM.

Z drugiej strony korzystanie z RAG oznacza dłuższe prompty, a niektóre usługi interfejsu LLM API pobierają opłaty za każdy wysłany token wejściowy. Ostatecznie musisz ocenić kompromisy dotyczące kosztów w przypadku swoich aplikacji.

RAG to bardzo szeroka dziedzina, a do osiągnięcia najlepszej jakości RAG służy wiele różnych technik. Podstawowa platforma Genkit oferuje 3 główne abstrakcje, które ułatwiają realizację procesu RAG:

  • Indeksatory: dodawanie dokumentów do „indeksu”.
  • Embedders: przekształca dokumenty w postać wektorową
  • Retrievers: pobiera dokumenty z „indeksu” na podstawie zapytania.

Te definicje są celowo ogólne, ponieważ Genkit nie ma zdania na temat tego, czym jest „indeks” ani jak dokładnie dokumenty są z niego pobierane. Genkit udostępnia tylko format Document, a wszystko inne jest definiowane przez dostawcę implementacji wyszukiwarki lub indeksatora.

indeksowanie,

Indeks odpowiada za śledzenie dokumentów w taki sposób, aby można było szybko pobrać odpowiednie dokumenty w odpowiedzi na konkretne zapytanie. Najczęściej odbywa się to za pomocą bazy danych wektorów, która indeksuje dokumenty za pomocą wielowymiarowych wektorów zwanych wektorami dystrybucyjnymi. Umieszczenie tekstu (nieprzejrzyście) przedstawia pojęcia wyrażone przez fragment tekstu; są one generowane za pomocą modeli AI do specjalnych celów. Dzięki indeksowaniu tekstu za pomocą jego uczenia się, baza danych wektorowej może grupować teksty o powiązaniach semantycznych i pobierać dokumenty związane z nowym ciągiem tekstowym (zapytaniem).

Zanim będzie można pobrać dokumenty na potrzeby generowania, musisz je zaimportować do indeksu dokumentów. Typowy proces przetwarzania obejmuje te czynności:

  1. Podziel duże dokumenty na mniejsze, aby do wzbogacania promptów używać tylko odpowiednich fragmentów (tzw. „chunking”). Jest to konieczne, ponieważ wiele modeli LLM ma ograniczone okno kontekstu, przez co dołączanie całych dokumentów z promptem jest niepraktyczne.

    Genkit nie udostępnia wbudowanych bibliotek dzielenia na części, ale dostępne są biblioteki open source, które są z nim zgodne.

  2. wygenerować embeddingi dla każdego fragmentu; W zależności od używanej bazy danych możesz to zrobić za pomocą modelu generowania wektorów lub skorzystać z generatora wektorów udostępnianego przez bazę danych.

  3. Dodaj fragment tekstu i jego indeks do bazy danych.

Jeśli pracujesz ze stabilnym źródłem danych, przetwarzanie możesz uruchamiać rzadko lub tylko raz. Jeśli jednak pracujesz z danymi, które często się zmieniają, możesz stale uruchamiać przetwarzanie (np. w wyzwalaczu Cloud Firestore, gdy dokument jest aktualizowany).

Umieszczacze

Funkcja embedder przyjmuje dane (tekst, obrazy, dźwięk itp.) i tworzy wektor liczbowy, który koduje znaczenie semantyczne pierwotnych danych. Jak wspomnieliśmy powyżej, osadzanie jest wykorzystywane w ramach procesu indeksowania, ale można go też używać niezależnie do tworzenia elementów osadzenia bez indeksu.

Retrievery

Retriever to koncepcja obejmująca logikę związaną z dowolnym rodzajem wyszukiwania dokumentów. Najpopularniejsze przypadki wyszukiwania obejmują zazwyczaj wyszukiwanie z magazynów wektorów, ale w Genkit wyszukiwarka może być dowolną funkcją zwracającą dane.

Aby utworzyć retriever, możesz użyć jednej z dostępnych implementacji lub utworzyć własną.

Obsługiwane indeksatory, pobierający i wstawiający

Genkit obsługuje indeksator i wyszukiwarkę za pomocą systemu wtyczek. Te wtyczki są oficjalnie obsługiwane:

Genkit obsługuje też te wektorowe repozytoria danych za pomocą wstępnie zdefiniowanych szablonów kodu, które możesz dostosować do konfiguracji i schematu bazy danych:

Obsługa modela wstawiania jest dostępna w tych wtyczkach:

Wtyczka Modele
Generative AI od Google Wektor dystrybucyjny tekstu w Gecko
Google Vertex AI Wektor dystrybucyjny tekstu w Gecko

Definiowanie przepływu RAG

Poniższe przykłady pokazują, jak można przetworzyć kolekcję dokumentów PDF z menu restauracji do bazy danych wektorów i pobrać je do użycia w przepływie, który określa, jakie produkty spożywcze są dostępne.

Instalowanie zależności do przetwarzania plików PDF

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

Dodawanie lokalnego magazynu wektorów do konfiguracji

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,
      },
    ]),
  ],
});

Definiowanie indeksatora

W tym przykładzie pokazujemy, jak utworzyć indeksator, który przetworzy kolekcję dokumentów PDF i zapisze je w lokalnej bazie danych wektorów.

Korzysta z lokalnego modułu wyszukiwania podobieństwa wektorów w plikach, który Genkit udostępnia „w standardzie” do prostego testowania i tworzenia prototypów (nie używaj w wersji produkcyjnej).

Tworzenie indeksatora

export const menuPdfIndexer = devLocalIndexerRef('menuQA');

Tworzenie konfiguracji podziału na fragmenty

W tym przykładzie użyto biblioteki llm-chunk, która udostępnia prosty dzielnik tekstu do dzielenia dokumentów na segmenty, które można wektoryzować.

Definicja poniżej konfiguruje funkcję dzielenia na części, aby zagwarantować, że segment dokumentu będzie zawierać od 1000 do 2000 znaków, będzie podzielony na końcu zdania, a poszczególne części będą się na siebie nakładać o 100 znaków.

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

Więcej opcji podziału tej biblioteki znajdziesz w dokumentacji llm-chunk.

Definiowanie procesu indeksowania

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,
    });
  }
);

Uruchamianie procesu indeksowania

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

Po uruchomieniu procesu indexMenu baza danych wektorów zostanie zasilona dokumentami i będzie gotowa do użycia w procesach Genkit z krokami wyszukiwania.

Definiowanie przepływu danych z odzyskiwaniem

Z tego przykładu dowiesz się, jak używać funkcji pobierania w ramach procesu RAG. Podobnie jak przykład indeksatora, ten przykład korzysta z uzyskiwacza wektorów opartego na plikach Genkit, którego nie należy używać w sytuacji produkcyjnej.

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;
  }
);

Pisanie własnych indeksatorów i wyszukiwarek

Możesz też utworzyć własnego retrievera. Jest to przydatne, jeśli dokumenty są zarządzane w magazynie dokumentów, który nie jest obsługiwany przez Genkit (np. MySQL, Dysk Google itp.). Pakiet SDK Genkit udostępnia elastyczne metody, które umożliwiają podanie niestandardowego kodu do pobierania dokumentów. Możesz też zdefiniować niestandardowe retrievery, które będą się opierać na istniejących retrieverach w Genkit i stosować zaawansowane techniki RAG (np. ponowne rankingowanie lub rozszerzenia promptów).

Simple Retrievers

Dzięki prostemu modułowi wyszukiwania możesz łatwo przekształcić istniejący kod w moduł wyszukiwania:

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;
  }
);

Niestandardowe funkcje pobierania

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);
  }
);

(funkcje extendPromptrerank musisz zaimplementować samodzielnie, ponieważ nie są one udostępniane przez framework)

Następnie możesz po prostu zamienić odzyskiwanie:

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

Rerankery i pobieranie dwustopniowe

Model ponownego pozycjonowania, zwany też enkoderem krzyżowym, to typ modelu, który na podstawie zapytania i dokumentu zwraca wynik podobieństwa. Korzystamy z tego wyniku, aby uporządkować dokumenty według ich trafności w odniesieniu do zapytania. Interfejsy API służące do ponownego sortowania danych biorą listę dokumentów (np. dane wyjściowe z interfejsu retrievera) i zmieniają ich kolejność na podstawie ich trafności w odniesieniu do zapytania. Ten krok może być przydatny do dokładnego dostosowania wyników i upewnienia się, że w promptach przekazywanych modelowi generatywnemu są używane najbardziej istotne informacje.

Przykład funkcji Reranker

Funkcja reranker w Genkit jest definiowana w podobnej składni jak funkcje retriever i indexer. Oto przykład użycia funkcji reranker w Genkit. Ten proces zmienia kolejność zestawu dokumentów na podstawie ich trafności w odniesieniu do podanego zapytania za pomocą zdefiniowanego wstępnie narzędzia do zmiany kolejności w 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,
    }));
  }
);

Ten moduł do ponownego ustalania rankingu używa wtyczki genkit Vertex AI z parametrem semantic-ranker-512 do oceniania i ustalania rankingu dokumentów. Im wyższy wynik, tym dokument jest trafniejszy w odniesieniu do zapytania.

Niestandardowe algorytmy rerankingu

Możesz też zdefiniować niestandardowe algorytmyk rerankingu, aby dostosować je do konkretnego przypadku użycia. Jest to przydatne, gdy chcesz ponownie ustawić kolejność dokumentów za pomocą własnej logiki lub niestandardowego modelu. Oto prosty przykład definiowania niestandardowego modułu przeszeregowania:

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);
  }
);

Po zdefiniowaniu tego niestandardowego algorytmu ponownego rankingowania możesz go używać tak jak każdego innego algorytmu w procesach RAG, co daje Ci elastyczność w wdrażaniu zaawansowanych strategii ponownego rankingowania.