AI

Cloudflare Web Search API入門——AIエージェントの検索結果をFirestoreで監査する

Cloudflare Web Search API betaとAI Gatewayを題材に、Next.js + FirestoreでAIエージェントのWeb検索を監査・再利用できる形にする実践手順を解説します。

2026年10月3日
CloudflareAI GatewayWeb Search APINext.jsFirestore
Cloudflare Web Search API入門——AIエージェントの検索結果をFirestoreで監査する

はじめに

AIエージェントに問い合わせ対応やリサーチ補助を任せるとき、こんな困りごとが出てきます。

場面よくある困りごと
最新の料金や障害情報を調べたいモデルの学習済み知識だけでは古い
Webページを直接取りに行かせたいURLを推測して404になり、無駄なツール呼び出しが増える
検索結果を業務判断に使いたいどの検索語で、どのURLを根拠にしたか残りにくい

2026年10月2日、Cloudflareは Web Search API via AI Gateway を発表しました。Web Search APIはbetaとして提供され、AI Gateway経由でWeb検索を実行し、検索リクエストを通常のAI Gatewayログや課金管理の中で扱える仕組みです。

本記事では、架空の問い合わせ管理SaaS「SupportDock」を題材に、Next.js App Router + Firestore + TypeScriptで「AIエージェントの検索結果を監査できる検索台帳」を作ります。この記事を読み終えると、検索をただAIに渡すだけでなく、検索語、provider、結果URL、利用者、再利用可否をFirestoreに残す設計にできます。

参考にした一次情報は次の通りです。

何が新しいのか

CloudflareのWeb Search APIは、AIエージェントやアプリケーションがインターネット上の情報を検索し、最新情報で回答を補強するためのAPIです。公式ドキュメントでは、REST APIとWorkersのAI bindingの両方から呼び出せると説明されています。

2026年10月2日時点のドキュメントでは、検索providerとして ceramic、exa、linkup が挙げられています。providerを指定しない場合はCeramic.aiがdefaultです。検索リクエストはAI Gatewayを通るため、検索もモデル推論と同じようにログ、分析、課金、アクセス制御の対象にできます。

ポイントは、Web Search APIが「モデルそのもの」ではないことです。検索結果を取得し、その結果をどのモデルに渡すか、どの画面に出すか、どのログを残すかはアプリ側の設計です。

事前に押さえる制約

Cloudflare公式ドキュメントでは、Web Search APIを使う前提としてCloudflareアカウント、AI Gateway、AI Gateway creditsまたはGatewayに保存したprovider API keyが必要だとされています。REST APIで呼び出す場合、API tokenには少なくとも Account > Workers AI > Read と Account > AI Gateway > Read の権限が必要です。

項目2026-10-02時点の内容
提供状況beta
REST endpointPOST /client/v4/accounts/{account_id}/ai/websearch/
Workers bindingenv.AI.websearch()
providerceramic、exa、linkup
limit1〜10、defaultは10
BYOKbyokAlias でGateway上のprovider key aliasを指定

また、Cloudflareの発表では、検索providerはZero Data Retention対応やVerified botsのクロール基準にコミットしていると説明されています。AIエージェント向け検索を導入するときは、精度だけでなく、クロールの透明性やログの扱いも確認しておくと安心です。

ハンズオン1: 検索を薄い関数に閉じ込める

まず、Next.jsのサーバー側からCloudflare Web Search APIを呼び出す関数を作ります。クライアントコンポーネントから直接呼ばず、Route HandlerやServer Actionの内側で使う想定です。

type CloudflareSearchProvider = "ceramic" | "exa" | "linkup";

type WebSearchItem = {
  url: string;
  title: string;
  description?: string;
};

type WebSearchResponse = {
  items: WebSearchItem[];
  metadata?: {
    query?: string;
    requestId?: string;
    latencyMs?: number;
  };
};

