Skip to content

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

Agents SDK

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

Cloudflare Agents SDK を使うと、Workers 上で動くステートフルな AI エージェントを構築できます。このガイドでは、独自の AI Search インスタンスをプロビジョニングし、ドキュメントをインデックスし、回答の前にツールでそのコンテンツを検索するチャットエージェントを構築します。

このガイドは、AI Search の search() をツールとしてモデルに公開する、推奨のエージェントパターンを使います。このパターンの詳細は エージェントツールとしての AI Search を参照してください。

前提条件

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

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

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

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

create-cloudflare CLI(C3)を使って、新しい Worker プロジェクトを作成します。C3 は、Cloudflare への新しいアプリケーションのセットアップとデプロイを支援するコマンドラインツールです。

次を実行して、ai-search-agent という新しいプロジェクトを作成します。

npm create cloudflare@latest -- ai-search-agent

セットアップでは、次のオプションを選びます。

  • 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? では、TypeScript を選びます。
  • Do you want to use git for version control? では、Yes を選びます。
  • Do you want to deploy your application? では、No を選びます(デプロイ前にいくつか変更します)。

アプリケーションディレクトリへ移動します。

cd ai-search-agent

2. Agents SDK パッケージをインストールする

Agents SDK、AI SDK、Workers AI プロバイダーをインストールします。

npm i agents @cloudflare/ai-chat ai workers-ai-provider zod

3. Worker を AI Search にバインドする

Wrangler 構成ファイル を次の内容に置き換えます。AI Search の 名前空間バインディング、応答生成用の Workers AI バインディング、エージェントのチャット履歴を保存する Durable Object を追加します。

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "ai-search-agent",
  "main": "src/server.ts",
  // Set this to today's date
  "compatibility_date": "2026-09-20",
  "compatibility_flags": [
    "nodejs_compat"
  ],
  "ai": {
    "binding": "AI"
  },
  "ai_search_namespaces": [
    {
      "binding": "AI_SEARCH",
      "namespace": "default",
      "remote": true
    }
  ],
  "durable_objects": {
    "bindings": [
      {
        "name": "SearchAgent",
        "class_name": "SearchAgent"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": [
        "SearchAgent"
      ]
    }
  ]
}
name = "ai-search-agent"
main = "src/server.ts"
# Set this to today's date
compatibility_date = "2026-09-20"
compatibility_flags = ["nodejs_compat"]

[ai]
binding = "AI"

[[ai_search_namespaces]]
binding = "AI_SEARCH"
namespace = "default"
remote = true

[[durable_objects.bindings]]
name = "SearchAgent"
class_name = "SearchAgent"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["SearchAgent"]

エージェントは実行時に create() を呼び出すため、単一インスタンスの ai_search バインディングではなく、名前空間バインディング(ai_search_namespaces)が必要です。AI Search はローカルでは動かないため、remote オプションにより wrangler dev がデプロイ済みインスタンスへリクエストをプロキシします。AIChatAgent はメッセージを SQLite に永続化するため、そのクラスを new_sqlite_classes に列挙する必要があります。

4. エージェントを書く

src/server.ts を作成します。エージェントは初回実行時に ハイブリッド検索 を有効にした AI Search インスタンスをプロビジョニングし、ドキュメントでシードし、2 つのツールを公開します。search_knowledge_base はコンテンツを取得し、save_resolution は新しいコンテンツを書き戻します。

src/server.jsjs
import { AIChatAgent } from "@cloudflare/ai-chat";
import { routeAgentRequest } from "agents";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
import { z } from "zod";

const INSTANCE_NAME = "knowledge-base";

const SEED_DOC = `# Getting started
AI Search indexes your content so an agent can retrieve it at query time.`;

// AIChatAgent stores the conversation history and calls onChatMessage() once
// for each user message.
export class SearchAgent extends AIChatAgent {
	// Guard so the one-time instance setup runs only once per running agent.
	ready = false;

