---
title: "Cloudflare PagesとWorkersとAI GatewayでAIチャットBotの土台を作る"
description: "Cloudflare Pagesの静的UI、Pages Function、Workers AI binding、AI Gatewayを組み合わせ、ブラウザにAPIキーを出さないAIチャットBotの最小構成を検証する。"
lang: "ja"
canonical: "https://llm-lab.dev/posts/cloudflare-pages-workers-ai-gateway-chatbot/"
source: "https://llm-lab.dev/posts/cloudflare-pages-workers-ai-gateway-chatbot.md"
publishedAt: "2026-07-05"
updatedAt: "2026-07-05"
category: "実装検証"
tags:
  - "cloudflare"
  - "cloudflare-pages"
  - "workers-ai"
  - "ai-gateway"
  - "chatbot"
  - "agentops"
---

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

> [!NOTE]
> この記事で確認したこと
>
> 今回は、Cloudflare Pages、Pages Function、Workers AI binding、AI Gatewayを組み合わせて、AIチャットBotの土台を作りました。
>
> 確認できた流れは、まずmock modeでUIと `/api/chat` の契約を固め、その後live modeでWorkers AIの実応答と対応するAI Gatewayログを照合する、というものです。小さなBotでも、モデル認証情報はブラウザへ出さず、Gateway metadataはサーバー側で設計し、公開前にGatewayログで実際に何が見えるか確認する必要があります。

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ログ取得まで確認しました。

## 今回の構成

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

```text
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/chat`へ`message`と`skipCache`だけを送ります。モデル名、Gateway ID、metadata、AI bindingの存在はブラウザ側へ持たせません。

![Cloudflare Pagesで配信するチャットUIから/api/chatへ送信し、mock応答とGateway optionを確認している画面](/images/posts/cloudflare-pages-workers-ai-gateway-chatbot/01-chat-ui.webp)

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

```js
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を付けています。

```js
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",
      },
    },
  },
);
```

> [!IMPORTANT] metadataはアプリ側で設計する
> AI Gatewayを「あとでダッシュボードを見れば何とかなるもの」として扱わないことが重要です。少なくとも、どのアプリ、どの環境、どのAPIルートから出たLLM呼び出しなのかは、アプリ側でmetadataに付けておいた方がよいです。

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

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

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

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

```bash
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に`app`、`env`、`route`が入ること
* 空メッセージがHTTP 400になること

![ローカル検証画面でhealth、mock source、gateway metadataがOKになっている画面](/images/posts/cloudflare-pages-workers-ai-gateway-chatbot/02-local-verification.webp)

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

## live modeではWorkers AI bindingを使う

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

> [!WARNING] live previewでも利用量が発生する
> AI bindingはremote resourceへ接続されるため、ローカル開発中でも利用量が発生します。

```bash
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`として設定しています。

```jsonc
{
  "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_date`を`2026-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回リクエストを送りました。レスポンスでは、`source`が`workers-ai`になり、モデルは`@cf/zai-org/glm-5.2`、Gateway optionには`id: "default"`、`skipCache: true`、`collectLog: true`、metadataとして`app`、`env`、`route`が入りました。

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

> [!WARNING] ログ確認エンドポイントは検証用
> このエンドポイントは、`ENABLE_LOG_INSPECTION=true`のときだけ動く検証用です。公開運用のBotにそのまま置くものではありません。

![Workers AIの実レスポンスとAI Gatewayログ要約を照合しているlive検証画面](/images/posts/cloudflare-pages-workers-ai-gateway-chatbot/03-live-ai-gateway-log.webp)

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

| 項目 | 結果 |
|---|---|
| provider | `workers-ai` |
| model | `@cf/zai-org/glm-5.2` |
| status | `200` |
| cached | `false` |
| duration | `17182` ms |
| cost | `0.0028052000000000003` |
| tokens in | `74` |
| tokens out | `614` |
| metadata | `app`, `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で魅力的に見えます。同じ質問なら上流モデルへ投げずに返せるので、レイテンシとコストの両方に効く可能性があります。

> [!CAUTION] cacheは自由入力の万能な削減策ではない
> 公式ドキュメントでは、cacheは同一リクエストに対して効くものとして説明されています。自由入力のチャットでは、文末や表現が少し変わるだけで別リクエストになります。したがって、「チャットBotならcacheで大幅削減」と雑に言うのは危ないです。

向いているのは、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の不具合を一度に抱え込まずに済みます。

## 参考資料

* [Cloudflare Pages Functions](https://developers.cloudflare.com/pages/functions/)
* [Cloudflare Pages Functions Bindings](https://developers.cloudflare.com/pages/functions/bindings/)
* [Cloudflare AI Gateway Workers Bindings](https://developers.cloudflare.com/ai-gateway/usage/worker-binding-methods/)
* [Cloudflare AI Gateway Logging](https://developers.cloudflare.com/ai-gateway/observability/logging/)
* [Cloudflare AI Gateway Caching](https://developers.cloudflare.com/ai-gateway/features/caching/)
* [Cloudflare AI Gateway REST API](https://developers.cloudflare.com/ai-gateway/usage/rest-api/)