export async function searchWithCloudflare(input: {
  query: string;
  provider?: CloudflareSearchProvider;
  limit?: number;
}) {
  const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
  const token = process.env.CLOUDFLARE_API_TOKEN;

  if (!accountId || !token) {
    throw new Error("Cloudflare credentials are not configured");
  }

  const response = await fetch(
    `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/websearch/`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        query: input.query,
        provider: input.provider ?? "ceramic",
        limit: input.limit ?? 5,
        options: {
          gateway: { id: "default" },
        },
      }),
    },
  );

  if (!response.ok) {
    throw new Error(`Cloudflare Web Search API failed: ${response.status}`);
  }

  return (await response.json()) as WebSearchResponse;
}

ここでは公式ドキュメントにあるREST APIの形に合わせています。limit は最大10なので、UI側から大きな値を受け取ってもそのまま渡さないようにします。検索結果は便利ですが、根拠候補が多すぎるとレビューが逆に難しくなります。

ハンズオン2: Firestoreに検索台帳を残す

SupportDockでは、問い合わせごとにAI検索を実行し、検索結果を ticketSearchRuns コレクションへ保存します。検索結果本文を全部保存するのではなく、まずはURL、タイトル、description、requestId、latencyを残します。

// app/api/tickets/[ticketId]/web-search/route.ts
import { getApps, initializeApp } from "firebase-admin/app";
import { FieldValue, getFirestore } from "firebase-admin/firestore";
import { NextRequest, NextResponse } from "next/server";
import { searchWithCloudflare } from "@/lib/cloudflare/web-search";

if (!getApps().length) {
  initializeApp();
}

const db = getFirestore();

export async function POST(
  request: NextRequest,
  { params }: { params: Promise<{ ticketId: string }> },
) {
  const { ticketId } = await params;
  const body = (await request.json()) as { query?: string };

  if (!body.query || body.query.length < 3 || body.query.length > 300) {
    return NextResponse.json(
      { error: "query must be between 3 and 300 characters" },
      { status: 400 },
    );
  }

  const runRef = db.collection("ticketSearchRuns").doc();

  await runRef.set({
    ticketId,
    query: body.query,
    provider: "ceramic",
    status: "running",
    createdAt: FieldValue.serverTimestamp(),
    updatedAt: FieldValue.serverTimestamp(),
  });

  try {
    const result = await searchWithCloudflare({
      query: body.query,
      provider: "ceramic",
      limit: 5,
    });

    await runRef.update({
      status: "completed",
      requestId: result.metadata?.requestId ?? null,
      latencyMs: result.metadata?.latencyMs ?? null,
      items: result.items.map((item) => ({
        url: item.url,
        title: item.title,
        description: item.description ?? null,
      })),
      updatedAt: FieldValue.serverTimestamp(),
    });

    return NextResponse.json({ runId: runRef.id, items: result.items });
  } catch (error) {
    await runRef.update({
      status: "failed",
      errorMessage: error instanceof Error ? error.message : "unknown error",
      updatedAt: FieldValue.serverTimestamp(),
    });

    return NextResponse.json({ error: "search failed" }, { status: 502 });
  }
}

これで、AIが後続で回答を作る前に「どんな検索をしたか」が残ります。あとから担当者が回答を確認するときも、検索語と参照URLを見れば、AIの下書きがどこから来たのか追いやすくなります。

ハンズオン3: AIへの入力を根拠付きにする

検索結果をそのままユーザーに返すだけなら、普通の検索UIと変わりません。AIエージェントで使うなら、検索結果を回答生成の根拠として渡し、回答側にも参照URLを含めるのが実務向きです。

type SearchGroundingItem = {
  title: string;
  url: string;
  description?: string;
};