	// Create the agent's instance with hybrid search enabled, then seed it so
	// the first query has content. create() throws if the instance already
	// exists, so the try/catch makes this idempotent.
	async ensureInstance() {
		if (this.ready) return;
		try {
			// index_method with both vector and keyword enables hybrid search.
			await this.env.AI_SEARCH.create({
				id: INSTANCE_NAME,
				index_method: { vector: true, keyword: true },
			});
			// upload() queues the file; indexing runs in the background. Poll the
			// item status until it is searchable so the first query has content.
			const instance = this.env.AI_SEARCH.get(INSTANCE_NAME);
			const { id } = await instance.items.upload(
				"getting-started.md",
				SEED_DOC,
			);
			let info = await instance.items.get(id).info();
			while (info.status === "queued" || info.status === "running") {
				await new Promise((resolve) => setTimeout(resolve, 2_000));
				info = await instance.items.get(id).info();
			}
		} catch {
			// Instance already exists.
		}
		this.ready = true;
	}

	// Runs on every chat message: make sure the instance exists, then stream a
	// tool-using response.
	async onChatMessage() {
		await this.ensureInstance();

		// The agent exposes AI Search's search() as a tool instead of using AI
		// Search's own chat model, so this model must support function calling to
		// decide when to search and drive the tool calls.
		const workersai = createWorkersAI({ binding: this.env.AI });

		const result = streamText({
			model: workersai("@cf/zai-org/glm-5.2"),
			system:
				"You are a support assistant. Use search_knowledge_base to find " +
				"relevant content before answering, and cite what you use.",
			// this.messages is the stored chat history; convert it to the format
			// the model expects.
			messages: await convertToModelMessages(this.messages),
			tools: {
				search_knowledge_base: tool({
					description: "Search the knowledge base for relevant content.",
					inputSchema: z.object({
						query: z.string().describe("The user's question or search terms"),
					}),
					// Hybrid search runs by default because the instance indexes
					// both vectors and keywords.
					execute: async ({ query }) => {
						const instance = this.env.AI_SEARCH.get(INSTANCE_NAME);
						return await instance.search({
							query,
							ai_search_options: { retrieval: { max_num_results: 5 } },
						});
					},
				}),
				save_resolution: tool({
					description:
						"Save a resolved answer to the knowledge base for reuse.",
					inputSchema: z.object({
						title: z.string().describe("Short descriptive title"),
						content: z.string().describe("The resolution to save"),
					}),
					// upload() returns as soon as the file is queued; indexing then
					// finishes in the background.
					execute: async ({ title, content }) => {
						const instance = this.env.AI_SEARCH.get(INSTANCE_NAME);
						const item = await instance.items.upload(`${title}.md`, content);
						return { key: item.key, status: item.status };
					},
				}),
			},
			// Cap the tool-call loop so the agent cannot run tools indefinitely.
			stopWhen: stepCountIs(5),
		});

		// Stream the reply, including any tool activity, back to the client.
		return result.toUIMessageStreamResponse();
	}
}

export default {
	async fetch(request, env) {
		// Route the request to the matching agent instance, keyed by the URL.
		return (
			(await routeAgentRequest(request, env)) ||
			new Response("Not found", { status: 404 })
		);
	},
};
src/server.tsts
import { AIChatAgent } from "@cloudflare/ai-chat";
import { routeAgentRequest } from "agents";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
import { z } from "zod";

const INSTANCE_NAME = "knowledge-base";

const SEED_DOC = `# Getting started
AI Search indexes your content so an agent can retrieve it at query time.`;

// AIChatAgent stores the conversation history and calls onChatMessage() once
// for each user message.
export class SearchAgent extends AIChatAgent {
	// Guard so the one-time instance setup runs only once per running agent.
	private ready = false;

	// Create the agent's instance with hybrid search enabled, then seed it so
	// the first query has content. create() throws if the instance already
	// exists, so the try/catch makes this idempotent.
	private async ensureInstance() {
		if (this.ready) return;
		try {
			// index_method with both vector and keyword enables hybrid search.
			await this.env.AI_SEARCH.create({
				id: INSTANCE_NAME,
				index_method: { vector: true, keyword: true },
			});
			// upload() queues the file; indexing runs in the background. Poll the
			// item status until it is searchable so the first query has content.
			const instance = this.env.AI_SEARCH.get(INSTANCE_NAME);
			const { id } = await instance.items.upload(
				"getting-started.md",
				SEED_DOC,
			);
			let info = await instance.items.get(id).info();
			while (info.status === "queued" || info.status === "running") {
				await new Promise((resolve) => setTimeout(resolve, 2_000));
				info = await instance.items.get(id).info();
			}
		} catch {
			// Instance already exists.
		}
		this.ready = true;
	}

