检索增强生成 (RAG)

Firebase Genkit 提供了可帮助您构建检索增强生成 (RAG) 流程的抽象,以及可提供与相关工具集成的插件。

什么是 RAG?

检索增强生成是一种技术,用于将外部信息来源整合到 LLM 的响应中。能做到这一点很重要,因为虽然 LLM 通常要基于广泛的材料进行训练,但实际使用 LLM 通常需要特定的领域知识(例如,您可能希望使用 LLM 回答客户关于贵公司产品的问题)。

一种解决方案是使用更具体的数据微调模型。但是,就计算费用和准备充足训练数据所需的工作量而言,这可能都很昂贵。

相比之下,RAG 的工作原理是在将外部数据源传递到模型时将外部数据源合并到提示中。例如,您可以想象一下,通过在前面追加一些相关信息,可以对“Bart 与 Lisa 的关系是什么?”这一提示进行扩展(“增强”),从而产生“Homer 和 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 通过预定义的代码模板支持以下矢量存储区,您可以根据自己的数据库配置和架构对其进行自定义:

  • 将 PostgreSQL 与 pgvector 搭配使用

嵌入模型支持通过以下插件提供:

插件 模型
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 文档集合并将其存储在本地矢量数据库中。

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