检索增强生成 (RAG)

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

什么是 RAG?

检索增强生成是一种技术,用于将外部 将信息来源转化为 LLM 的回答。务必能够做到 这是因为,虽然 LLM 通常在训练时 要实质性地使用 LLM,通常需要具备特定的领域知识(例如 例如,你可以使用 LLM 来回答客户的关于您的 公司产品)。

一种解决方案是使用更具体的数据对模型进行微调。不过, 无论是计算费用还是所需工作量 准备充足的训练数据。

相比之下,RAG 的工作原理是将外部数据源整合到 传递给模型的时间。例如,您可以想象 “张三和丽娜是什么样的关系?”可能会通过 在前面加上一些相关信息,从而得到如下提示:“Homer and Marge 的孩子的名字是 Bart、Lisa 和 Maggie。小白和老婆是怎么回事 给 Lisa?”

这样做具有很多优势:

  • 它更具成本效益,因为您无需重新训练模型。
  • 您可以持续更新数据源,LLM 会立即 对更新后的信息的使用。
  • 现在,你可以在 LLM 的回答中引用参考文献。

另一方面,使用 RAG 自然意味着需要更长的提示, 服务对发送的每个输入令牌收费。最后,您必须评估 降低应用的成本。

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

  • 索引器:将文档添加到“索引”。
  • 嵌入器:将文档转换为向量表示
  • 检索器:根据给定查询从“索引”中检索文档。

这些定义的目的很宽泛,因为 Genkit 对于 什么是“索引”以及从其中检索文档的确切方式。仅 Genkit 提供 Document 格式,其他所有内容均由检索器或 索引器实现提供程序。

索引器

索引负责采用一种方式来跟踪文档 您可以快速检索特定查询的相关文档。这是 通常是使用矢量数据库完成的,该数据库使用 称为嵌入的多维向量。文本嵌入(不透明) 表示一段文本表达的概念;它们是 使用特殊用途的机器学习模型来训练该模型。通过使用嵌入将文本编入索引, 数据库能够对概念上相关的文本进行聚类并检索文档 与新的文本字符串(查询)有关。

在检索文档以便生成之前,您需要 提取到文档索引中。典型的注入流程 以下:

  1. 将大型文档拆分为多个较小的文档,确保只与文档相关 部分用于增强您的提示,即“分块”。这是必要的 因为许多 LLM 的上下文窗口有限, 来生成包含整个文档的提示。

    Genkit 不提供内置分块库;然而,还有一些 与 Genkit 兼容的源代码库。

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

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

