Skip to content
Cloudflare Docs
非官方翻译 - 此文档为非官方中文翻译版本,仅供参考。如有疑问请以 英文官方文档 为准。

构建检索增强生成 (RAG) AI

Last reviewed: 7 months ago

本指南将指导您设置和部署您的第一个 Cloudflare AI 应用程序。您将使用 Workers AI、Vectorize、D1 和 Cloudflare Workers 等工具构建一个功能齐全的 AI 驱动的应用程序。

在本教程结束时,您将构建一个 AI 工具,允许您存储信息并使用大型语言模型进行查询。这种模式被称为检索增强生成(RAG),是您可以结合 Cloudflare AI 工具包的多个方面构建的一个有用的项目。您无需具备使用 AI 工具的经验即可构建此应用程序。

  1. Sign up for a Cloudflare account.
  2. Install Node.js.

Node.js version manager

Use a Node version manager like Volta or nvm to avoid permission issues and change Node.js versions. Wrangler, discussed later in this guide, requires a Node version of 16.17.0 or later.

您还需要访问 Vectorize。在本教程中,我们将展示如何选择性地与 Anthropic Claude 集成。您需要一个 Anthropic API 密钥 才能这样做。

1. 创建一个新的 Worker 项目

C3 (create-cloudflare-cli) 是一个命令行工具,旨在帮助您尽快设置和部署 Workers 到 Cloudflare。

打开一个终端窗口并运行 C3 来创建您的 Worker 项目:

Terminal window
npm create cloudflare@latest -- rag-ai-tutorial

For setup, select the following options:

  • For What would you like to start with?, choose Hello World example.
  • For Which template would you like to use?, choose Worker only.
  • For Which language do you want to use?, choose JavaScript.
  • For Do you want to use git for version control?, choose Yes.
  • For Do you want to deploy your application?, choose No (we will be making some changes before deploying).

在您的项目目录中,C3 生成了几个文件。

C3 创建了哪些文件?

  1. wrangler.jsonc: 您的 Wrangler 配置文件。
  2. worker.js (在 /src 中): 一个用 ES 模块 语法编写的最小化 'Hello World!' Worker。
  3. package.json: 一个最小化的 Node 依赖项配置文件。
  4. package-lock.json: 请参阅 npm 关于 package-lock.json 的文档
  5. node_modules: 请参阅 npm 关于 node_modules 的文档

现在,移动到您新创建的目录中:

Terminal window
cd rag-ai-tutorial

2. 使用 Wrangler CLI 进行开发

Workers 命令行界面 Wrangler 允许您 创建测试部署 您的 Workers 项目。C3 将默认在项目中安装 Wrangler。

创建您的第一个 Worker 后,在项目目录中运行 wrangler dev 命令以启动本地服务器来开发您的 Worker。这将允许您在开发过程中本地测试您的 Worker。

Terminal window
npx wrangler dev --remote

您现在可以访问 http://localhost:8787 来查看您的 Worker 正在运行。您对代码的任何更改都将触发重新构建,重新加载页面将显示您的 Worker 的最新输出。

3. 添加 AI 绑定

要开始使用 Cloudflare 的 AI 产品,您可以将 ai 块添加到 Wrangler 配置文件 中。这将在您的代码中设置一个到 Cloudflare AI 模型的绑定,您可以使用它与平台上的可用 AI 模型进行交互。

此示例使用了 @cf/meta/llama-3-8b-instruct 模型,该模型可以生成文本。

{
"ai": {
"binding": "AI"
}
}

现在,找到 src/index.js 文件。在 fetch 处理程序中,您可以查询 AI 绑定:

export default {
async fetch(request, env, ctx) {
const answer = await env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [{ role: "user", content: `9 的平方根是多少?` }],
});
return new Response(JSON.stringify(answer));
},
};

通过 AI 绑定查询 LLM,我们可以直接在代码中与 Cloudflare AI 的大型语言模型进行交互。在此示例中,我们使用的是 @cf/meta/llama-3-8b-instruct 模型,该模型可以生成文本。

您可以使用 wrangler 部署您的 Worker:

Terminal window
npx wrangler deploy

向您的 Worker 发出请求现在将从 LLM 生成文本响应,并将其作为 JSON 对象返回。

Terminal window
curl https://example.username.workers.dev
{"response":"答案:9的平方根是3。"}

4. 使用 Cloudflare D1 和 Vectorize 添加嵌入

嵌入允许您向 Cloudflare AI 项目中使用的语言模型添加附加功能。这是通过 Vectorize(Cloudflare 的向量数据库)完成的。

