検索拡張生成(RAG)

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

RAG とは

検索拡張生成は、LLM のレスポンスに外部ソースの情報を組み込むために使用される手法です。LLM は通常、幅広い資料でトレーニングされますが、LLM を実際に使用するには特定の分野の知識が必要になることがよくあります(たとえば、会社の製品に関する顧客の質問に答えるには LLM を使用します)。

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

一方、RAG は、外部データソースがモデルに渡される時点でプロンプトに外部データソースを組み込むことで機能します。たとえば、「バートとリサの関係は何ですか?」というプロンプトが、関連情報を先頭に追加して拡張(「拡張」)すると、「Homer と Marge の子供は Bart、Lisa、Maggie という名前になります。バートとリサの関係は?」

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

  • モデルを再トレーニングする必要がないため、費用対効果が向上する可能性があります。
  • データソースは継続的に更新でき、更新された情報は LLM ですぐに利用できます。
  • これで、LLM の回答で参照を引用できるようになりました。

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

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

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

Genkit は「インデックス」の概要や、そのインデックスから正確にドキュメントを取得する方法に関して独自の意見を持たないため、これらの定義は目的が広くなっています。Genkit は Document 形式のみを提供し、それ以外はすべて、Retriever またはインデクサーの実装プロバイダによって定義されます。

インデクサ

インデックスは、特定のクエリに対して関連ドキュメントを迅速に取得できるようにドキュメントを追跡します。これは多くの場合、エンベディングと呼ばれる多次元ベクトルを使用してドキュメントをインデックスに登録するベクトル データベースを使用して実現されます。テキスト エンベディング(不透明)は、テキストの一節によって表現されるコンセプトを表します。これらは、特別な ML モデルを使用して生成されます。エンベディングを使用してテキストをインデックス化することで、ベクトル データベースは概念的に関連するテキストをクラスタ化し、新しいテキスト文字列(クエリ)に関連するドキュメントを取得できます。

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

  1. 大きなドキュメントを小さなドキュメントに分割し、関連する部分のみを使用してプロンプトを拡張します。つまり、「チャンク」です。多くの LLM はコンテキスト ウィンドウが限られており、プロンプトにドキュメント全体を含めるのは実用的ではないため、これが必要となります。

    Genkit には組み込みのチャンキング ライブラリは用意されていませんが、Genkit と互換性のあるオープンソース ライブラリが利用可能です。

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

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

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

エンベダー

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

レトリバー

取得ツールは、あらゆる種類のドキュメントの取得に関連するロジックをカプセル化する概念です。最も一般的なリトリーブのケースには通常、Vector Store からの検索が含まれますが、Genkit のレトリーバーはデータを返す任意の関数です。

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

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

Genkit はプラグイン システムを通じて、インデクサーとレトリーバーをサポートしています。公式にサポートされているプラグインは次のとおりです。

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

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

Plugin(プラグイン) モデル
Google の生成 AI Gecko テキスト エンベディング
Google Vertex AI Gecko テキスト エンベディング

RAG フローの定義

次の例は、レストランのメニューの PDF ドキュメントのコレクションをベクトル データベースに取り込み、取得して、利用可能な食品を決定するフローで使用する方法を示しています。

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

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

ローカルの Vector Store を構成に追加する

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 フローで Retriever を使用する方法を示しています。インデクサの例と同様に、この例では Genkit のファイルベースの Vector Retriever を使用していますが、本番環境では使用しないでください。

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