每个智能体都需要搜索功能:编码智能体需要搜索代码存储库中数百万个文件,支持智能体则需要搜索客户工单和内部文档。尽管用例各不相同,但根本问题是一样的:及时将正确的信息传递给模型。
如果您自行构建搜索功能,则需要一个向量索引、一个用于解析和分块文档的索引管道,以及在数据更改时保持索引同步更新的某种方法。如果您还需要关键词搜索,则需要一个单独的索引以及顶部的融合逻辑。如果每个智能体都需要自己的可搜索上下文,则需要为每个智能体分别设置。
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 超时”查询中,嵌入会捕捉连接失败的宽泛概念。但用户并不是在寻找通用网络文档,而是在寻找提到“ERR_CONNECTION_REFUSED”的特定文档。向量搜索可能会返回关于故障排除的结果,但不会显示包含该确切错误字符串的页面。
关键词搜索可以弥补这种不足。AI Search 现在支持 BM25,这是应用最广泛的检索评分函数之一。BM25 根据查询术语出现频率、这些术语在整个语料库中的稀有程度以及文档长度,对文档进行评分。它会奖励特定术语的匹配,惩罚常用填充词,并对文档长度进行规范化。搜索“ERR_CONNECTION_REFUSED 超时”时,BM25 会找到包含“ERR_CONNECTION_REFUSED”这个术语的文档。然而,BM25 可能会遗漏关于“排查网络连接”的网页,即便该网页可能描述了相同的问题。这正是向量搜索的优势所在,也是您需要同时使用这两种搜索方式的原因。
启用混合搜索后,它会并行运行向量和 BM25,融合搜索结果,以及根据需要可选地对结果进行重新排序:
我们来看看 BM25 的新配置,以及它们如何协同工作。
分词器控制着在索引文档时如何将其拆分成可匹配的术语。波特词干提取器(选项:porter)提取单词的词干,因此“running”是“run”的匹配项。字母三元组(选项:trigram)匹配字符子字符串,因此“conf”是“configuration”的匹配项。您可以使用波特方法处理文档等自然语言内容,以及使用字母三元组方法处理部分匹配至关重要的代码。
关键词匹配模式控制着哪些文档是查询时用于 BM25 评分的候选文档。AND 要求查询的所有术语必须出现在文档中,OR 则只须包含至少有一个匹配项的文档。
融合控制着查询时如何将向量和关键词结果合并到最终结果列表中。倒数排名融合(选项:rrf)按排名位置而不是分数合并,以免比较两个不兼容的评分标准;最大融合(选项: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"
});
检索功能可以为您提供相关结果,但仅靠相关性还是不够的。例如,在新闻搜索中,上周的文章与三年前的文章可能都与“选举结果”语义相关,但大多数用户可能希望查看最新的文章。提高相关性让您在检索时通过根据文档元数据对排名进行微调,在检索基础上添加业务逻辑。
您可以根据时间戳(每个项目都有)或通过自定义元数据字段来提高相关性。
// 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 |
每月的查询量 | 20000 | 无限制 |
每天的最大抓取页面数 | 500 | 无限制 |
现有实例怎么办?
如果您在此版本发布之前创建了实例,它们将继续像现在一样正常运行。R2 存储桶、Vectorize 索引和 Browser Run 使用量均保留在您的账户中,并按照以前的方式计费。我们将尽快分享现有实例迁移的详细信息。
搜索是智能体最基本的功能之一。使用 AI Search,您无需构建任何基础设施即可搜索。创建实例、为其提供数据,即可让智能体进行搜索。
立即运行以下命令,创建您的第一个实例:
npx wrangler ai-search create my-search
欢迎查看文档并加入 Cloudflare Developer Discord,分享您正在构建的应用。