Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

Retrieval Augmented Generation(RAG)AI を構築する

最終更新 Markdown で表示Agent セットアップ

このガイドでは、Cloudflare AI で最初のアプリケーションを設定してデプロイする手順を説明します。Workers AI、Vectorize、D1、Cloudflare Workers などのツールを使い、一通りの機能を備えた AI アプリを構築します。

このチュートリアルの終わりには、情報を保存し、大規模言語モデルで照会できる AI ツールができあがります。このパターンは Retrieval Augmented Generation(RAG)と呼ばれ、Cloudflare の AI ツールキットの複数の機能を組み合わせて作れる実用的なプロジェクトです。AI ツールの経験がなくても、このアプリケーションは構築できます。

  1. Cloudflare アカウント に登録します。
  2. Node.js をインストールします。

Node.js のバージョンマネージャー

権限の問題を避け、Node.js のバージョンを切り替えられるよう、Voltanvm などの Node バージョンマネージャーを使います。このガイドの後半で説明する Wrangler には、Node バージョン 16.17.0 以降が必要です。

あわせて Vectorize へのアクセスも必要です。このチュートリアルでは、任意で Anthropic Claude と連携する方法も示します。連携する場合は Anthropic API キー が必要です。

1. 新しい Worker プロジェクトを作成する

C3(create-cloudflare-cli)は、Workers をできるだけ早くセットアップして 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.jsoncWrangler の設定ファイルです。
  2. index.js/src 内):ES module 構文で書かれた最小の 'Hello World!' Worker です。
  3. package.json:最小の Node 依存関係の設定ファイルです。
  4. package-lock.jsonnpm の package-lock.json ドキュメント を参照してください。
  5. node_modulesnpm の 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 バインディングを追加する

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 deploy

Worker にリクエストすると、LLM がテキスト応答を生成し、JSON オブジェクトとして返します。

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

4. Cloudflare D1 と Vectorize で埋め込みを追加する

埋め込みを使うと、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')"

5. ワークフローを作成する

ノートを作成する前に、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 } });

6. ノートを作成して Vectorize に追加する

複数のルートを扱えるように Workers 関数を広げるため、Workers 向けのルーティングライブラリ hono を追加します。これで、データベースにノートを追加する新しいルートを作れます。npmhono をインストールします。

npm i hono

次に honosrc/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,
				},
			]);
		});
	}
}

このワークフローは次のことを行います。

  1. text パラメーターを受け取ります。
  2. D1 の notes テーブルに新しい行を挿入し、その行の id を取得します。
  3. LLM バインディングの embeddings モデルで text をベクトルに変換します。
  4. idvectors を 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);
});

7. Vectorize を照会してノートを取得する

コードを完成させるため、ルートパス(/)を更新して 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;

8. Anthropic Claude モデルを追加する(任意)

大きな文書を扱う場合は、コンテキストウィンドウが大きく RAG ワークフローに向いている Anthropic の Claude モデル を使えます。

まず @anthropic-ai/sdk パッケージをインストールします。

npm i @anthropic-ai/sdk

src/index.jsGET / ルートを更新し、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.jsDELETE /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

さらに進めるには、次を参照してください。

役に立ちましたか?