要开始使用 Vectorize,请使用 wrangler 创建一个新的嵌入索引。此索引将存储具有 768 个维度的向量,并将使用余弦相似度来确定哪些向量彼此最相似:

Terminal window
npx wrangler vectorize create vector-index --dimensions=768 --metric=cosine

然后,将新 Vectorize 索引的配置详细信息添加到 Wrangler 配置文件中:

{
"vectorize": [
{
"binding": "VECTOR_INDEX",
"index_name": "vector-index"
}
]
}

向量索引允许您存储维度集合,维度是用于表示数据的浮点数。当您要查询向量数据库时,您也可以将查询转换为维度。Vectorize 旨在高效地确定哪些存储的向量与您的查询最相似。

要实现搜索功能,您必须设置一个 Cloudflare 的 D1 数据库。在 D1 中,您可以存储应用程序的数据。然后,您将此数据更改为向量格式。当有人搜索并与向量匹配时,您可以向他们显示匹配的数据。

使用 wrangler 创建一个新的 D1 数据库:

Terminal window
npx wrangler d1 create database

然后,将上一个命令输出的配置详细信息粘贴到 Wrangler 配置文件 中:

{
"d1_databases": [
{
"binding": "DB",
"database_name": "database",
"database_id": "abc-def-geh"
}
]
}

在此应用程序中,我们将在 D1 中创建一个 notes 表,这将允许我们存储笔记并稍后在 Vectorize 中检索它们。要创建此表,请使用 wrangler d1 execute 运行一个 SQL 命令:

Terminal window
npx wrangler d1 execute database --remote --command "CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, text TEXT NOT NULL)"

现在,我们可以使用 wrangler d1 execute 向我们的数据库中添加一个新笔记:

Terminal window
npx wrangler d1 execute database --remote --command "INSERT INTO notes (text) VALUES ('最好的披萨配料是意大利辣香肠')"

5. 创建工作流

在我们开始创建笔记之前,我们将引入一个 Cloudflare 工作流。这将允许我们定义一个持久的工作流,可以安全、稳健地执行 RAG 过程的所有步骤。

首先,将一个新的 [[workflows]] 块添加到您的 Wrangler 配置文件 中:

{
"workflows": [
{
"name": "rag",
"binding": "RAG_WORKFLOW",
"class_name": "RAGWorkflow"
}
]
}

src/index.js 中,添加一个名为 RAGWorkflow 的新类,它扩展了 WorkflowEntrypoint

import { WorkflowEntrypoint } from "cloudflare:workers";
export class RAGWorkflow extends WorkflowEntrypoint {
async run(event, step) {
await step.do("example step", async () => {
console.log("Hello World!");
});
}
}

此类将定义一个工作流步骤,该步骤将在控制台中记录“Hello World!”。您可以根据需要向工作流中添加任意数量的步骤。

就其本身而言,此工作流不会执行任何操作。要执行工作流,我们将调用 RAG_WORKFLOW 绑定,并传入工作流正常完成所需的任何参数。以下是我们如何调用工作流的示例:

env.RAG_WORKFLOW.create({ params: { text } });

6. 创建笔记并将其添加到 Vectorize

为了扩展您的 Workers 函数以处理多个路由,我们将添加 hono,这是一个用于 Workers 的路由库。这将允许我们为向数据库中添加笔记创建一个新路由。使用 npm 安装 hono

Terminal window
npm i hono

然后,将 hono 导入您的 src/index.js 文件中。您还应该更新 fetch 处理程序以使用 hono

import { Hono } from "hono";
const app = new Hono();
app.get("/", async (c) => {
const answer = await c.env.AI.run("@cf/meta/llama-3-8b-instruct", {
messages: [{ role: "user", content: `9 的平方根是多少?` }],
});
return c.json(answer);
});
export default app;

这将在根路径 / 处建立一个路由,其功能与先前版本的应用程序相同。

现在,我们可以更新工作流以开始将笔记添加到数据库中,并生成它们的相关嵌入。

此示例使用了 @cf/baai/bge-base-en-v1.5 模型,该模型可用于创建嵌入。嵌入存储在 Vectorize 中,这是 Cloudflare 的向量数据库。用户查询也会转换为嵌入,以便在 Vectorize 中进行搜索。

import { WorkflowEntrypoint } from "cloudflare:workers";
export class RAGWorkflow extends WorkflowEntrypoint {
async run(event, step) {
const env = this.env;
const { text } = event.payload;
const record = await step.do(`create database record`, async () => {
const query = "INSERT INTO notes (text) VALUES (?) RETURNING *";
const { results } = await env.DB.prepare(query).bind(text).run();
const record = results[0];
if (!record) throw new Error("Failed to create note");
return record;
});
const embedding = await step.do(`generate embedding`, async () => {
const embeddings = await env.AI.run("@cf/baai/bge-base-en-v1.5", {
text: text,
});
const values = embeddings.data[0];
if (!values) throw new Error("Failed to generate vector embedding");
return values;
});
await step.do(`insert vector`, async () => {
return env.VECTOR_INDEX.upsert([
{
id: record.id.toString(),
values: embedding,
},
]);
});
}
}

