検索拡張生成(RAG)

Firebase Genkit は、検索拡張生成の構築に役立つ抽象化機能を提供します (RAG)フローと、関連ツールとの統合を可能にするプラグインが含まれます。

RAG とは

検索拡張生成は、外部のソフトウェア ツールを組み込むために使用される手法 LLM のレスポンスに変換しています。LLM は通常、広範な資料に基づいてトレーニングされますが、LLM の実用には特定分野の知識が必要になることが多いため、これを可能にすることが重要です(たとえば、LLM を使用して、自社の製品に関する顧客からの質問に回答するなど)。

解決策の 1 つは、より具体的なデータを使用してモデルをファインチューニングすることです。ただし、コンピューティングの費用面と、十分なトレーニング データを準備するために必要な労力面の双方で、費用がかかる可能性があります。

対照的に、RAG は外部データソースをプロンプトに組み込み、 渡される時刻を表します。たとえば、「バートとリサの関係は?」というプロンプトが、関連する情報を先頭に追加して拡大(「拡張」)され、「ホーマーとマージの子供はバート、リサ、マギーです。バートとリサの関係は?」というプロンプトになります。

このアプローチにはいくつかのメリットがあります。

  • モデルを再トレーニングする必要がないため、費用対効果を高めることがで��ます。
  • データソースは継続的に更新でき、LLM はすぐに 更新された情報の使用についてのみです。
  • LLM の回答で参照を引用できるようになります。

一方、RAG を使用する場合は当然、プロンプトが長くなり、LLM API も サービスの料金は、送信した入力トークンごとに発生します。最終的には、アプリケーションの費用に関するトレードオフを評価する必要があります。

RAG は非常に広い分野であり、最高品質の RAG を実現するためにさまざまな手法が使用されます。Genkit のコア フレームワークは、RAG を実行するための 2 つの主要な抽象化を提供します。

  • インデクサー: ドキュメントを「インデックス」に追加します。
  • エンベッダー: ドキュメントをベクトル表現に変換します。
  • リトリーバー: クエリを指定して「インデックス」からドキュメントを検索します。

これらの定義には、意図的に曖昧さを残しています。これは、Genkit では「インデックス」とは何か、またはインデックスからドキュメントがどのように正確に検索されるかを、一つに定めないようにしているためです。Genkit は Document 形式のみを提供し、それ以外はすべて、リトリーバーまたはインデクサーの実装プロバイダによって定義されます。

インデクサ

インデックスは、特定のクエリに関連するドキュメントをすばやく検索できるように、ドキュメントを追跡する役割を果たします。ほとんどの場合、これはベクトル データベースを使用して実現されます。ベクトル データベースは、エンベディングと呼ばれる多次元ベクトルを使用してドキュメントのインデックスを作成します。テキスト エンベディングは、テキストの文章で表現される概念を(不透明な形で)表現したものであり、専用の ML モデルを使用して生成されます。ベクトル データベースは、そのエンベディングを使用してテキストのインデックスを作成することで、概念的に関連するテキストをクラスタ化し、新しいテキスト文字列(クエリ)に関連するドキュメントを検索できます。

生成のためにドキュメントを検索するには、ドキュメントをドキュメント インデックスに取り込む必要があります。一般的な取り込みフローでは、 次のとおりです。

  1. 関連する部分のみがプロンプトの強化に使用されるように、大きなドキュメントを小さなドキュメントに分割します(チャンク化)。多くの LLM はコンテキスト ウィンドウが限られているため、プロンプトにドキュメント全体を含めることは現実的ではないことが、これを行う理由です。

    Genkit には、組み込みのチャンク ライブラリはありません。ただしオープンテストでは Genkit と互換性のあ��ソース ライブラリがあります。

  2. チャンクごとにエンベディングを生成する。使用しているデータベースに応じて、エンベディング生成モデルで明示的に行����と��������ータベースが提供するエンベディング生成ツールを使用することもできます。

  3. テキスト チャンクとそのインデックスをデータベースに追加します。

安定したデータソースを使用している場合は、取り込みフローを頻繁に実行しないか、1 回だけ実行することをおすすめします。一方、頻繁に変更されるデータを処理する場合は、取り込みフローを継続的に実行することをおすすめします(たとえば、ドキュメントが更新されるたびに Cloud Firestore トリガーで実行する)。

エンベッダー

エンベッダーは、コンテンツ(テキスト、画像、音声など)を受け取り、元のコンテンツの意味をエンコードする数値ベクトルを作成する関数です。前述のように、エンベッダーはインデックス作成プロセスの一部として活用されますが、インデックスなしでエンベディングを作成するために独立に使用することもできます。

リトリーバー

リトリーバーは、あらゆる種類のドキュメント検索に関連するロジックをカプセル化するコンセプトです。検索の最も一般的なケースには、通常、ベクトル ストアからの検索が含まれますが、Genkit では、データを返す任意の関数をリトリーバーにすることができます。