export function buildGroundedPrompt(input: {
  ticketBody: string;
  searchItems: SearchGroundingItem[];
}) {
  const sources = input.searchItems
    .map(
      (item, index) =>
        `[${index + 1}] ${item.title}\nURL: ${item.url}\n概要: ${
          item.description ?? "説明なし"
        }`,
    )
    .join("\n\n");

  return [
    "あなたは問い合わせ対応の下書きを作るアシスタントです。",
    "必ず与えられた検索結果だけを外部根拠として使い、推測でURLを追加しないでください。",
    "回答の末尾に参照URLを箇条書きで含めてください。",
    "",
    "<ticket>",
    input.ticketBody,
    "</ticket>",
    "",
    "<sources>",
    sources,
    "</sources>",
  ].join("\n");
}

この関数自体は特定のモデルAPIに依存しません。OpenAI Responses API、Anthropic Messages API、Workers AIなど、既存のAI呼び出しの直前で使えます。大事なのは、検索と生成を分け、検索結果をFirestoreへ保存してから生成へ進むことです。

ハンズオン4: GitHub Actionsで鮮度を点検する

FAQや料金ページなど、同じ問い合わせで何度も検索されるテーマは、日次で再検索して差分を確認できます。たとえば「主要連携サービスの料金変更」を毎朝チェックし、前回とURLやタイトルが変わったらFirestoreへイベントを残します。

name: refresh-ai-search-topics

on:
  schedule:
    - cron: "0 21 * * *"
  workflow_dispatch:

jobs:
  refresh:
    runs-on: ubuntu-latest
    steps:
      - name: Refresh tracked search topics
        run: |
          curl --fail --request POST "${APP_URL}/api/internal/search-topics/refresh" \
            --header "Authorization: Bearer ${CRON_SECRET}"
        env:
          APP_URL: ${{ secrets.APP_URL }}
          CRON_SECRET: ${{ secrets.CRON_SECRET }}

内部APIでは、Firestoreの trackedSearchTopics から有効なテーマを少数ずつ読み、searchWithCloudflare() を呼びます。検索providerやlimitをテーマごとに変えられるようにしておくと、精度とコストの調整がしやすくなります。

const snapshot = await db
  .collection("trackedSearchTopics")
  .where("enabled", "==", true)
  .orderBy("updatedAt", "asc")
  .limit(10)
  .get();

Firestoreで where と orderBy を組み合わせる場合、複合インデックスが必要になることがあります。開発時にFirebaseコンソールのリンクが出たら、必要なクエリだけインデックスを作成します。

運用で気をつけたいこと

Web Search APIを入れると、AIエージェントは最新情報へアクセスしやすくなります。ただし、検索結果は最終回答そのものではありません。上位に出たページが必ず正しいとは限らないため、業務判断に使う画面では「参照URLを表示する」「担当者が承認して送信する」「重要な回答は公式サイトに限定する」といったガードが必要です。

また、検索queryには個人情報や機密情報を入れない設計にします。問い合わせ本文をそのまま検索へ投げるのではなく、必要な固有名詞を落とし、製品名、エラーコード、公開情報だけでqueryを作る方が安全です。

設計ポイント理由
queryを300文字程度に制限する機密情報の混入と無駄な検索を減らす
検索結果をFirestoreへ保存するAI回答の根拠をあとから追える
providerを記録するCeramic、Exa、Linkupの比較や切り替えができる
重要回答は人が承認する検索結果の誤読や古い情報の混入を防ぐ

まとめ

Cloudflare Web Search API via AI Gatewayは、AIエージェントに「最新情報を探す入口」を持たせるための実務的なアップデートです。REST APIでもWorkers bindingでも呼び出せ、AI Gatewayのログ、課金、アクセス制御の流れに検索を載せられる点が魅力です。

Next.js + Firestoreのアプリでは、まず検索をサーバー側に閉じ込め、検索語、provider、結果URL、requestIdを台帳化するところから始めるのが安全です。AIに渡す前に検索結果を保存しておけば、回答の根拠をあとから確認でき、GitHub Actionsで定期的な鮮度チェックも回せます。

AIエージェントにWeb検索を持たせると、できることは一気に増えます。だからこそ、検索をブラックボックスにせず、監査できる小さな部品として組み込むのが、業務アプリでの第一歩になります。