工作流执行以下操作:

  1. 接受一个 text 参数。
  2. 在 D1 的 notes 表中插入一个新行,并检索新行的 id
  3. 使用 LLM 绑定的 embeddings 模型将 text 转换为向量。
  4. idvectors 上传到 Vectorize 中的 vector-index 索引。

通过这样做,您将创建一个新的向量表示形式的笔记,可以用于稍后检索该笔记。

要完成代码,我们将添加一个路由,允许用户向数据库提交笔记。此路由将解析 JSON 请求正文,获取 note 参数,并创建一个新的工作流实例,传递参数:

app.post("/notes", async (c) => {
const { text } = await c.req.json();
if (!text) return c.text("Missing text", 400);
await c.env.RAG_WORKFLOW.create({ params: { text } });
return c.text("Created note", 201);
});

7. 查询 Vectorize 以检索笔记

要完成您的代码,您可以更新根路径(/)以查询 Vectorize。您将把查询转换为向量,然后使用 vector-index 索引来查找最相似的向量。

topK 参数限制了函数返回的向量数量。例如,提供 topK 为 1 将仅返回基于查询的 最相似 向量。将 topK 设置为 5 将返回 5 个最相似的向量。

给定一组相似的向量,您可以检索与存储在这些向量旁边的记录 ID 匹配的笔记。在这种情况下,我们只检索一个笔记 - 但您可以根据需要自定义此设置。

您可以将这些笔记的文本插入 LLM 绑定的提示中。这是检索增强生成(RAG)的基础:在 LLM 的提示中提供来自数据外部的附加上下文,以增强 LLM 生成的文本。

我们将更新提示以包含上下文,并要求 LLM 在回应时使用上下文:

import { Hono } from "hono";
const app = new Hono();
// Existing post route...
// app.post('/notes', async (c) => { ... })
app.get("/", async (c) => {
const question = c.req.query("text") || "9 的平方根是多少?";
const embeddings = await c.env.AI.run("@cf/baai/bge-base-en-v1.5", {
text: question,
});
const vectors = embeddings.data[0];
const vectorQuery = await c.env.VECTOR_INDEX.query(vectors, { topK: 1 });
let vecId;
if (
vectorQuery.matches &&
vectorQuery.matches.length > 0 &&
vectorQuery.matches[0]
) {
vecId = vectorQuery.matches[0].id;
} else {
console.log("No matching vector found or vectorQuery.matches is empty");
}
let notes = [];
if (vecId) {
const query = `SELECT * FROM notes WHERE id = ?`;
const { results } = await c.env.DB.prepare(query).bind(vecId).all();
if (results) notes = results.map((vec) => vec.text);
}
const contextMessage = notes.length
? `Context:\n${notes.map((note) => `- ${note}`).join("\n")}`
: "";
const systemPrompt = `When answering the question or responding, use the context provided, if it is provided and relevant.`;
const { response: answer } = await c.env.AI.run(
"@cf/meta/llama-3-8b-instruct",
{
messages: [
...(notes.length ? [{ role: "system", content: contextMessage }] : []),
{ role: "system", content: systemPrompt },
{ role: "user", content: question },
],
},
);
return c.text(answer);
});
app.onError((err, c) => {
return c.text(err);
});
export default app;

8. 添加 Anthropic Claude 模型(可选)

如果您正在处理较大的文档,您有选择使用 Anthropic 的 Claude 模型,这些模型具有大型上下文窗口,非常适合 RAG 工作流。

要开始,安装 @anthropic-ai/sdk 包:

Terminal window
npm i @anthropic-ai/sdk

src/index.js 中,您可以更新 GET / 路由以检查 ANTHROPIC_API_KEY 环境变量。如果设置了该变量,我们可以使用 Anthropic SDK 生成文本。如果没有设置,我们将回退到现有的 Workers AI 代码:

