「Cloudflare Agents と AI SDK でAIエージェントを作ってみた」 というタイトルでDevelopersIO Sapporo 2025 に登壇しました #devio2025

「Cloudflare Agents と AI SDK でAIエージェントを作ってみた」 というタイトルでDevelopersIO Sapporo 2025 に登壇しました #devio2025

2025.10.08

クラウド事業本部サービス開発室の佐藤です。

2025/9/26 (金) にクラスメソッドの札幌オフィスで開催されたDevelopersIO 2025 Sapporoに 「Cloudflare AgentsとAI SDKでAIエージェントを作ってみた」 というタイトルで登壇させていただきました。

登壇時間内では足りなく、伝えきれなかった部分をブログで共有しようと思います。

Cloudflare Agents とは

Cloudflare Agents (Agents SDK) とは、Cloudflare上でエージェントの開発に必要な基盤機能を提供してくれるCloudflare Workers向けのライブラリです。

https://agents.cloudflare.com/

エージェントの実装というよりは、エージェントの実装に必要な各種機能(WebSocketを利用したチャット、MCP、スケジューリング、状態管理、イベント駆動、エージェントのインスタンスごとに独立した実行基盤)を提供してくれる仕組みになります。

主に、以下のエージェントの実装が可能です。いくつかサンプルの解説をしたいと思います。

  • チャットエージェント
  • メールエージェント
  • スケジュールエージェント
  • MCPエージェント

チャットエージェント

チャットエージェントは、LLMを利用したChatBotを作成するための機能をCloudflare上で提供してくれます。簡単に試すなら以下のコマンドを入力することでChatBotの雛形を作成してくれます。

			
			npm create cloudflare@latest agents-starter -- --template=cloudflare/agents-starter

		

Agents SDKの AIChatAgent というクラスを利用します。これは内部的に partykit と呼ばれるDurable ObjectベースのWebSocketライブラリの PartyServer を継承したクラスになっていて、これを継承したクラスを作成することで、簡単にクライアントとのチャットベースのアプリケーションを作成することができます。

最小限の使い方は以下のとおりで、AIChatAgent を継承したクラスを作成します。その中に onChatMessage メソッドを作成することで、エージェントがチャットを受信した時の処理を実装することができます。AI SDK のcreateUIMessageStream を利用し、メッセージのストリームを作成し streamTextを使ってLLMを呼び出して結果をマージするという形です。こうすることで、エージェント内でコンテキストが蓄積され、前の会話内容を覚えている状態でLLMと通信させることができます。

			
			import { AIChatAgent } from "agents/ai-chat-agent";
import { streamText } from "ai";

export class Chat extends AIChatAgent<Env> {
  async onChatMessage(
    onFinish: StreamTextOnFinishCallback<ToolSet>
  ) {
    const stream = createUIMessageStream({
      execute: async ({ writer }) => {
        const result = streamText({
          system: `You are a helpful assistant that can do various tasks`,
          messages: convertToModelMessages(this.messages),
          model,
          tools: allTools,
          onFinish: onFinish as unknown as StreamTextOnFinishCallback<
            typeof allTools
          >,
          stopWhen: stepCountIs(10)
        });
        writer.merge(result.toUIMessageStream());
      }
    });

    return createUIMessageStreamResponse({ stream });
  }
}

		

クライアントはReactベースのWebアプリケーションになっていて、起動すると以下のような画面になります。

AI_Chat_Agent

デフォルトで良い感じのチャットのアプリケーションが出来上がっています。適宜好みのデザインに自由に変更することができます。

クライアントから先程のエージェントに接続するにはどうするかというと、専用のReact Hooksが用意されていて、Agents SDKの useAgentuseAgentChat を利用します。

			
			 const agent = useAgent({
    agent: "Chat"
  });

		

useAgent で対象のエージェントのクラス名を指定することで、対象のエージェントとWebSocketで接続することができます。こちらも内部的に partykit を利用していて、DurableObject の PartyServer と接続するための PartySocket が利用されています。

次に、useChatAgent Hookを利用します。

			
			  const {
    messages: agentMessages,
    addToolResult,
    clearHistory,
    status,
    sendMessage,
    stop
  } = useAgentChat<unknown, UIMessage<{ createdAt: string }>>({
    agent
  });

		

これを利用することで、チャットの実装に必要なメッセージ送信や過去のメッセージ、コンテキストのクリア機能などを提供してくれます。

メールエージェント

Cloudflareの Email Routing を利用して、メールを受信したら起動するエージェントを作成することができます。Email RoutingとはCloudflare内に自分のドメインのメールアドレスを作成して、メールをルーティングさせることができる機能です。メールエージェントではこの機能を利用して、メールをCloudflare Workersへ転送しそこからメールエージェントを呼び出す構成にすることができます。

Untitled

