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

はじめに
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 Blog: Introducing Web Search API via AI Gateway
- Cloudflare Docs: How to use Web Search API
- Cloudflare AI Gateway Changelog: Introducing Web Search API
- Cloudflare Docs: Web Search API providers
何が新しいのか
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 endpoint | POST /client/v4/accounts/{account_id}/ai/websearch/ |
| Workers binding | env.AI.websearch() |
| provider | ceramic、exa、linkup |
| limit | 1〜10、defaultは10 |
| BYOK | byokAlias で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検索を持たせると、できることは一気に増えます。だからこそ、検索をブラックボックスにせず、監査できる小さな部品として組み込むのが、業務アプリでの第一歩になります。