检索增强生成 (RAG)

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

什么是 RAG?

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

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

相比之下,RAG 的工作原理是,在将外部数据源传递给模型时将其整合到提示中。例如,您可以想象一下,通过在前面添加一些相关信息,可以对提示“What is Bart's relationship to Lisa?”这一提示进行扩展(“增强”),从而生成提示“Homer and Marge's children are named Bart, Lisa, and Maggie.What is Bart's relationship to Lisa?”

这样做具有很多优势:

  • 这可能更具成本效益,因为您无需重新训练模型。
  • 您可以持续更新数据源,并且 LLM 可以立即利用更新后的信息。
  • 现在,您可以在 LLM 的回答中引用参考信息。

另一方面,使用 RAG 本质上意味着较长的提示,并且某些 LLM API 服务会针对您发送的每个输入词元收费。归根结底,您必须评估应用的费用权衡。

RAG 是一个非常广泛的领域,有许多不同的技术可用于实现最佳质量的 RAG。核心 Genkit 框架提供了三个主要抽象来帮助您执行 RAG:

  • 索引器:将文档添加到“索引”中。
  • 嵌入器:将文档转换为矢量表示
  • 检索器:在给定查询的情况下,通过“索引”检索文档。

这些定义本来就很宽泛,因为 Genkit 对“索引”是什么或者如何从其中检索文档没有确切的看法。Genkit 仅提供 Document 格式,而其他所有内容均由检索器或索引器实现提供程序定义。

索引器

索引负责跟踪您的文档,以便您根据特定查询快速检索相关文档。这通常是通过矢量数据库来实现的,矢量数据库使用称为嵌入的多维矢量将文档编入索引。文本嵌入(不透明)表示由一段文本表示的概念;这些概念是使用特殊用途的机器学习模型生成的。通过使用嵌入将文本编入索引,矢量数据库能够对概念相关的文本进行聚类,并检索与新型文本字符串(查询)相关的文档。

您需要先将文档注入到文档索引中,然后才能检索文档以进行生成。典型的注入 flow 会执行以下操作:

  1. 将大型文档拆分成较小的文档,以便仅使用相关部分来增强提示,即“分块”。这一点很有必要,因为许多 LLM 的上下文窗口有限,因此在提示中包含整个文档是不切实际的。

    Genkit 不提供内置分块库;不过,有可用的开源库与 Genkit 兼容。

  2. 为每个分块生成嵌入。根据您使用的数据库,您可以使用嵌入生成模型明确执行此操作,也可以使用数据库提供的嵌入生成器。

  3. 将文本块及其索引添加到数据库中。

如果您处理的是稳定的数据源,则可以不经常或仅运行一次注入 flow。另一方面,如果您处理的是经常更改的数据,则可以持续运行注入 flow(例如,在 Cloud Firestore 触发器中,每当文档更新时)。

嵌入器

嵌入器是一种函数,它会接受内容(文本、图片、音频等)并创建一个数字矢量,以对原始内容的语义含义进行编码。如上所述,嵌入器会作为索引编入过程的一部分来使用,但也可以独立使用来创建没有索引的嵌入。

检索器

检索器是一种概念,用于封装与任何类型的文档检索相关的逻辑。最常见的检索情况通常包括从矢量存储区检索,但在 Genkit 中,检索器可以是任何返回数据的函数。

如需创建检索器,您可以使用提供的某个实现,也可以创建自己的实现。

支持的索引器、检索器和嵌入器

Genkit 通过其插件系统提供索引器和检索器支持。以下插件受官方支持:

此外,Genkit 还通过预定义的代码模板支持以下矢量存储区,您可以根据自己的数据库配置和架构对其进行自定义:

  • 将 PostgreSQL 与 pgvector 搭配使用

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

插件 模型
Google 生成式 AI Gecko 文本嵌入
Google Vertex AI Gecko 文本嵌入

定义 RAG Flow

以下示例展示了如何将餐厅菜单 PDF 文档集合注入到矢量数据库中,并检索这些文档,以便在确定可提供食材的流程中使用。

安装用于处理 PDF 文件的依赖项

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

将本地矢量存储区添加到配置中

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

定义索引器

以下示例展示了如何创建索引器来提取 PDF 文档集合并将其存储在本地矢量数据库中。

它使用 Genkit 提供开箱即用的基于本地文件的矢量相似性检索器,用于简单的测试和原型设计(请勿在生产环境中使用

创建索引器

export const menuPdfIndexer = devLocalIndexerRef('menuQA');

创建分块配置

此示例使用 llm-chunk 库,该库提供了一个简单的文本拆分器,用于将文档拆分为可矢量化的片段。

以下定义会将分块函数配置为保证文档段在 1000 到 2000 个字符之间,在句子末尾断开,并且分块之间有 100 个字符的重叠。

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

如需了解此库的更多分块选项,请参阅 llm-chunk 文档

定义索引器 flow

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

运行索引器 flow

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

运行 indexMenu flow 后,矢量数据库将添加文档种子,并准备好在包含检索步骤的 Genkit flow 中使用。

定义包含检索的 flow

以下示例展示了如何在 RAG flow 中使用检索器。与索引器示例一样,此示例使用 Genkit 的基于文件的矢量检索器,您不应在生产环境中使用此检索器。

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

编写您自己的索引器和检索器

您还可以创建自己的检索器。如果您的文档在 Genkit 不支持的文档存储区(例如 MySQL、Google 云端硬盘等)中进行管理,这会非常有用。Genkit SDK 提供了灵活的方法,可让您提供用于提取文档的自定义代码。您还可以定义基于 Genkit 中的现有检索器构建的自定义检索器,并在其基础上应用高级 RAG 技术(例如重排序或提示扩展)。

简单检索器

借助简单的检索器,您可以轻松将现有代码转换为检索器:

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

自定义检索器

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

extendPromptrerank 是您必须自行实现的,框架不提供)

然后,您只需更换检索器即可:

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

重新排名器和两阶段检索

重新排名模型(也称为交叉编码器)是一种模型,在给定查询和文档的情况下,会输出相似度得分。我们会根据此得分对文档进行重新排序,以便按与查询的相关性对文档进行排序。重新排名器 API 会接受文档列表(例如检索器的输出),并根据文档与查询的相关性对文档进行重新排序。此步骤对于微调结果和确保在向生成式模型提供的提示中使用最相关的信息非常有用。

重新排名器示例

Genkit 中的重新排名器的定义语法与检索器和索引器类似。以下是在 Genkit 中使用重新排名器的示例。此流程使用预定义的 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,
    }));
  }
);

此重新排名器将 Vertex AI genkit 插件与 semantic-ranker-512 搭配使用,以对文档进行评分和排名。得分越高,文档与查询的相关性就越高。

自定义重新排名器

您还可以定义自定义重新排名器,以适应您的特定用例。当您需要使用自己的自定义逻辑或自定义模型重新对文档进行排名时,这会很有用。以下是一个定义自定义重新排名器的简单示例:

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

定义完成后,此自定义重新排名器的使用方式与 RAG 流程中的任何其他重新排名器一样,让您可以灵活地实现高级重新排名策略。