	// Runs on every chat message: make sure the instance exists, then stream a
	// tool-using response.
	async onChatMessage() {
		await this.ensureInstance();

		// The agent exposes AI Search's search() as a tool instead of using AI
		// Search's own chat model, so this model must support function calling to
		// decide when to search and drive the tool calls.
		const workersai = createWorkersAI({ binding: this.env.AI });

		const result = streamText({
			model: workersai("@cf/zai-org/glm-5.2"),
			system:
				"You are a support assistant. Use search_knowledge_base to find " +
				"relevant content before answering, and cite what you use.",
			// this.messages is the stored chat history; convert it to the format
			// the model expects.
			messages: await convertToModelMessages(this.messages),
			tools: {
				search_knowledge_base: tool({
					description: "Search the knowledge base for relevant content.",
					inputSchema: z.object({
						query: z.string().describe("The user's question or search terms"),
					}),
					// Hybrid search runs by default because the instance indexes
					// both vectors and keywords.
					execute: async ({ query }) => {
						const instance = this.env.AI_SEARCH.get(INSTANCE_NAME);
						return await instance.search({
							query,
							ai_search_options: { retrieval: { max_num_results: 5 } },
						});
					},
				}),
				save_resolution: tool({
					description:
						"Save a resolved answer to the knowledge base for reuse.",
					inputSchema: z.object({
						title: z.string().describe("Short descriptive title"),
						content: z.string().describe("The resolution to save"),
					}),
					// upload() returns as soon as the file is queued; indexing then
					// finishes in the background.
					execute: async ({ title, content }) => {
						const instance = this.env.AI_SEARCH.get(INSTANCE_NAME);
						const item = await instance.items.upload(`${title}.md`, content);
						return { key: item.key, status: item.status };
					},
				}),
			},
			// Cap the tool-call loop so the agent cannot run tools indefinitely.
			stopWhen: stepCountIs(5),
		});

		// Stream the reply, including any tool activity, back to the client.
		return result.toUIMessageStreamResponse();
	}
}

export default {
	async fetch(request: Request, env: Env) {
		// Route the request to the matching agent instance, keyed by the URL.
		return (
			(await routeAgentRequest(request, env)) ||
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

this.env.AI_SEARCH.get(INSTANCE_NAME) は同期的で、遅延解決されます。インスタンスは作成しないため、ensureInstance が先に作成します。1 回の呼び出しで複数インスタンスを検索するには、ai_search_options.instance_ids を使った名前空間レベルの検索を使います。名前空間 を参照してください。

ツールの動き

search_knowledge_base ツールは、インスタンスの search() を呼び出します。インスタンスはベクトルとキーワードの両方をインデックスするため、取得はデフォルトでハイブリッド検索を使います。

save_resolution ツールは items.upload() を呼び出します。組み込みストレージへドキュメントをアップロードし、インデックス待ち行列に入れます。呼び出しはすぐに戻り、バックグラウンドのインデックスが完了するとコンテンツが検索可能になります。同じ名前のファイルをアップロードすると上書きされ、再インデックスされます。

5. ローカルでテストする

型を生成し、開発サーバーを起動します。

npx wrangler types
npm run dev

AIChatAgent は WebSocket 上のチャットプロトコルを使うため、素の curl リクエストではなくチャットクライアントから操作します。最も簡単な方法は、Agents SDK の useAgentChat フックで構築した UI をローカルサーバーに向けることです。

How do I get started? のようなメッセージを送ります。最初のメッセージでエージェントがインスタンスを作成してシードするため、シードドキュメントのインデックス中は最初の応答に 1〜2 分かかることがあります。

連携が動いていることは、次で確認できます。

  • wrangler dev のログに /agents/search-agent/<name> へのリクエストが表示され、そのあと返信の前に search_knowledge_base ツールが実行される。
  • エージェントの回答がシードコンテンツに基づき、それを引用している。一般的な回答になっていない。

6. デプロイする

Cloudflare アカウントでログインします。

npx wrangler login

Worker をデプロイし、インターネットからアクセスできるようにします。

npx wrangler deploy

次のステップ

ハイブリッド検索

設定可能なフュージョンで、ベクトル検索とキーワード検索を組み合わせます。

テナント別検索

テナントまたはエージェントごとに、分離したインスタンスを持たせます。

役に立ちましたか?