このコンテンツは自動機械翻訳サービスによる翻訳版であり、皆さまの便宜のために提供しています。原本の英語版と異なる誤り、省略、解釈の微妙な違いが含まれる場合があります。ご不明な点がある場合は、英語版原本をご確認ください。
すべてのエージェントは検索が必要です。コーディングエージェントはリポジトリの数百万ファイルを検索し、サポートエージェントは顧客チケットや社内ドキュメントを検索します。ユースケースは異なりますが、根本的な問題は同じです。適切なタイミングでモデルに適切な情報を得ることです。
自分で検索を構築する場合は、ベクトルインデックス、ドキュメントを解析してチャンク化するインデックス作成パイプライン、そしてデータが変更されたときにインデックスを最新に保つためのものが必要になります。キーワード検索も必要な場合は、それは別のインデックスと融合ロジックです。また、各エージェントに独自の検索可能なコンテキストが必要な場合は、エージェントごとにすべて設定することができます。
AI Search(旧AutoRAG)は、必要なプラグアンドプレイの検索プリミティブです。Worker、Agents SDK、またはWrangler CLIを使用して、インスタンスを動的に作成し、データを提供し、検索することができます。内容は以下の通りです。
ハイブリッド検索。同じクエリで、セマンティックとキーワードの一致を有効にします。ベクトル検索とBM25は並行して実行され、結果は融合されます。(現在、当社ブログの検索はAI Searchによって行われています。右上の虫眼鏡アイコンをお試しください。)
内蔵されたストレージとインデックス。新しいインスタンスには、独自のストレージとベクトルインデックスが付属しています。APIを介してインスタンスに直接ファイルをアップロードすると、インデックスが作成されます。R2バケットのセットアップも、最初に接続する必要のある外部データソースもありません。新しいai_search_namespacesバインディングでは、Workerから実行時にインスタンスを作成/削除できるため、再デプロイなしに、エージェントごと、顧客ごと、または言語ごとに1つをスピンアップできます。
ドキュメントにメタデータを添付してクエリ時にランキングを上げるために使用したり、1回の呼び出しで複数のインスタンスにクエリを実行したりすることもできるようになりました。
では、これが実際に何を意味するのかを見ていきましょう。
共有製品ドキュメントと過去の解決方法のような顧客ごとの履歴という2種類のナレッジを検索するサポートエージェントについて説明します。製品ドキュメントはコンテキストウィンドウに収まらない容量です。また、解決された問題ごとに顧客の履歴も更新されるため、エージェントは情報を検索して関連性のある情報を見つける必要があります。
AI SearchとAgents SDKを使用した場合の例を次に示します。まずはプロジェクトのスキャフォールディングから始めましょう:
npm create cloudflare@latest -- --template cloudflare/agents-starter
まず、AI SearchネームスペースをWorkerにバインドします。
// wrangler.jsonc
{
"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インスタンスを作成することで追跡できます。各問題が解決されると、エージェントは何が問題だったのか、どのように修正したのかの概要を保存します。時間の経過とともに、これにより過去の解決状況を示す検索可能なログが蓄積されます。ネームスペースバインディングを使用して、動的にインスタンスを作成できます。
// create a per-customer instance when they first show up
await env.SUPPORT_KB.create({
id: `customer-${customerId}`,
index_method:{ keyword: true, vector: true }
});
各インスタンスは、R2とVectorizeを活用した独自の内蔵ストレージとベクトルインデックスを取得します。インスタンスは空から始まり、時間の経過とともにコンテキストを蓄積します。次回のお客様の訪問時には、そのすべてが検索可能です。
一部のお客様のネームスペースは次のようになります:
namespace: "support"
├── product-knowledge (R2 as source, shared across all agents)
├── customer-abc123 (managed storage, per-customer)
├── customer-def456 (managed storage, per-customer)
└── customer-ghi789 (managed storage, per-customer)
エージェント自体がそうです。Agents SDKのAIChatAgentを拡張し、2つのツールを定義しています。Workers AIを介し、LLMとしてKin 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) {
// the client passes customerId in the request body
// via the Agent SDK's sendMessage({ body: { customerId } })
const customerId = options?.body?.customerId;
// create a per-customer instance when they first show up.
// each instance gets its own storage and vector index.
if (customerId) {
try {
await this.env.SUPPORT_KB.create({
id: `customer-${customerId}`,
index_method: { keyword: true, vector: true }
});
} catch {
// instance already exists
}
}
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.`,
// this.messages is the full conversation history, automatically
// persisted by AIChatAgent across reconnects
messages: await convertToModelMessages(this.messages),
tools: {
// tool 1: search across shared product docs AND this
// customer's past resolutions in a single call
search_knowledge_base: tool({
description: "Search product docs and customer history",
inputSchema: z.object({
query: z.string().describe("The search query"),
}),
execute: async ({ query }) => {
// always search product docs;
// include customer history if available
const instances = ["product-knowledge"];
if (customerId) {
instances.push(`customer-${customerId}`);
}
return await this.env.SUPPORT_KB.search({
query: query,
ai_search_options: {
// surface recent docs over older ones
boost_by: [
{ field: "timestamp", direction: "desc" }
],
// search across both instances at once
instance_ids: instances
}
});
}
}),
// tool 2: after resolving an issue, the agent saves a
// summary so future agents have full context
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}`
);
// uploadAndPoll waits until indexing is complete,
// so the resolution is searchable before the next query
const item = await instance.items.uploadAndPoll(
filename, content
);
return { saved: true, filename, status: item.status };
}
}),
},
// cap agentic tool-use loops at 10 steps
stopWhen: stepCountIs(10),
abortSignal: options?.abortSignal,
});
return result.toUIMessageStreamResponse();
}
}
// route requests to the SupportAgent durable object
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はベクトル検索のみを提供していました。ベクトル検索は意図の理解に長けますが、具体性を失うことがあります。「ERR_CONNECTION_REFused timeout」クエリでは、エンベッディングは接続失敗の広範な概念を捉えています。しかし、ユーザーは一般的なネットワークドキュメントを探しているわけではありません。「ERR_CONNECTION_REFused」に記載された特定の文書を探しています。ベクトル検索は、その正確なエラー文字列を含むページを表示することなく、トラブルシューティングに関する結果を返す可能性があります。
キーワード検索がそのギャップを埋めるのです。AI Searchは、最も広く使用されている検索スコアリング機能の1つであるBM25をサポートするようになりました。BM25は、クエリー条件が表示される頻度、コーパス全体におけるその用語の出現頻度、文書に存在する期間によって文書を評価します。特定の条件で一致した場合に報酬を与え、一般的な単語を罰し、文書の長さを正規化します。「ERR_CONNECTION_REFused タイムアウト」を検索すると、BM25は実際に「ERR_CONNECTION_REFused」を用語として含むドキュメントを検索します。ただし、BM25では、同じ問題が記述されているにもかかわらず、「ネットワーク接続のトラブルシューティング」に関するページは見逃される可能性があります。そこではベクトル検索の出番であり、両方が必要なのです。
ハイブリッド検索を有効にすると、ベクトルとBM25を並行して実行し、結果を融合させ、必要に応じて再ランク付けします:
BM25の新しい設定とその組み合わせを見てみましょう。
トークナイザーは、インデックス作成時に文書が一致する条件にどう分割されるかを制御します。Porter Stemmer(オプション:porter)は、「running」を「run」に一致させるように単語を語幹に変換します。Trigram (オプション:trigram) は、文字の部分文字列と一致するため、"conf" は "configuration" と一致します。ドキュメントのような自然言語コンテンツにはporterを使用し、部分一致が重要なコードにはtrigramを使用することができます。
キーワード一致モードで、クエリ時にBM25のスコアリング対象となるドキュメントを管理します。ANDは、すべてのクエリー条件を文書内に表示する必要があるか、少なくとも1つの一致するものを含めます。
Fusion は、クエリ時間中に、ベクトルとキーワードの結果をどのように最終的な結果リストに結合するかを制御します。再帰的ランクフュージョン(オプション: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年前の記事の両方が、「選挙結果」に意味的に関連するかもしれませんが、ほとんどのユーザーはおそらく最近の記事を求めているでしょう。Boostingを使用すると、ドキュメントのメタデータに基づいてランキングを急上昇させることで、検索の上にビジネスロジックを重ねることができます。
タイムスタンプ(すべての項目に組み込まれている)または定義したカスタムメタデータフィールドに基づいてブーストできます。
// boost high priority docs
const results = await instance.search({
query: "deployment guide",
ai_search_options: {
boost_by: [
{ field: "timestamp", direction: "desc" }
]
}
});
サポートエージェントの例では、製品ドキュメントとお客様の解決履歴は設計上、別々のインスタンスに存在します。しかし、エージェントが質問に応答する場合、一度に両方の場所からのコンテキストが必要です。インスタンス間検索がなければ、2つの別々の呼び出しを行い、結果を自分で統合することになります。
ネームスペースのバインディングは、これを処理するsearch()メソッドを公開します。インスタンス名の配列を渡して、ランク付けされたリストを 1 つ取得する:
const results = await env.SUPPORT_KB.search({
query: "billing error",
ai_search_options: {
instance_ids: ["product-knowledge", "customer-abc123"]
}
});
結果はインスタンス間で統合され、ランク付けされます。エージェントは、共有ドキュメントと顧客解決の履歴が別々の場所に存在することを知る必要はなく、気にする必要はありません。
ここまで、AI Searchが適切な結果を見つける方法について説明してきました。次は、検索インスタンスの作成と管理方法です。
このリリース以前にAI Searchを使用していれば、R2バケットを作成し、AI Searchインスタンスにリンクし、AI SearchはサービスAPIトークンを生成し、アカウントにプロビジョニングされるVectorizeインデックスを管理します。オブジェクトをアップロードするには、R2に書き込み、同期ジョブが実行されるのを待ってオブジェクトにインデックスを付ける必要があります。
作成された新しいインスタンスは、他とは異なる動作をします。create()を呼び出すと、インスタンスには独自のストレージとベクトルインデックスが組み込まれます。ファイルをアップロードすると、ファイルはすぐにインデックスに送信され、インデックス作成ステータスを1つのuploadAndpoll() APIで調査できます。完了したら、すぐにインスタンスを検索でき、外部依存関係も連携させる必要はありません。
const instance = env.AI_SEARCH.get("my-instance");
// upload and wait for indexing to complete
const item = await instance.items.uploadAndPoll("faq.md", content, {
metadata: { category: "onboarding" }
});
console.log(item.status); // "completed"
// immediately search after indexing is completed
const results = await instance.search({
// alternative way to pass in users' query other than using parameter query
messages: [{ role: "user", content: "onboarding guide" }],
});
各インスタンスは、1つの外部データソース(R2バケットまたはWebサイト)に接続し、同期スケジュールで実行することもできます。提供されたビルトインストレージと並行して存在することができます。サポートエージェントの例では、product-knowledgeは共有ドキュメント用にR2バケットによって支えられ、各お客様のインスタンスは内蔵型ストレージを使用してその場でアップロードされるコンテキストを使用します。
ai_search_namespacesは、実行時に検索インスタンスを動的に作成するために活用できる新しいバインディングです。これは、以前のenv.AI.autorag()を置き換えるものです。AIバインディングを通じてAI SearchにアクセスしたAPI。古いバインディングは、Workers の互換性日付を使用して引き続き機能します。
// wrangler.jsonc
{
"ai_search_namespaces": [
{ "binding": "AI_SEARCH", "namespace": "example" },
]
}
ネームスペースバインディングは、ネームスペースレベルでcreate()、delete()、list()、search()などのAPIを提供します。インスタンスを動的に作成する場合(例:エージェントごと、カスタマー、テナントごと)、これは使用するバインディングです。
// create an instance
const instance = await env.AI_SEARCH.create({
id: "my-instance"
});
// delete an instance and all its indexed data
await env.AI_SEARCH.delete("old-instance");
本日作成された新規インスタンスは、内蔵ストレージとベクトルインデックスを自動的に取得します。
これらのインスタンスは、AI Searchのオープンベータ版中は、以下に記載された制限付きで無料でご利用いただけます。Webサイトをデータソースとして使用する場合、Browser Run(旧ブラウザレンダリング)を使用したWebサイトのクローリングも、ビルトインのサービスとなり、個別に請求されることはありません。ベータ版提供後は、AI Searchを単一のサービスとして統一し、基本となるコンポーネントごとに個別に請求するのではなく、単一のサービスとして統一した価格設定を目指しています。Workers AIとAI Gatewayの使用量は引き続き個別に請求されます。
請求が開始される前に、少なくとも30日前に通知と価格の詳細をお伝えします。
制限 | Workers 無料 | Workers 有料 |
|---|
アカウントあたりのAI Searchインスタンス数 | 100 | 5,000 |
インスタンスあたりのファイル数 | 100,000 | ハイブリッド検索は100万または50万 |
最大ファイルサイズ | 4MB | 4MB |
月間のクエリー数 | 20,000 | 無制限 |
1日あたり最大クロールページ数 | 500 | 無制限 |
既存のインスタンスはどうですか?
このリリース以前にインスタンスを作成した場合は、現在と同じように機能し続けます。R2バケット、Vectorizeインデックス、Browser Runの使用量はアカウントに引き続き使用され、課金も以前のままとなります。既存インスタンスの移行詳細は、近日中にお知らせいたします。
検索は、エージェントができる最も基本的なことの1つです。AI Searchを使用すれば、インフラを構築する必要はありません。インスタンスを作成し、データを渡して、エージェントに検索させましょう。
今日から、このコマンドを実行して最初のインスタンスを作成しましょう。
npx wrangler ai-search create my-search
ドキュメントをご覧になり、Cloudflare Developer Discordで何を構築しているか、ぜひお聞かせください。