メールエージェントではAgent SDKの Agent を継承したクラスを作成します。このクラスに onEmail メソッドを実装することでメールを処理するエージェントを作成することができます。この中に具体的なエージェントの処理を記述していきます。以下の例では、LLMにはbedrockのclaude-sonnet-4を利用し、メールの添付ファイルを取得し、AI SDKの generateText 経由でメールの添付ファイル(ここでは領収書ファイル)をLLMに送信してファイルの情報を取得しSlackに通知している例です。

			
			export class MailAgent extends Agent<Env> {
  async onEmail(email: AgentEmail): Promise<void> {
    const model = bedrock("us.anthropic.claude-sonnet-4-20250514-v1:0");

    // メール解析
    const rawEmail = await email.getRaw();
    const parsedEmail = await PostalMime.parse(rawEmail);

    // 添付された領収書PDFから必要な情報を抽出する
    const { text: extractedReceiptText } = await generateText({
      model,
      messages: [
        {
          role: "user",
          content: [
            {
              type: "text",
              text: `
      添付された領収書から次の情報を抽出してください:
      - 金額(税込)
      - 税額(わかる場合)
      - 領収日
      - 請求元会社名
      - 領収書番号(わかる場合)
      - 領収内容の説明
      - 適切な経費科目(電車代、資格試験、仮払金、タクシー代)
      日付はYYYY-MM-DD形式で返してください。`,
            },
            {
              type: "file",
              data: firstAttachment.content,
              mediaType: firstAttachment.mimeType,
            },
          ],
        },
      ],
    });

    // 抽出した情報からJSONオブジェクトに変換
    const { object } = await generateObject({
      model,
      schema: receiptDataSchema,
      prompt: extractedReceiptText,
    });

    // Slackに通知を送信
    const { text: slackNotificationText } = await generateText({
      model,
      tools: {
        slackReceiptNotificationTool,
      },
      messages: [
        {
          role: "user",
          content: `以下の領収書データをSlackに通知してください:
            領収書データ: ${JSON.stringify(object, null, 2)}
            送信元メールアドレス: ${parsedEmail.from?.address || "不明"}
            メール件名: ${parsedEmail.subject || "件名なし"}`,
        },
      ],
      toolChoice: "required",
    });
  }
}

		

次にこのエージェントクラスをDurableObjectとバインディングするための設定をwrangler.jsonに追加します。これによりPartyServerを利用したWebSocketの機能やDurableObjectの状態管理の仕組みを利用することができます。

			
			{
	// 追加
	"durable_objects": {
		"bindings": [
			{
				"name": "MailAgent",
				"class_name": "MailAgent"
			}
		]
	},
	"migrations": [
		{
			"tag": "v1",
			"new_sqlite_classes": [
				"MailAgent"
			]
		}
	]
}

		

次にSlack通知の部分です。ここでは、AI SDKの Tool を利用していて、エージェントがプロンプトから自律的に選択して利用することができるツールを定義することができます。tool関数を利用します。

  • description にはエージェントがプロンプトからツールを利用する判断をするための具体的な内容を定義します。
  • inputSchema にはzod形式でLLMがこのツールを利用する際に渡す引数を定義します。これによりLLMはプロンプトからこのツールを利用する際に、自動的にここに値を設定して利用してくれます。
  • execute には、LLMがこのツールを利用した際に実行する関数を定義します。引数にはinputSchemaに設定したものが型付きで取得できます。ここでは、具体的なSlack通知の処理を実装しています。
			
			import { env } from "cloudflare:workers";
import { tool } from "ai";
import { z } from "zod";

export const slackReceiptNotificationTool = tool({
  description: "領収書データをSlackチャンネルに通知します",
  inputSchema: z.object({
    receiptData: z
      .object({
        amount: z.number().describe("領収金額(税込)"),
        taxAmount: z.number().optional().describe("税額"),
        date: z.string().describe("領収日(YYYY-MM-DD形式)"),
        vendor: z.string().describe("請求元会社名"),
        invoiceNumber: z.string().optional().describe("領収書番号"),
        description: z.string().describe("領収内容の説明"),
        expenseName: z.string().describe("経費科目名"),
      })
      .describe("抽出された領収書データ"),
    emailFrom: z.string().optional().describe("送信元メールアドレス"),
    emailSubject: z.string().optional().describe("メール件名"),
  }),
  execute: async ({ receiptData, emailFrom, emailSubject }) => {
    // 具体的なSlack通知の処理
    const apiToken = env.SLACK_API_TOKEN;
    const channel = "#notify";
    
    // ...

    const response = await fetch("https://slack.com/api/chat.postMessage", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${apiToken}`,
      },
      body: JSON.stringify({
        channel,
        text: message,
        attachments,
      }),
    });

    const data = (await response.json()) as {
      ok: boolean;
      error?: string;
      ts?: string;
    };

    return {
      success: true,
      message: "Notification sent to Slack",
      ts: data.ts,
    };
  },
});


		

参考資料

登壇資料

この記事をシェアする

FacebookHatena blogX

関連記事