擷取產生的產生作業 (RAG)

Firebase Genkit 提供抽象化機制,可協助您建構檢索機制 (RAG) 流程,以及提供相關工具整合的外掛程式。

什麼是 RAG?

重新擷取產生技術的作用是將外部資訊來源納入 LLM 的回應中。這種方法很重要,因為 LLM 通常接受大量材質的訓練,但實際應用 LLM 通常需要具備特定領域知識 (例如,您可能會想使用 LLM 回答客戶對公司產品的問題)。

其中一個解決方法,就是使用更具體的資料微調模型。然而,這在運算成本和準備充分的訓練資料方面可能耗費資源。

相反地,RAG 會在將外部資料來源傳遞至模型時,將外部資料來源加入提示中。例如,您可能會想像「Bart 與 Lisa 的關係為何?」這樣的提示可能會延伸 (「加盟」) 一些相關資訊,導致提示「Homer and Marge's 的子項是 Bart、Lisa 和 Maggie」。Bart 和 Lisa 的關係是什麼?」

這種方法有幾個優點:

  • 由於您不必重新訓練模型,這可能會更具成本效益。
  • 您可以持續更新資料來源,讓 LLM 立即使用更新後的資訊。
  • 現在可以在 LLM 的回覆中引用參考資料。

另一方面,使用 RAG 自然會產生較長的提示,部分 LLM API 服務會針對您傳送的每個輸入權杖收費。最後,您必須評估應用程式的成本取捨。

RAG 的領域相當廣泛,目前也有許多不同的技術可以達到最佳的 RAG。核心 Genkit 架構提供兩項主要抽象化機制,可協助您執行 RAG:

  • 索引:將文件新增至「索引」。
  • 嵌入程式:將文件轉換成向量表示法
  • 擷取:依據查詢從「索引」擷取文件。

這些定義的定義相當廣泛,因為 Genkit 對於「索引」的定義或從其中擷取文件的方式沒有意見。Genkit 僅提供 Document 格式,其他則是由擷取器或索引工具實作提供者定義。

索引員

索引可以負責追蹤您的文件,方式是方便您針對特定查詢快速擷取相關文件。這類作業通常是使用向量資料庫完成,這類資料庫會使用名為嵌入的多維度向量為文件建立索引。文字嵌入 (不透明) 代表由文字段落表達的概念,而這類概念是使用特殊用途的機器學習模型產生。透過使用其嵌入功能建立文字索引,向量資料庫能夠建立概念性相關的文字叢集,並擷取與新文字字串 (查詢) 相關的文件。

在擷取用於產生文件之前,您需要將文件擷取至文件索引。一般的擷取流程會執行下列作業:

  1. 將大型文件分割成多個較小的文件,只用相關的部分來增強提示,也就是「區塊」。這是必要的,因為許多 LLM 的背景期間有限,因此難以在含有提示的情況下加入整份文件。

    Genkit 並未提供內建的區塊化程式庫,但有與 Genkit 相容的開放原始碼程式庫。

  2. 為每個區塊產生嵌入。視您使用的資料庫而定,您可能是透過嵌入產生模型明確執行此操作,也可以使用資料庫提供的嵌入產生器。

  3. 將文字區塊及其索引新增至資料庫。

您可能不常執行擷取流程,或者只有在使用穩定的資料來源時才會執行一次。另一方面,如果您要處理的資料會經常變更,就可能需要持續執行擷取流程 (例如在 Cloud Firestore 觸發條件中,每當文件更新時)。

嵌入程式

嵌入器是一種函式,可從文字、圖片、音訊等內容擷取內容,並建立數字向量,將原始內容的語意含義編碼。如前所述,系統在建立索引的過程中會使用嵌入程式,不過也可以獨立用於建立沒有索引的嵌入。

擷取工具

擷取器是一種概念,用來封裝與任何類型文件擷取相關的邏輯。最常見的擷取案例通常包括從向量商店擷取,但在 Genkit 中,擷取器可以是任何傳回資料的函式。

如要建立擷取器,您可以使用其中一個實作項目,或自行建立。

支援的索引器、擷取器和嵌入器

Genkit 透過其外掛程式系統提供索引工具和擷取工具支援。下列外掛程式已正式支援:

此外,Genkit 透過預先定義的程式碼範本支援下列向量儲存庫,而您可以根據資料庫設定和結構定義進行自訂:

嵌入模型支援可透過下列外掛程式提供:

外掛程式 模型
Google 生成式 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 文件集合,並儲存至本機向量資料庫。

這個 API 會使用 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 { 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 },
});