每個智慧體都需要搜尋功能:編碼智慧體需要在數百萬個儲存庫檔案中搜尋,客服智慧體則需要查找客戶工單與內部文件。使用情境各不相同,但根本問題是一樣的:在正確的時機將正確的資訊提供給模型。
如果您要自己打造搜尋功能,就需要向量索引、能夠剖析並切割文件的索引管線,還需要一套機制在資料變更時更新索引。若需關鍵字搜尋,則需額外建立獨立索引與融合邏輯。而如果您的每個智慧體都需要自己專屬的可搜尋脈絡,您就得為每個智慧體重複設定這一切。
AI Search(原名 AutoRAG)就是您所需的隨插即用搜尋基礎組件。您可以動態建立執行個體、輸入資料,然後開始搜尋——無論透過 Worker、Agents SDK 或 Wrangler CLI 皆可。以下是我們推出的功能:
混合搜尋。單一查詢同時啟用語義與關鍵字比對。向量搜尋與 BM25 並行執行,結果自動融合。(本部落格的搜尋功能現在由 AI Search 驅動,請點擊右上方放大鏡圖示體驗。)
內建儲存與索引。新的執行個體會自帶儲存空間與向量索引。您可以直接透過 API 上傳檔案到某個執行個體,系統就會自動建立索引。無需額外設定 R2 儲存桶,也不必先連接外部資料來源。新的 ai_search_namespaces 繫結讓您可以從 Worker 在執行期間動態建立、刪除執行個體,因此您可以為每個智慧體、每位客戶、每種語言分別建立一個執行個體,而無需重新部署。
現在,您還可以為文件附加中繼資料,並在查詢時用來提升排名,也能在單一呼叫中跨多個執行個體進行查詢。
接下來,讓我們透過實際案例,深入探討這些功能究竟意味著什麼。
讓我們來看看客服智慧體,它需要搜尋兩種知識:一種是共用產品文件,另一種是每個客戶的歷程記錄(例如過去解決過的案例)。產品文件內容太多,無法全部塞進脈絡視窗,而每位客戶的歷程記錄會隨著每次解決問題而不斷累積,所以智慧體必須透過擷取來找出相關內容。
以下展示如何透過 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 並定義兩種工具。我們透過 Workers AI,將 Kimi K2.5 用作 LLM,由模型根據對話內容判斷何時呼叫工具:
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 現已支援 BM25,這是業界最廣泛採用的擷取評分函式之一。BM25 根據查詢詞彙出現頻率、詞彙在整體文件集的稀有程度,以及文件長度進行評分。它能獎勵特定詞彙的比對結果,濾除常見虛詞,並根據文件長度正規化評分。當您搜尋「ERR_CONNECTION_REFUSED timeout」時,BM25 能準確找出包含該錯誤碼的文件。然而,BM25 演算法也可能遺漏某些關於「網路連線疑難排解」的頁面,儘管這些頁面所描述的問題可能與您的查詢內容完全一致。這正是向量搜尋的優勢所在,也彰顯兩者並用的必要性。
啟用混合搜尋後,系統將並行執行向量與 BM25,融合結果並可選擇性進行重新排序:
讓我們來看看 BM25 的新設定,以及它們是如何協同運作的。
分詞器 (Tokenizer) 控制在建立索引時,如何將您的文件切割成可被比對的詞彙。Porter 詞幹提取器(選項:porter)會將單詞還原為詞幹,使「running」與「run」相符。Trigram(選項:trigram)會比對字元子字串,使「conf」與「configuration」相符。您可以對文件等自然語言內容使用 porter,對需要部分比對的程式碼使用 trigram。
關鍵字比對模式 (Keyword match mode) 控制在查詢時,哪些文件會成為 BM25 評分的候選項目。AND 要求文件中必須出現所有查詢詞彙;OR 則會納入任何至少有一個詞彙相符的文件。
融合 (Fusion) 控制在查詢時,如何將向量結果與關鍵字結果合併成最終的結果清單。互補排名融合(選項:rrf)會根據排名位置而非分數來合併,避免比較兩種不相容的評分尺度;而最大值融合(選項:max)則會取較高的分數。
(選用)重新排序 (Reranking) 會額外加入一個交叉編碼器步驟,將查詢與文件搭配成對來重新評分。這有助於捕捉那些結果雖然包含正確詞彙,但實際上並未回答問題的情況。
在使用者省略不填時,所有選項皆有合理的預設值。您可在建立新執行個體時,根據需求靈活設定重要參數:
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"
});
擷取機制能獲取相關結果,但僅有關聯性往往不足。在新聞搜尋中,上週的文章和三年前的文章可能都與「選舉結果」語意相關,但大多數使用者可能想要的是最近的那一篇。透過提升功能,您可以在擷取結果之上疊加業務邏輯,根據文件的中繼資料來微調排名。
您可以根據時間戳記(內建於每個項目之中)或任何您自訂的中繼資料欄位來進行提升。
// boost high priority docs
const results = await instance.search({
query: "deployment guide",
ai_search_options: {
boost_by: [
{ field: "timestamp", direction: "desc" }
]
}
});
在客服智慧體的範例中,產品文件與客戶歷史解決記錄是刻意存放在不同執行個體中的。但是當智慧體在回答問題時,它需要同時從兩個來源取得情境資訊。如果沒有跨執行個體搜尋,您就必須分別呼叫兩次,然後自己合併結果。
命名空間繫結提供了一個 search() 方法來為您處理這件事。傳入一個執行個體名稱的陣列,就能得到一份經過合併排名後的結果清單:
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() 時,該執行個體會自帶內建的儲存空間與向量索引。您可以直接上傳檔案,檔案會立即被送入索引,而且您可以透過同一個 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" }],
});
每個執行個體還可以連接到一個外部資料來源(例如一個 R2 儲存桶或一個網站),並依照排程進行同步。這項功能可以與內建儲存空間並存。在客服智慧體的範例中,product-knowledge 由 R2 儲存桶提供支援,用於存放共用文件;而每位客戶的執行個體則使用內建儲存空間,用於存放即時上傳的脈絡資訊。
ai_search_namespaces 是一種全新的繫結,您可以利用它在執行時期動態建立搜尋執行個體。它取代了先前的 env.AI.autorag() API(後者是透過 AI 繫結來存取 AI Search)。舊的繫結將透過 Workers 相容性日期設定繼續運作。
// wrangler.jsonc
{
"ai_search_namespaces": [
{ "binding": "AI_SEARCH", "namespace": "example" },
]
}
命名空間繫結提供了命名空間層級的 API,例如 create()、delete()、list() 和 search()。如果您需要動態建立執行個體(例如為每個智慧體、每位客戶或每個租用戶分別建立),此即為適用方案。
// 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 公開測試期間,這些執行個體可免費使用,並適用下列限制。當使用網站作為資料來源時,透過 Browser Run(原為 Browser Rendering)進行的網站爬取現已成為內建服務,代表不會另行計費。測試版結束後,我們的目標是為 AI Search 提供統一的單一服務定價,而不是針對底層各個元件分別計費。Workers AI 與 AI Gateway 的使用則會繼續維持分別計費。
在開始任何計費之前,我們會提前至少 30 天通知,並公布定價細節。
限制 | Workers Free | Workers Paid |
|---|
每個帳戶的 AI Search 執行個體數量 | 100 | 5,000 |
每個執行個體的檔案數 | 100,000 | 100 萬(混合搜尋為 50 萬) |
檔案大小上限 | 4MB | 4MB |
每月查詢數 | 20,000 | 無限 |
每天爬取頁面數上限 | 500 | 無限 |
既有執行個體的處理方式
如果您在此次發布前就已經建立執行個體,這些執行個體會維持原本的運作方式,不受影響。您的 R2 儲存桶、Vectorize 索引及 Browser Run 用量仍然會留在您的帳戶中,並按照先前的方式計費。我們將會儘快提供現有執行個體的遷移細節。
搜尋是智慧體最基礎的功能之一。有了 AI Search,您無需自行打造基礎架構。建立執行個體、輸入資料,即可讓您的智慧體開始進行搜尋。
立即執行以下指令建立您的第一個執行個體:
npx wrangler ai-search create my-search
歡迎查閱技術文件,並前往 Cloudflare 開發人員 Discord 社群分享您的建置成果。