リトリーバーの作成には、提供されている実装のいずれかを使用するか、独自に作成します。

サポートされているインデクサー、リトリーバー、エンベッダー

Genkit は、プラグイン システムでインデクサーとリトリーバーをサポートします。「 次のプラグインが正式にサポートされています。

また、Genkit は、事前定義されたコード テンプレートで次のベクトル ストアをサポートします。テンプレートは、データベースの構成とスキーマに合わせてカスタマイズできます。

エンベディング モデルのサポートは、次のプラグインによって提供されます。

プラグイン モデル
Google Generative AI Gecko テキスト エンベディング
Google Vertex AI Gecko テキスト エンベディング

RAG フローを定義する

次の例は、レスト��ン メニューの PDF ドキュメントのコレクションをベクトル データベースに取り込み、フロー内で使用するために検索する方法を示しています。

PDF を処理するための依存関係をインストールする

npm install llm-chunk pdf-parse
npm i -D --save @types/pdf-parse

ローカルのベクトルストアを構成に追加する

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

configureGenkit({
  plugins: [
    // vertexAI provides the textEmbeddingGecko embedder
    vertexAI(),

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

インデクサーを定義する

次の例は、PDF ドキュメントのコレクションを取り込んでローカルのベクトル データベースに保存するインデクサーの作成方法を示しています。

シンプルなテストとプロトタイピングのために、Genkit が提供するローカルのファイルベースのベクトル類似度リトリーバーを使用します(本番環境では使用しないでください)。

インデクサーを作成する

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

export const menuPdfIndexer = devLocalIndexerRef('menuQA');

チャンク化構成を作成する

この例では、シンプルなテキスト分割機能を提供する llm-chunk ライブラリを使用して、ドキュメントをベクトル化可能なセグメントに分割します。

次の定義では、1, 000 ~ 2, 000 文字のドキュメント セグメントを文の末尾で分割し、100 文字のチャンク間で重複するようにチャンク関数を構成します。

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

このライブラリのその他のチャンク オプションについては、llm-chunk のドキュメントをご覧ください。

インデクサ フローを定義する

import { index } from '@genkit-ai/ai';
import { Document } from '@genkit-ai/ai/retriever';
import { defineFlow, run } from '@genkit-ai/flow';
import { readFile } from 'fs/promises';
import { chunk } from 'llm-chunk';
import path from 'path';
import pdf from 'pdf-parse';
import * as z from 'zod';

export const indexMenu = 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 index({
      indexer: menuPdfIndexer,
      documents,
    });
  }
);

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

インデクサ フローを実行する

genkit flow:run indexMenu "'../pdfs'"

indexMenu フローを実行すると、ベクトル データベースにドキュメントがシードされ、取得ステップとともに Genkit フローで使用できる状態になります。

検索を含むフローを定義する

次の例は、RAG フロー内でリトリーバーを使用する方法を示しています。インデクサーの例と同様に、この例では Genkit のファイルベースのベクトル リトリーバーを使用します。これは本番環境では使用しないでください。

import { generate } from '@genkit-ai/ai';
import { retrieve } from '@genkit-ai/ai/retriever';
import { devLocalRetrieverRef } from '@genkit-ai/dev-local-vectorstore';
import { defineFlow } from '@genkit-ai/flow';
import { gemini15Flash } from '@genkit-ai/vertexai';
import * as z from 'zod';

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

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

    // generate a response
    const llmResponse = await generate({
      model: gemini15Flash,
      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}
    `,
      context: docs,
    });

    const output = llmResponse.text();
    return output;
  }
);

独自のインデクサーとリトリーバーを作成する

リトリーバーは独自に作成することもできます。この方法は、 ドキュメントが Genkit でサポートされていないドキュメント ストア( MySQL、Google ドライブなど)。Genkit SDK には柔軟なメソッドが備わっており、 ドキュメントを取得するためのカスタムコードを提供しますまた、カスタム Genkit の既存のレトリバーを基にして高度な手法を適用し、 RAG 手法(再ランキングやプロンプト拡張機能など)を使用します。

シンプルなレトリバー

シンプルなレトリバーを使用すると、既存のコードを簡単にレトリバーに変換できます。

import {
  defineSimpleRetriever,
  retrieve
} from '@genkit-ai/ai/retriever';
import { searchEmails } from './db';
import { z } from 'zod';

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,
  defineRetriever,
  retrieve,
} from '@genkit-ai/ai/retriever';
import * as z from 'zod';

export const menuRetriever = devLocalRetrieverRef('menuQA');

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

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

extendPromptrerank はご自身で実装する必要がありますが、 フレームワークによって提供されない)

次に、レトリーバーをスワップアウトします。

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