跳转到内容
搜索文档

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

最后更新 查看 MarkdownAgent 设置

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

完成本教程后,你将构建一个 AI 工具,可存储信息并使用大型语言模型查询。这种模式称为检索增强生成(RAG),是通过组合 Cloudflare AI 工具包多个方面可以构建的有用项目。构建此应用无需 AI 工具使用经验。

  1. 注册 Cloudflare 账户
  2. 安装 Node.js

Node.js 版本管理器

使用 Voltanvm 等 Node 版本管理器,以避免权限问题并切换 Node.js 版本。本指南后续将介绍的 Wrangler 需要 Node 版本 16.17.0 或更高。

你还需要访问 Vectorize。在本教程中,我们还将展示如何可选地与 Anthropic Claude 集成。为此需要 Anthropic API key

1. 创建新的 Worker 项目

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

打开终端窗口并运行 C3 创建 Worker 项目:

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

进行设置时,请选择以下选项:

  • 对于 What would you like to start with?,选择 Hello World example
  • 对于 Which template would you like to use?,选择 Worker only
  • 对于 Which language do you want to use?,选择 JavaScript
  • 对于 Do you want to use git for version control?,选择 Yes
  • 对于 Do you want to deploy your application?,选择 No(部署前我们还会做一些修改)。

在项目目录中,C3 已生成多个文件。

C3 创建了哪些文件?

  1. wrangler.jsonc: Your Wrangler configuration file.
  2. index.js (in /src): A minimal 'Hello World!' Worker written in ES module syntax.
  3. package.json: A minimal Node dependencies configuration file.
  4. package-lock.json: Refer to npm documentation on package-lock.json.
  5. node_modules: Refer to npm documentation node_modules.

现在,进入新创建的目录:

cd rag-ai-tutorial

2. 使用 Wrangler CLI 开发

Workers 命令行接口 Wrangler 允许你创建测试部署 Workers 项目。C3 默认会在项目中安装 Wrangler。

创建第一个 Worker 后,在项目目录中运行 wrangler dev 命令以启动本地服务器进行开发。这让你可以在开发期间本地测试 Worker。

npx wrangler dev

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

3. 添加 AI 绑定(binding)

要开始使用 Cloudflare 的 AI 产品,可以在 Wrangler 配置文件 中添加 ai 块作为远程绑定(binding)。这将在代码中设置与 Cloudflare AI 模型的绑定,可用于与平台上可用的 AI 模型交互。

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

{
	"ai": {
		"binding": "AI",
		"remote": true
	}
}
[ai]
binding = "AI"
remote = true

现在,找到 src/index.js 文件。在 fetch handler 中,可以查询 AI 绑定(binding):

export default {
	async fetch(request, env, ctx) {
		const answer = await env.AI.run("@cf/meta/llama-3-8b-instruct", {
			messages: [{ role: "user", content: `What is the square root of 9?` }],
		});

		return new Response(JSON.stringify(answer));
	},
};

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

使用 wrangler 部署 Worker:

npx wrangler deploy

向 Worker 发起请求现在会从 LLM 生成文本响应,并以 JSON 对象返回。

curl https://example.username.workers.dev
{"response":"Answer: The square root of 9 is 3."}

4. 使用 Cloudflare D1 和 Vectorize 添加 embedding

Embedding 让你为 Cloudflare AI 项目中使用的语言模型添加额外能力。这通过 **Vectorize(Cloudflare 的向量数据库)**实现。

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

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

然后,将新 Vectorize 索引的配置详情添加到 Wrangler 配置文件

{
	// ... existing wrangler configuration
	"vectorize": [
		{
			"binding": "VECTOR_INDEX",
			"index_name": "vector-index"
		}
	]
}
[[vectorize]]
binding = "VECTOR_INDEX"
index_name = "vector-index"

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

要实现搜索功能,必须设置 Cloudflare 的 D1 数据库。在 D1 中,你可以存储应用数据。然后将数据转换为向量格式。当用户搜索且与向量匹配时,可以显示匹配的数据。

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

npx wrangler d1 create database

然后,将上一条命令输出的配置详情粘贴到 Wrangler 配置文件

{
	// ... existing wrangler configuration
	"d1_databases": [
		{
			"binding": "DB", // available in your Worker on env.DB
			"database_name": "database",
			"database_id": "abc-def-geh" // replace this with a real database_id (UUID)
		}
	]
}
[[d1_databases]]
binding = "DB"
database_name = "database"
database_id = "abc-def-geh"

在本应用中,我们将在 D1 中创建 notes 表,用于存储笔记并在 Vectorize 中检索。要创建此表,使用 wrangler d1 execute 运行 SQL 命令:

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

现在,可以使用 wrangler d1 execute 向数据库添加新笔记:

npx wrangler d1 execute database --remote --command "INSERT INTO notes (text) VALUES ('The best pizza topping is pepperoni')"

5. 创建工作流

在开始创建笔记之前,我们将介绍 Cloudflare Workflow。这允许我们定义 durable 工作流,安全稳健地执行 RAG 流程的所有步骤。

首先,在 Wrangler 配置文件 中添加新的 [[workflows]] 块:

{
	// ... existing wrangler configuration
	"workflows": [
		{
			"name": "rag",
			"binding": "RAG_WORKFLOW",
			"class_name": "RAGWorkflow"
		}
	]
}
[[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 绑定(binding),传入工作流正确完成所需的参数。以下是调用工作流的示例:

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

6. 创建笔记并添加到 Vectorize

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

npm i hono

然后,将 hono 导入 src/index.js 文件。还应更新 fetch handler 以使用 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: `What is the square root of 9?` }],
	});

	return c.json(answer);
});

export default app;

这将在根路径 / 建立与应用先前版本功能等价的路由。

现在,我们可以更新工作流,开始向数据库添加笔记并为其生成相关 embedding。

此示例使用 @cf/baai/bge-base-en-v1.5 模型 创建 embedding。Embedding 在 Vectorize(Cloudflare 的向量数据库)中存储和检索。用户查询也会转换为 embedding,以便在 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 upsert 到 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 绑定的 prompt。这是检索增强生成(RAG)的基础:提供 LLM 外部数据的额外上下文,以增强 LLM 生成的文本。

我们将更新 prompt 以包含上下文,并要求 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") || "What is the square root of 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).run();
		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 模型,它们具有大 context window,非常适合 RAG 工作流。

首先,安装 @anthropic-ai/sdk 包:

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 = ""
	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 完成:

$ 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

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 会在发布过程中提示你设置。

npx wrangler deploy

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

相关资源

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

后续可尝试:

这篇文档对您有帮助吗?