import Anthropic from '@anthropic-ai/sdk';
app.get('/', async (c) => {
// ... Existing code
const systemPrompt = `When answering the question or responding, use the context provided, if it is provided and relevant.`
let modelUsed: string = ""
let response = null
if (c.env.ANTHROPIC_API_KEY) {
const anthropic = new Anthropic({
apiKey: c.env.ANTHROPIC_API_KEY
})
const model = "claude-3-5-sonnet-latest"
modelUsed = model
const message = await anthropic.messages.create({
max_tokens: 1024,
model,
messages: [
{ role: 'user', content: question }
],
system: [systemPrompt, notes ? contextMessage : ''].join(" ")
})
response = {
response: message.content.map(content => content.text).join("\n")
}
} else {
const model = "@cf/meta/llama-3.1-8b-instruct"
modelUsed = model
response = await c.env.AI.run(
model,
{
messages: [
...(notes.length ? [{ role: 'system', content: contextMessage }] : []),
{ role: 'system', content: systemPrompt },
{ role: 'user', content: question }
]
}
)
}
if (response) {
c.header('x-model-used', modelUsed)
return c.text(response.response)
} else {
return c.text("We were unable to generate output", 500)
}
})

最后,您需要在 Workers 应用程序中设置 ANTHROPIC_API_KEY 环境变量。您可以使用 wrangler secret put 来实现:

Terminal window
$ npx wrangler secret put ANTHROPIC_API_KEY

9. 删除笔记和向量

如果您不再需要笔记,可以从数据库中删除它。每次删除笔记时,您还需要从 Vectorize 中删除相应的向量。您可以通过在 src/index.js 文件中构建 DELETE /notes/:id 路由来实现这一点:

app.delete("/notes/:id", async (c) => {
const { id } = c.req.param();
const query = `DELETE FROM notes WHERE id = ?`;
await c.env.DB.prepare(query).bind(id).run();
await c.env.VECTOR_INDEX.deleteByIds([id]);
return c.status(204);
});

10. 文本分割(可选)

对于较大的文本块,建议将文本分割成较小的块。这允许 LLM 更有效地收集相关上下文,而无需检索大块文本。

为了实现这一点,我们将向项目中添加一个新的 NPM 包,@langchain/textsplitters

Terminal window
npm i @langchain/textsplitters

此包提供的 RecursiveCharacterTextSplitter 类将文本分割成较小的块。它可以根据需要进行自定义,但默认配置在大多数情况下都有效:

import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
const text = "Some long piece of text...";
const splitter = new RecursiveCharacterTextSplitter({
// These can be customized to change the chunking size
// chunkSize: 1000,
// chunkOverlap: 200,
});
const output = await splitter.createDocuments([text]);
console.log(output); // [{ pageContent: 'Some long piece of text...' }]

要使用此分割器,我们将更新工作流以将文本分割成较小的块。然后,我们将遍历这些块,并为每个文本块运行工作流的其余部分:

export class RAGWorkflow extends WorkflowEntrypoint {
async run(event, step) {
const env = this.env;
const { text } = event.payload;
let texts = await step.do("split text", async () => {
const splitter = new RecursiveCharacterTextSplitter();
const output = await splitter.createDocuments([text]);
return output.map((doc) => doc.pageContent);
});
console.log(
"RecursiveCharacterTextSplitter generated ${texts.length} chunks",
);
for (const index in texts) {
const text = texts[index];
const record = await step.do(
`create database record: ${index}/${texts.length}`,
async () => {
const query = "INSERT INTO notes (text) VALUES (?) RETURNING *";
const { results } = await env.DB.prepare(query).bind(text).run();
const record = results[0];
if (!record) throw new Error("Failed to create note");
return record;
},
);
const embedding = await step.do(
`generate embedding: ${index}/${texts.length}`,
async () => {
const embeddings = await env.AI.run("@cf/baai/bge-base-en-v1.5", {
text: text,
});
const values = embeddings.data[0];
if (!values) throw new Error("Failed to generate vector embedding");
return values;
},
);
await step.do(`insert vector: ${index}/${texts.length}`, async () => {
return env.VECTOR_INDEX.upsert([
{
id: record.id.toString(),
values: embedding,
},
]);
});
}
}
}

现在,当向 /notes 端点提交大块文本时,它们将被分割成较小的块,并且每个块将由工作流处理。

11. 部署您的项目

如果您在第 1 步中没有部署您的 Worker,请使用 Wrangler 将您的 Worker 部署到 *.workers.dev 子域、自定义域(如果您已配置),或者如果您没有配置任何子域或域,Wrangler 将在发布过程中提示您设置一个。

Terminal window
npx wrangler deploy

<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev 预览您的 Worker。

相关资源

完整版本的此代码库可在 GitHub 上找到。它包括一个前端 UI 用于查询、添加和删除笔记,以及一个后端 API 用于与数据库和向量索引进行交互。您可以在这里找到它:github.com/kristianfreeman/cloudflare-retrieval-augmented-generation-example

要做更多: