つれづれなる Agent OPS
つれづれなる Agent OPS
実装検証

Cloudflare PagesとWorkersとAI GatewayでAIチャットBotの土台を作る

Cloudflare Pagesの静的UI、Pages Function、Workers AI binding、AI Gatewayを組み合わせ、ブラウザにAPIキーを出さないAIチャットBotの最小構成を検証する。

Xでシェア
Markdown を表示

Cloudflare AI Gatewayを触っていると、最初は「LLM APIのURLを差し替えればログが見える」という理解から入ります。これは導入としては正しいのですが、AIチャットBotとして公開する段階では、もう少し設計する場所が増えます。

ブラウザにモデルAPIキーを置かないこと。フロントエンドとLLM呼び出しの境界を分けること。AI Gatewayのログ、cache、metadataを、あとから運用で見られる形にしておくこと。小さいBotでも、このあたりを曖昧にすると、あとで「動いているが何が起きているか分からない」状態になりやすいです。

今回は、Cloudflare Pages、Pages Function、Workers AI binding、AI Gatewayを組み合わせて、AIチャットBotの土台を作ります。実験では、まずローカルのmock modeでAPI境界とGateway optionの組み立てを確認し、その後にCHAT_MODE=liveへ切り替えてWorkers AIの実モデル呼び出しとAI Gatewayログ取得まで確認しました。

今回の構成

構成はかなり小さくしています。

Browser

Cloudflare Pages static UI
  ↓ /api/chat
Pages Function on Workers runtime
  ↓ env.AI.run(..., { gateway })
Cloudflare AI Gateway

Workers AI model

Pagesは静的UIを配信し、/api/chatだけをPages Functionで受けます。AI Gatewayは、LLM呼び出しの途中に入る観測と制御のレイヤーです。Cloudflareの公式ドキュメントでは、WorkersのAI bindingからenv.AI.run()を呼び、その第3引数でgatewayを指定できます。ここにGateway ID、cache制御、log収集、metadataを渡せます。

今回のサンプルではWorkers AIの@cf/zai-org/glm-5.2を指定しています。Workers AIはローカル開発中でもCloudflareアカウント上の推論を使うため、最初から実モデルへ投げるのではなく、mock modeでUI/API契約を確認してからlive modeへ進めました。

まずUIとAPI境界を作る

Pages側の画面は、単純なチャットUIにしました。ブラウザは/api/chatmessageskipCacheだけを送ります。モデル名、Gateway ID、metadata、AI bindingの存在はブラウザ側へ持たせません。

Cloudflare Pagesで配信するチャットUIから/api/chatへ送信し、mock応答とGateway optionを確認している画面

Pages Function側は薄くして、処理本体を共通ハンドラへ寄せます。

import { handleChat, handleOptions } from "../../src/chat.mjs";

export const onRequestPost = (context) => handleChat(context);

export const onRequestOptions = () => handleOptions();

export const onRequestGet = () =>
  Response.json({
    ok: true,
    route: "/api/chat",
    methods: ["POST", "OPTIONS"],
  });

この分け方にしておくと、Pages Functionとして動かすときも、ローカルのmock serverで同じhandleChat()を呼ぶときも、入力検証とレスポンス形式を揃えられます。

AI Gateway optionをアプリ側で組み立てる

CloudflareのAI bindingでは、env.AI.run()にモデル入力を渡すだけでなく、Gateway optionを一緒に渡せます。今回のサンプルでは、Gateway ID、cacheをスキップするか、ログを収集するか、アプリ識別用のmetadataを付けています。

const raw = await env.AI.run(
  model,
  {
    messages,
  },
  {
    gateway: {
      id: env.CLOUDFLARE_AI_GATEWAY_ID ?? "default",
      skipCache: Boolean(body.skipCache),
      collectLog: env.COLLECT_AI_LOG !== "false",
      metadata: {
        app: "pages-workers-ai-chatbot",
        env: env.ENVIRONMENT ?? "local",
        route: "api-chat",
      },
    },
  },
);

逆に、ユーザーIDや問い合わせIDのような値を入れる場合は注意が必要です。Gatewayのログは運用に便利ですが、今回の実測で確認できたのは、provider、model、status、duration、cost、token、metadataといったメタデータ寄りの情報です。promptやresponse本文をどこまで保存し、誰が見られる状態にするかは、Gateway側のログ設定、payload保存方針、アプリのデータ分類に合わせて別途決めるべきです。