您可以不频繁运行提取流程,也可以只在工作的情况下运行一次 稳定的数据源另一方面,如果您处理的是数据 您可能会持续运行提取流程( (例如,在 Cloud Firestore 触发器中,每当文档更新时触发)。

嵌入器

嵌入器是一个函数,它接受内容(文本、图像、音频等)并创建数字向量,该向量对原始内容的语义含义进行编码。如上所述,在编入索引的过程中利用了嵌入器,不过,也可以独立使用嵌入器来创建没有索引的嵌入。

检索器

检索器是一个概念,它封装了与任何类型的文档相关的逻辑 检索。最常见的检索用例通常包括从 而在 Genkit 中,检索器可以是任何返回数据的函数。

要创建检索器,您可以使用所提供的某个实现或 创建您自己的模板。

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

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

此外,Genkit 通过预定义的 代码模板,您可以根据自己的数据库配置 schema:

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

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

定义 RAG 流

以下示例展示了如何注入餐厅菜单 PDF 文档集合 导入矢量数据库中,然后检索它们,以便在用于确定可用食品的流中使用。

安装依赖项

在此示例中,我们将使用 langchaingo 中的 textsplitter 库, ledongthuc/pdf PDF 解析库:

go get github.com/tmc/langchaingo/textsplitter
go get github.com/ledongthuc/pdf

定义索引器

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

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

创建索引器

// Import Genkit's file-based vector retriever, (Don't use in production.)
import "github.com/firebase/genkit/go/plugins/localvec"

// Vertex AI provides the text-embedding-004 embedder model.
import "github.com/firebase/genkit/go/plugins/vertexai"
ctx := context.Background()

err := vertexai.Init(ctx, &vertexai.Config{})
if err != nil {
    log.Fatal(err)
}
err = localvec.Init()
if err != nil {
    log.Fatal(err)
}

menuPDFIndexer, _, err := localvec.DefineIndexerAndRetriever(
    "menuQA",
    localvec.Config{
        Embedder: vertexai.Embedder("text-embedding-004"),
    },
)
if err != nil {
    log.Fatal(err)
}

创建分块配置

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

以下定义将分块函数配置为返回文档 200 个字符组成的片段,且由 20 个字符组成的文本块重叠。

splitter := textsplitter.NewRecursiveCharacter(
    textsplitter.WithChunkSize(200),
    textsplitter.WithChunkOverlap(20),
)

此库的更多分块选项可在 langchaingo 文档

定义索引器流

genkit.DefineFlow(
    "indexMenu",
    func(ctx context.Context, path string) (any, error) {
        // Extract plain text from the PDF. Wrap the logic in Run so it
        // appears as a step in your traces.
        pdfText, err := genkit.Run(ctx, "extract", func() (string, error) {
            return readPDF(path)
        })
        if err != nil {
            return nil, err
        }

        // Split the text into chunks. Wrap the logic in Run so it
        // appears as a step in your traces.
        docs, err := genkit.Run(ctx, "chunk", func() ([]*ai.Document, error) {
            chunks, err := splitter.SplitText(pdfText)
            if err != nil {
                return nil, err
            }

            var docs []*ai.Document
            for _, chunk := range chunks {
                docs = append(docs, ai.DocumentFromText(chunk, nil))
            }
            return docs, nil
        })
        if err != nil {
            return nil, err
        }

        // Add chunks to the index.
        err = menuPDFIndexer.Index(ctx, &ai.IndexerRequest{Documents: docs})
        return nil, err
    },
)
// Helper function to extract plain text from a PDF. Excerpted from
// https://github.com/ledongthuc/pdf
func readPDF(path string) (string, error) {
    f, r, err := pdf.Open(path)
    if f != nil {
        defer f.Close()
    }
    if err != nil {
        return "", err
    }

    reader, err := r.GetPlainText()
    if err != nil {
        return "", err
    }

    bytes, err := io.ReadAll(reader)
    if err != nil {
        return "", err
    }
    return string(bytes), nil
}

运行索引器流程

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

运行 indexMenu 流程后,矢量数据库将基于 文档,可在包含检索步骤的 Genkit 流程中使用。

定义检索流程

以下示例展示了如何在 RAG 流中使用检索器。点赞 索引器示例,此示例使用 Genkit 的文件级向量检索器, 不应该在生产环境中使用。

    ctx := context.Background()

    err := vertexai.Init(ctx, &vertexai.Config{})
    if err != nil {
        log.Fatal(err)
    }
    err = localvec.Init()
    if err != nil {
        log.Fatal(err)
    }

    model := vertexai.Model("gemini-1.5-pro")

    _, menuPdfRetriever, err := localvec.DefineIndexerAndRetriever(
        "menuQA",
        localvec.Config{
            Embedder: vertexai.Embedder("text-embedding-004"),
        },
    )
    if err != nil {
        log.Fatal(err)
    }

    genkit.DefineFlow(
        "menuQA",
        func(ctx context.Context, question string) (string, error) {
            // Retrieve text relevant to the user's question.
            docs, err := menuPdfRetriever.Retrieve(ctx, &ai.RetrieverRequest{
                Document: ai.DocumentFromText(question, nil),
            })
            if err != nil {
                return "", err
            }

            // Construct a system message containing the menu excerpts you just
            // retrieved.
            menuInfo := ai.NewSystemTextMessage("Here's the menu context:")
            for _, doc := range docs.Documents {
                menuInfo.Content = append(menuInfo.Content, doc.Content...)
            }

            // Call Generate, including the menu information in your prompt.
            resp, err := model.Generate(ctx, &ai.GenerateRequest{
                Messages: []*ai.Message{
                    ai.NewSystemTextMessage(`
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.`),
                    menuInfo,
                    ai.NewUserTextMessage(question),
                },
            }, nil)
            if err != nil {
                return "", err
            }

            return resp.Text()
        })

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

您也可以创建自己的检索器。如果您 在 Genkit 不支持的文档存储区中管理文档(例如: MySQL、Google 云端硬盘等)。Genkit SDK 提供了灵活的方法, 您需要提供用于提取文档的自定义代码。

您还可以定义基于现有检索器构建的自定义检索器 并运用高级 RAG 技术(例如重排或提示) )。

例如,假设您要使用自定义重排序函数。通过 以下示例定义了一个自定义检索器,该检索器将您的函数应用于 之前定义的菜单检索器:

type CustomMenuRetrieverOptions struct {
    K          int
    PreRerankK int
}
advancedMenuRetriever := ai.DefineRetriever(
    "custom",
    "advancedMenuRetriever",
    func(ctx context.Context, req *ai.RetrieverRequest) (*ai.RetrieverResponse, error) {
        // Handle options passed using our custom type.
        opts, _ := req.Options.(CustomMenuRetrieverOptions)
        // Set fields to default values when either the field was undefined
        // or when req.Options is not a CustomMenuRetrieverOptions.
        if opts.K == 0 {
            opts.K = 3
        }
        if opts.PreRerankK == 0 {
            opts.PreRerankK = 10
        }

        // Call the retriever as in the simple case.
        response, err := menuPDFRetriever.Retrieve(ctx, &ai.RetrieverRequest{
            Document: req.Document,
            Options:  localvec.RetrieverOptions{K: opts.PreRerankK},
        })
        if err != nil {
            return nil, err
        }

        // Re-rank the returned documents using your custom function.
        rerankedDocs := rerank(response.Documents)
        response.Documents = rerankedDocs[:opts.K]

        return response, nil
    },
)