すべてのエージェントには検索が必要です。コーディングエージェントはリポジトリ全体の数百万のファイルを検索し、サポートエージェントはカスタマーチケットと内部ドキュメントを検索します。ユースケースは異なりますが、根本的な問題は同じです。適切なタイミングで適切な情報をモデルに提供することです。
検索を自分で構築する場合、ベクトルインデックス、ドキュメントを解析・分割するインデックスパイプライン、データが変更されたときにインデックスを最新に保つ仕組みが必要です。キーワード検索も必要な場合は、別のインデックスと融合ロジックが必要になります。さらに、各エージェントが独自の検索可能なコンテキストを必要とする場合は、エージェントごとにすべてをセットアップする必要があります。
AI Search(旧称 AutoRAG)は、必要なプラグアンドプレイ検索プリミティブです。動的にインスタンスを作成し、データを提供して検索できます。Worker、Agents SDK、または Wrangler CLI から利用できます。
提供される機能
ハイブリッド検索
同じクエリで意味的マッチングとキーワードマッチングの両方を有効にします。ベクトル検索と BM25 が並列で実行され、結果が融合されます。(当社のブログの検索は現在 AI Search で動作しています。右上の虫眼鏡アイコンをお試しください。)
組み込みストレージとインデックス
新しいインスタンスには独自のストレージとベクトルインデックスが付属しています。API 経由でインスタンスに直接ファイルをアップロードすると、自動的にインデックスされます。R2 バケットをセットアップしたり、外部データソースを事前に接続したりする必要はありません。
新しい ai_search_namespaces バインディングを使用すると、Worker から実行時にインスタンスを作成・削除できるため、エージェントごと、顧客ごと、言語ごとに 1 つずつスピンアップでき、再デプロイは不要です。
ドキュメントにメタデータを添付し、クエリ時にランキングをブーストするために使用できるようになりました。また、単一の呼び出しで複数のインスタンスにまたがってクエリできます。
実践例:カスタマーサポートエージェント
2 種類の知識を検索するサポートエージェントを見てみましょう。共有製品ドキュメントと、過去の解決策などの顧客ごとの履歴です。製品ドキュメントはコンテキストウィンドウに収まらないほど大きく、各顧客の履歴は解決された問題ごとに増加するため、エージェントは関連情報を見つけるために検索が必要です。
AI Search と Agents SDK を使用した場合の例を見てみましょう。
プロジェクトのスキャフォルディング
npm create cloudflare@latest -- --template cloudflare/agents-starter
AI Search 名前空間をバインド
まず、AI Search 名前空間を Worker にバインドします。
{
"ai_search_namespaces": [
{
"binding": "SUPPORT_KB",
"namespace": "support"
}
],
"ai": {
"binding": "AI"
},
"durable_objects": {
"bindings": [
{
"name": "SupportAgent",
"class_name": "SupportAgent"
}
]
}
}
共有知識ベースの作成
共有製品ドキュメントが product-doc という R2 バケットにあるとします。Cloudflare ダッシュボード内の support 名前空間で、バケットを使用する 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 (管理ストレージ、顧客ごと)
├── customer-def456 (管理ストレージ、顧客ごと)
└── customer-ghi789 (管理ストレージ、顧客ごと)
エージェントの実装
エージェント自体は Agents SDK の AIChatAgent を拡張し、2 つのツールを定義します。Workers AI 経由で LLM として 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」という正確なエラーを記載しているドキュメントを探しています。ベクトル検索は、そのエラー文字列を含むページを表示することなく、トラブルシューティングに関する結果を返す可能性があります。
キーワード検索がそのギャップを埋めます。AI Search は現在、最も広く使用されている検索スコアリング関数の 1 つである BM25 をサポートしています。BM25 は、クエリ用語がどのくらい頻繁に表示されるか、それらの用語がコーパス全体でどのくらい稀であるか、ドキュメントの長さに基づいてドキュメントをスコアリングします。特定の用語のマッチを報酬し、一般的なフィラーワードにペナルティを与え、ドキュメント長を正規化します。
「ERR_CONNECTION_REFUSED timeout」を検索すると、BM25 は実際に「ERR_CONNECTION_REFUSED」という用語を含むドキュメントを見つけます。ただし、BM25 は「ネットワーク接続のトラブルシューティング」に関するページを見落とす可能性があります。これはベクトル検索が輝く場所であり、両方が必要な理由です。
ハイブリッド検索を有効にすると、ベクトルと BM25 が並列で実行され、結果が融合され、オプションで再ランク付けされます。
BM25 の新しい設定
トークナイザーはドキュメントがインデックス時にマッチ可能な用語にどのように分割されるかを制御します。
- Porter ステマー(オプション:
porter)は単語をステミングするため、「running」は「run」にマッチします。
- トライグラム(オプション:
trigram)は文字部分文字列にマッチするため、「conf」は「configuration」にマッチします。
ドキュメントなどの自然言語コンテンツには porter を使用し、部分マッチが重要なコードにはトライグラムを使用できます。
キーワードマッチモードはクエリ時に BM25 スコアリングの候補となるドキュメントを制御します。
- AND はすべてのクエリ用語がドキュメントに表示される必要があります。
- OR は少なくとも 1 つのマッチを持つ任意のものを含めます。
融合はクエリ時に結果の最終リストにベクトルとキーワード結果がどのように組み合わされるかを制御します。
- 相互ランク融合(オプション:
rrf)はランク位置によってマージするため、2 つの互換性のないスコアリングスケールの比較を回避します。
- 最大融合(オプション:
max)はより高いスコアを取得します。
(オプション)再ランク付けは、クエリとドキュメントをペアとして一緒に評価することで結果を再スコアリングするクロスエンコーダーパスを追加します。結果が正しい用語を持っているが質問に答えていない場合を捕捉するのに役立ちます。
省略された場合、すべてのオプションに適切なデフォルトがあります。新しいインスタンスを作成するときはいつでも、重要な内容を設定する柔軟性があります。
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"
});
関連性をブースト:重要なものを表示
検索は関連結果を取得しますが、関連性だけでは常に十分ではありません。たとえば、ニュース検索では、先週の記事と 3 年前の記事の両方が「選挙結果」に意味的に関連している可能性がありますが、ほとんどのユーザーはおそらく最近のものを望んでいます。ブーストを使用すると、ドキュメントメタデータに基づいてランキングを調整することで、検索の上にビジネスロジックをレイヤーできます。