このガイドでは、Cloudflare AI で最初のアプリケーションを設定してデプロイする手順を説明します。Workers AI、Vectorize、D1、Cloudflare Workers などのツールを使い、一通りの機能を備えた AI アプリを構築します。
このチュートリアルの終わりには、情報を保存し、大規模言語モデルで照会できる AI ツールができあがります。このパターンは Retrieval Augmented Generation(RAG)と呼ばれ、Cloudflare の AI ツールキットの複数の機能を組み合わせて作れる実用的なプロジェクトです。AI ツールの経験がなくても、このアプリケーションは構築できます。
- Cloudflare アカウント ↗ に登録します。
Node.js↗ をインストールします。
Node.js のバージョンマネージャー
権限の問題を避け、Node.js のバージョンを切り替えられるよう、Volta ↗ や nvm ↗ などの Node バージョンマネージャーを使います。このガイドの後半で説明する Wrangler には、Node バージョン 16.17.0 以降が必要です。
あわせて Vectorize へのアクセスも必要です。このチュートリアルでは、任意で Anthropic Claude ↗ と連携する方法も示します。連携する場合は Anthropic API キー ↗ が必要です。
C3(create-cloudflare-cli)は、Workers をできるだけ早くセットアップして 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:Wrangler の設定ファイルです。index.js(/src内):ES module 構文で書かれた最小の'Hello World!'Worker です。package.json:最小の Node 依存関係の設定ファイルです。package-lock.json:npm のpackage-lock.jsonドキュメント ↗ を参照してください。node_modules:npm のnode_modulesドキュメント ↗ を参照してください。
作成したディレクトリに移動します。
cd rag-ai-tutorialWorkers のコマンドラインインターフェースである Wrangler では、Workers プロジェクトの 作成、テスト、デプロイ ができます。C3 はデフォルトでプロジェクトに Wrangler をインストールします。
最初の Worker を作成したら、プロジェクトディレクトリで wrangler dev コマンドを実行し、開発用のローカルサーバーを起動します。開発中に Worker をローカルでテストできます。
npx wrangler devhttp://localhost:8787 ↗ を開くと、Worker が動いていることを確認できます。コードを変更すると再ビルドが走り、ページを再読み込みすると Worker の最新の出力が表示されます。
Cloudflare の AI プロダクトを使い始めるには、Wrangler 設定ファイル に ai ブロックを リモートバインディング として追加します。これで、プラットフォーム上の利用可能な AI モデルとやり取りするためのバインディングがコードに設定されます。
この例では、テキストを生成する @cf/meta/llama-3-8b-instruct モデル を使います。
{
"ai": {
"binding": "AI",
"remote": true
}
}[ai]
binding = "AI"
remote = true次に 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: `What is the square root of 9?` }],
});
return new Response(JSON.stringify(answer));
},
};AI バインディング経由で LLM を照会すると、コードから Cloudflare AI の大規模言語モデルと直接やり取りできます。この例では、テキストを生成する @cf/meta/llama-3-8b-instruct モデル を使っています。
wrangler で Worker をデプロイします。
npx wrangler deployWorker にリクエストすると、LLM がテキスト応答を生成し、JSON オブジェクトとして返します。
curl https://example.username.workers.dev{"response":"Answer: The square root of 9 is 3."}埋め込みを使うと、Cloudflare AI プロジェクトで使える言語モデルに機能を追加できます。これは Cloudflare のベクトルデータベースである Vectorize で行います。
Vectorize を使い始めるには、wrangler で新しい埋め込みインデックスを作成します。このインデックスは 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 を導入します。これで、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 に、WorkflowEntrypoint を継承する RAGWorkflow クラスを追加します。
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!" を出力するワークフローステップを 1 つ定義します。ワークフローには必要な数だけステップを追加できます。
このワークフロー単体では何も実行されません。ワークフローを実行するには、RAG_WORKFLOW バインディングを呼び出し、ワークフローが完了するために必要なパラメーターを渡します。呼び出し例は次のとおりです。
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 ハンドラーも 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;これで、ルートパス / に、以前のアプリケーションと機能的に同等のルートができます。
次に、ワークフローを更新し、データベースへノートを追加し、関連する埋め込みを生成するようにします。
この例では、埋め込みの作成に使える @cf/baai/bge-base-en-v1.5 モデル を使います。埋め込みは、Cloudflare のベクトルデータベースである Vectorize に保存・取得します。ユーザークエリも埋め込みに変換し、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とvectorsを Vectorize のvector-indexインデックスに upsert します。
これにより、あとでノートを取得できるベクトル表現が新しく作成されます。
最後に、ユーザーがデータベースへノートを送信できるルートを追加します。このルートは 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 に一致するノートを取得できます。この例ではノートを 1 件だけ取得しますが、必要に応じてカスタマイズできます。
それらのノートのテキストを、LLM バインディングのプロンプトにコンテキストとして挿入できます。これが Retrieval-Augmented Generation(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") || "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;大きな文書を扱う場合は、コンテキストウィンドウが大きく RAG ワークフローに向いている Anthropic の Claude モデル ↗ を使えます。
まず @anthropic-ai/sdk パッケージをインストールします。
npm i @anthropic-ai/sdkyarn add @anthropic-ai/sdkpnpm add @anthropic-ai/sdkbun add @anthropic-ai/sdksrc/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 ↗。
さらに進めるには、次を参照してください。