ローカルではmock modeで確認する

今回の検証では、いきなり実モデルへ接続しませんでした。理由は単純で、最初に確認したいのはLLMの回答品質ではなく、AIチャットBotとしての境界だからです。

検証用に用意したコマンドは次の3つです。

npm run check
npm run dev
npm run verify

npm run checkは、サンプル内のJavaScriptをnode --checkで構文確認するためのコマンドです。npm run devは、Pagesの静的UIと/api/chatをローカルで動かすmock serverです。npm run verifyは記事用に作った検証スクリプトで、起動中のローカルプレビューへ/api/health/api/chatを送り、次の点を確認します。

  • health endpointがok=trueを返すこと
  • local previewではsource=mockになること
  • Gateway IDがdefaultとして組み立てられること
  • metadataにappenvrouteが入ること
  • 空メッセージがHTTP 400になること

ローカル検証画面でhealth、mock source、gateway metadataがOKになっている画面

この段階で確認できたのは、UI、API、入力検証、Gateway optionの組み立てまでです。ここで止めると「実験した」とは言えないので、次にWorkers AIの実モデル呼び出しとAI Gatewayログ取得を確認します。

live modeではWorkers AI bindingを使う

Cloudflare上またはWranglerのlive previewで実モデルを呼ぶときは、CHAT_MODE=liveにします。Wranglerで確認する場合は、AI bindingを付けてPagesを起動します。

wrangler login
npx wrangler pages dev public \
  --port 8790 \
  --compatibility-date=2026-07-03 \
  --ai AI \
  --binding CHAT_MODE=live \
  --binding CLOUDFLARE_AI_GATEWAY_ID=default \
  --binding AI_MODEL=@cf/zai-org/glm-5.2 \
  --binding ENVIRONMENT=local-live \
  --binding COLLECT_AI_LOG=true \
  --binding ENABLE_LOG_INSPECTION=true

サンプルのwrangler.jsoncでは、AI bindingをAIとして設定しています。

{
  "name": "cloudflare-pages-workers-ai-gateway-chatbot",
  "compatibility_date": "2026-07-03",
  "pages_build_output_dir": "public",
  "ai": {
    "binding": "AI"
  },
  "vars": {
    "CHAT_MODE": "live",
    "CLOUDFLARE_AI_GATEWAY_ID": "default",
    "AI_MODEL": "@cf/zai-org/glm-5.2",
    "ENVIRONMENT": "preview",
    "COLLECT_AI_LOG": "true",
    "ENABLE_LOG_INSPECTION": "false"
  }
}

実行時に一度、compatibility_date2026-07-04にしたところ、Cloudflare側ではまだ未来日として拒否されました。この記事を書いているローカル環境の日付は2026年7月4日ですが、Cloudflareの受理基準ではその時点で2026-07-04が未対応だったため、2026-07-03に変更して検証を進めました。

ここでの注意点は、Workers AI binding経由でWorkers AIを呼ぶ構成では、ブラウザに外部プロバイダのAPIキーを渡しません。サンプルはCloudflare内のWorkers AIを使う前提なので、OpenAIやGeminiのAPIキーをWorker secretへ置く構成とは少し違います。

OpenAIやAnthropicなどの外部プロバイダを自分のキーで呼ぶ場合は、AI Gatewayのprovider-native endpointやREST API側の設計を見直す必要があります。公式ドキュメント上、AI bindingから第三者モデルを呼ぶ場合はCloudflare側のUnified Billingを使う説明になっており、BYOKをそのままAI bindingへ渡す設計ではありません。

このあたり、古い「OpenAI互換URLを差し替えるだけ」という理解のままだと少し詰まります。正直、ここは最初に混乱しやすい。

実モデル呼び出しとAI Gatewayログを確認する

live modeで/api/chatへ1回リクエストを送りました。レスポンスでは、sourceworkers-aiになり、モデルは@cf/zai-org/glm-5.2、Gateway optionにはid: "default"skipCache: truecollectLog: true、metadataとしてappenvrouteが入りました。

さらに、レスポンスに含まれたaiGatewayLogIdを使い、検証用に追加した/api/ai-gateway-logからAI Gatewayのログ要約を取得しました。これはCloudflareダッシュボードのAI Gatewayログに出る同じログエントリを、AI bindingのgetLog()で確認するための検証です。

Workers AIの実レスポンスとAI Gatewayログ要約を照合しているlive検証画面

今回取得できたAI Gatewayログの要約は次のとおりです。

項目結果
providerworkers-ai
model@cf/zai-org/glm-5.2
status200
cachedfalse
duration17182 ms
cost0.0028052000000000003
tokens in74
tokens out614
metadataapp, env, route を確認
prompt / response body今回のgetLog()要約では本文取得なし

これで、単にWorkerからモデル応答が返っただけではなく、そのリクエストがAI Gatewayログとして残り、provider、model、status、duration、cost、token、metadataまで後から照合できることを確認できました。一方で、今回のartifactではprompt本文とresponse本文は取得できていません。本文保存まで運用で使う場合は、AI Gateway側のログ設定とpayload保存方針を分けて確認する必要があります。

cacheでコスト削減を言いすぎない

AI Gatewayのcacheは、AIチャットBotで魅力的に見えます。同じ質問なら上流モデルへ投げずに返せるので、レイテンシとコストの両方に効く可能性があります。

向いているのは、FAQ、固定メニュー、テンプレート化された質問、同じsystem promptと同じuser promptを繰り返す用途です。逆に、ユーザーごとに文脈が変わる相談Botでは、cacheよりもmetadata、ログ、rate limit、payload保存方針の方が先に効きます。

今回のUIにskip cacheを置いたのは、cacheを常に有効化するためではありません。Botのリクエスト単位で「これはcacheしてよい入力なのか」を考えるためです。

小さいBotでも運用の論点は先に出る

今回の検証で一番大きかった発見は、AIチャットBotの最小構成でも、実装論点と運用論点を完全には分けられないことです。

UIをPagesへ置き、/api/chatをFunctionへ分けるだけなら簡単です。env.AI.run()でモデルを呼ぶことも難しくありません。ただ、公開するBotとして考えると、すぐに次の問いが出ます。

  • promptとresponseをGatewayログに残してよいか
  • metadataにどの粒度の識別子を入れるか
  • cacheを有効にしてよい質問と、毎回モデルへ投げる質問をどう分けるか
  • local previewで実モデルを呼ぶか、mockでAPI契約だけを見るか
  • Workers AIを使うのか、外部プロバイダをprovider-native endpointで呼ぶのか

この問いを後回しにすると、最初は速く作れます。しかし、ユーザーから「回答がおかしい」「遅い」「コストが高い」と言われたときに、どこを見ればよいか分からなくなります。

個人的には、Cloudflare Pages + Workers + AI Gatewayの組み合わせは、個人開発や小さなAIアプリの基盤としてかなり相性がよいです。静的UI、API境界、推論、観測の入口を同じCloudflare側に寄せられるからです。一方で、AI Gatewayを挟むだけでAgentOpsが完成するわけではありません。

AI Gatewayは、LLM呼び出しの出口を見えるようにする入口です。RAGでどの文書を取ったか、Agentがどのtoolを呼んだか、ユーザー評価がどうだったかまで扱うなら、Langfuseのようなtrace設計は別に必要になります。

まとめ

Cloudflare Pages、Pages Function、Workers AI binding、AI Gatewayを組み合わせると、ブラウザにモデルAPIキーを出さずにAIチャットBotの土台を作れます。今回の検証では、まず静的UIから/api/chatへ送り、同じハンドラで入力検証、mock応答、Gateway ID、metadataを確認しました。そのうえでlive modeに切り替え、Workers AIのGLM-5.2から実応答を受け取り、AI Gatewayログとしてprovider、model、status、duration、cost、tokens、metadataを取得できることを確認しました。

一方で、cache HIT/MISSの比較、payload保存方針、外部プロバイダのBYOK構成は別途確認が必要です。特にcacheは、自由入力のチャット全般で効くものではなく、同一リクエストや固定質問に寄った使い方で効果が出ます。

まずはmock modeでAPI境界を固め、次にWranglerのlive modeでWorkers AIとAI Gatewayログを確認する。この順番にすると、LLMの回答品質、Cloudflare binding、UIの不具合を一度に抱え込まずに済みます。

参考資料

DUOps

Author

DUOps(デュオプス)

LLMOps、Agent、MCP、Langfuse、Cloudflare 周辺の実装と運用を、個人で試しながら記録しています。

Xを見る

コメント

Related