本指南将指导你设置并部署第一个 Cloudflare AI 应用。你将使用 Workers AI、Vectorize、D1 和 Cloudflare Workers 等工具构建功能齐全的 AI 驱动应用。
完成本教程后,你将构建一个 AI 工具,可存储信息并使用大型语言模型查询。这种模式称为检索增强生成(RAG),是通过组合 Cloudflare AI 工具包多个方面可以构建的有用项目。构建此应用无需 AI 工具使用经验。
- 注册 Cloudflare 账户 ↗。
- 安装
Node.js↗。
Node.js 版本管理器
使用 Volta ↗ 或 nvm ↗ 等 Node 版本管理器,以避免权限问题并切换 Node.js 版本。本指南后续将介绍的 Wrangler 需要 Node 版本 16.17.0 或更高。
你还需要访问 Vectorize。在本教程中,我们还将展示如何可选地与 Anthropic Claude ↗ 集成。为此需要 Anthropic API key ↗。
C3(create-cloudflare-cli)是一个命令行工具,旨在帮助你尽快设置 Worker 并部署到 Cloudflare。
打开终端窗口并运行 C3 创建 Worker 项目:
npm create cloudflare@latest -- rag-ai-tutorialyarn create cloudflare rag-ai-tutorialpnpm 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 创建了哪些文件?
wrangler.jsonc: Your Wrangler configuration file.index.js(in/src): A minimal'Hello World!'Worker written in ES module syntax.package.json: A minimal Node dependencies configuration file.package-lock.json: Refer tonpmdocumentation onpackage-lock.json↗.node_modules: Refer tonpmdocumentationnode_modules↗.
现在,进入新创建的目录:
cd rag-ai-tutorialWorkers 命令行接口 Wrangler 允许你创建、测试和部署 Workers 项目。C3 默认会在项目中安装 Wrangler。
创建第一个 Worker 后,在项目目录中运行 wrangler dev 命令以启动本地服务器进行开发。这让你可以在开发期间本地测试 Worker。
npx wrangler dev你现在可以访问 http://localhost:8787 ↗ 查看运行中的 Worker。对代码的任何更改都会触发重新构建,刷新页面将显示 Worker 的最新输出。
要开始使用 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."}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')"在开始创建笔记之前,我们将介绍 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 } });为扩展 Workers 函数以处理多个路由,我们将添加 Workers 路由库 hono。这允许我们创建向数据库添加笔记的新路由。使用 npm 安装 hono:
npm i honoyarn add honopnpm add honobun add 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,
},
]);
});
}
}工作流执行以下操作:
- 接受
text参数。 - 向 D1 的
notes表插入新行,并检索新行的id。 - 使用 LLM 绑定的
embeddings模型将text转换为向量。 - 将
id和vectorsupsert 到 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);
});为完成代码,可以更新根路径(/)以查询 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;如果处理较大文档,可以选择使用 Anthropic 的 Claude 模型 ↗,它们具有大 context window,非常适合 RAG 工作流。
首先,安装 @anthropic-ai/sdk 包:
npm i @anthropic-ai/sdkyarn add @anthropic-ai/sdkpnpm add @anthropic-ai/sdkbun add @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如果不再需要某条笔记,可以从数据库中删除。每次删除笔记时,还需要从 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);
});对于大段文本,建议拆分为较小块。这使 LLM 能更有效地收集相关上下文,而无需检索大段文本。
要实现此功能,我们将向项目添加新的 NPM 包 @langchain/textsplitters:
npm i @langchain/textsplittersyarn add @langchain/textsplitterspnpm add @langchain/textsplittersbun add @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 端点时,将被拆分为较小块,每个块由工作流处理。
如果未在第 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 ↗。
后续可尝试: