AI Search:エージェント向け検索プリミティブ
2026-04-16 • Gabriel Massadas、Miguel Cardoso、Anni Wang • 読了約7分
すべてのエージェントは検索を必要とします。コーディングエージェントはリポジトリ内の何百万ものファイルを検索し、サポートエージェントは顧客チケットや社内ドキュメントを検索します。ユースケースは異なりますが、根本的な問題は同じです:モデルに適切な情報を適切なタイミングで渡すこと。
独自に検索を構築する場合、ベクトルインデックス、ドキュメントを解析・チャンク化するインデクシングパイプライン、データが変化したときにインデックスを最新に保つ仕組みが必要です。キーワード検索も必要なら、別のインデックスとその上の融合ロジックが必要になります。さらに各エージェントごとに検索可能なコンテキストが必要なら、それらをエージェント単位でセットアップしなければなりません。
AI Search(旧 AutoRAG)は、そのためのプラグアンドプレイな検索プリミティブです。インスタンスを動的に作成し、データを与え、Worker、Agents SDK、または Wrangler CLI から検索できます。
主な提供内容:
以下では、実際の利用例を見ていきます。
実践:カスタマーサポートエージェント
共通の製品ドキュメントと、顧客固有の履歴(過去の解決履歴)という2種類の知識を検索するサポートエージェントを例に説明します。製品ドキュメントはコンテキストウィンドウに収まりきらないことが多く、顧客ごとの履歴は解決ごとに増えていくため、関連する情報を取り出すために検索(retrieval)が必要です。
以下は AI Search と Agents SDK を使った構成例です。
まずプロジェクトをスキャフォールドします:
npm create cloudflare@latest
最初に、Worker に AI Search ネームスペースをバインドします(wrangler.jsonc の例):
{
"ai_search_namespaces": [
{
"binding": "SUPPORT_KB",
"namespace": "support"
}
],
"ai": {
"binding": "AI"
},
"durable_objects": {
"bindings": [
{
"name": "SupportAgent",
"class_name": "SupportAgent"
}
]
}
}
たとえば、共通の製品ドキュメントが R2 バケット product-doc にあるとします。Cloudflare Dashboard からサポート用ネームスペース内に、そのバケットをソースにした一時的(one-off)AI Search インスタンス product-knowledge を作成できます。
それが共有ナレッジベースで、すべてのエージェントが参照できるドキュメント群になります。顧客が再度問題を報告した際に、既に試したことを把握しておくと時間が節約できます。
顧客ごとの履歴は、顧客毎に AI Search インスタンスを作ることで追跡できます。各解決後に、エージェントは何が起こり、どのように直したかの要約を保存します。これにより過去の解決履歴の検索可能なログが蓄積されます。
ネームスペースバインディングを使ってインスタンスを動的に作成できます:
await env.SUPPORT_KB.create({ id: `customer-${customerId}`, index_method:{ keyword: true, vector: true } });
各インスタンスは専用の組み込みストレージとベクトルインデックス(R2 と Vectorize による)を持ちます。インスタンスは空の状態から始まり、時間とともにコンテキストが蓄積されます。次回顧客が戻ってきたとき、全てが検索可能です。
数名分の顧客が増えるとネームスペースは次のようになります:
- namespace: "support"
- product-knowledge (R2 をソースに、全エージェントで共有)
- customer-abc123 (managed storage, per-customer)
- customer-def456 (managed storage, per-customer)
- customer-ghi789 (managed storage, per-customer)
エージェント本体
エージェントは Agents SDK の AIChatAgent を拡張し、2つのツールを定義します。LLM には Workers AI を通じて Kimi K2.5 を使用します。モデルは会話に基づいてツールを呼ぶべきタイミングを決定します。
import { AIChatAgent, type OnChatMessageOptions } from "@cloudflare/ai-chat";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
import { routeAgentRequest } from "agents";
import { z } from "zod";
export class SupportAgent extends AIChatAgent<Env> {
async onChatMessage(_onFinish: unknown, options?: OnChatMessageOptions) {
const customerId = options?.body?.customerId;
if (customerId) {
try {
await this.env.SUPPORT_KB.create({ id: `customer-${customerId}`, index_method: { keyword: true, vector: true } });
} catch {
}
}
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/moonshotai/kimi-k2.5"),
system: `You are a support agent. Use search_knowledge_base to find relevant docs before answering. Search results include both product docs and this customer's past resolutions — use them to avoid repeating failed fixes and to recognize recurring issues. When the issue is resolved, call save_resolution before responding.`,
messages: await convertToModelMessages(this.messages),
tools: {
search_knowledge_base: tool({
description: "Search product docs and customer history",
inputSchema: z.object({
query: z.string().describe("The search query"),
}),
execute: async ({ query }) => {
const instances = ["product-knowledge"];
if (customerId) {
instances.push(`customer-${customerId}`);
}
return await this.env.SUPPORT_KB.search({
query: query,
ai_search_options: {
boost_by: [
{ field: "timestamp", direction: "desc" }
],
instance_ids: instances
}
});
}
}),
save_resolution: tool({
description: "Save a resolution summary after solving a customer's issue",
inputSchema: z.object({
filename: z.string().describe( "Short descriptive filename, e.g. 'billing-fix.md'" ),
content: z.string().describe( "What the problem was, what caused it, and how it was resolved" ),
}),
execute: async ({ filename, content }) => {
if (!customerId) return { error: "No customer ID" };
const instance = this.env.SUPPORT_KB.get( `customer-${customerId}` );
const item = await instance.items.uploadAndPoll( filename, content );
return { saved: true, filename, status: item.status };
}
}),
},
stopWhen: stepCountIs(10),
abortSignal: options?.abortSignal,
});
return result.toUIMessageStreamResponse();
}
}
export default {
async fetch(request: Request, env: Env) {
return (
(await routeAgentRequest(request, env)) || new Response("Not found", { status: 404 })
);
}
} satisfies ExportedHandler<Env>;
この仕組みでは、モデルが検索すべき時と保存すべき時を判断します。検索時には product-knowledge と当該顧客の過去の解決履歴を同時にクエリし、解決した後は要約を保存して次回以降に即座に検索可能にします。
AI Search がどのように目的の情報を見つけるか
AI Search は多段階の検索パイプラインを実行し、各ステップは設定可能です。
ハイブリッド検索:意図を理解しつつ用語も一致させる検索
これまでは AI Search はベクトル検索だけを提供していました。ベクトル検索は意図の理解に優れますが、詳細(特定の用語)を取りこぼすことがあります。例えばクエリ "ERR_CONNECTION_REFUSED timeout" では、埋め込みは接続失敗という概念を捉えますが、ユーザーは "ERR_CONNECTION_REFUSED" を含む特定のドキュメントを探しています。ベクトル検索は一般的なトラブルシューティングの結果を返すかもしれませんが、エラー文字列そのものを含むページを表面化しない可能性があります。
キーワード検索(BM25)はこのギャップを埋めます。BM25 はクエリ用語の出現頻度、コーパス全体での希少性、ドキュメント長を元にスコアリングします。特定の用語一致を重視し、一般的な助詞などを減点し、文書長で正規化します。クエリ "ERR_CONNECTION_REFUSED timeout" なら、BM25 は実際に "ERR_CONNECTION_REFUSED" を含むドキュメントを見つけます。とはいえ BM25 は「ネットワーク接続のトラブルシューティング」的なページを見逃すかもしれません。そこをベクトル検索が補完するため、両方が必要になります。
ハイブリッド検索を有効にするとベクトルとBM25を並列で実行し、結果を融合して(必要に応じて再ランク付けも行い)返します。
以下は BM25 の新しい設定とそれらがどのように組み合わさるかの概要です。
すべてのオプションは省略した場合に妥当なデフォルトが設定されています。新しいインスタンス作成時に必要な構成を指定できます:
const instance = await env.AI_SEARCH.create({
id: "my-instance",
index_method: { keyword: true, vector: true },
indexing_options: { keyword_tokenizer: "porter" },
retrieval_options: { keyword_match_mode: "or" },
fusion_method: "rrf",
reranking: true,
reranking_model: "@cf/baai/bge-reranker-base"
});
関連度のブースト:重要なものを表面化する
検索は関連性の高い結果を返しますが、関連性だけでは十分でないことがあります。たとえばニュース検索では「選挙結果」に関する記事が1週間前のものと3年前のものとで両方関連していても、ほとんどのユーザーはより最近の記事を欲しがるでしょう。ブースト(boosting)はドキュメントのメタデータに基づいてランキングを押し上げることで、ビジネスロジックを検索結果に重ねることを可能にします。