Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

AI Search:为你的 agent 而生的搜索原语

原文:AI Search: the search primitive for your agents Source: https://blog.cloudflare.com/ai-search-agent-primitive/

2026-04-16

每个 agent 都需要搜索:编码 agent 在仓库中搜索数百万个文件,或者支持 agent 搜索客户工单和内部文档。用例不同,但底层问题是一样的:在合适的时间将正确的信息送到模型面前。

如果你自己构建搜索,你需要一个向量索引、一个解析并切分文档的索引管线,以及一个在数据变化时让索引保持最新的东西。如果你还需要关键词搜索,那是一个独立的索引,加上在它之上的融合逻辑。如果你的每个 agent 都需要自己的可搜索上下文,你就要为每个 agent 设置所有这些。

AI Search(原 AutoRAG)就是你需要的即插即用的搜索原语。你可以动态创建实例,把数据交给它,然后搜索——从 Worker、Agents SDK 或 Wrangler CLI。下面是我们正在发布的内容:

  • 混合搜索。在同一个查询中同时启用语义匹配和关键词匹配。向量搜索和 BM25 并行运行,结果融合在一起。(我们博客的搜索现在由 AI Search 提供支持。请试试右上角的放大镜图标。)

  • 内置存储和索引。 新实例自带其存储和向量索引。通过 API 直接将文件上传到一个实例,它们就被索引了。无需设置 R2 bucket,无需先连接外部数据源。新的 ai_search_namespaces binding 让你可以从你的 Worker 在运行时创建和删除实例,所以你可以为每个 agent、每个客户或每种语言启动一个,无需重新部署。

你现在还可以给文档附加 metadata,在查询时用它来提升排名,以及在单次调用中跨多个实例查询。

现在,让我们看看这在实践中意味着什么。

实战:客户支持 Agent

我们走过一个 support agent 的例子,它搜索两种知识:共享的产品文档,以及像过去解决方案那样的 per-customer 历史。产品文档太大,无法装入上下文窗口,而每个客户的历史随每次解决的 issue 增长,所以 agent 需要检索来找到相关内容。

下面是用 AI Search 和 Agents SDK 实现的样子。从脚手架一个项目开始:

npm create cloudflare@latest -- --template cloudflare/agents-starter

首先,把 AI Search namespace 绑定到你的 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 bucket 中。你可以在 Cloudflare Dashboard 上,在 support namespace 中,以该 bucket 为后端创建一次性的 AI Search 实例(命名为 product-knowledge):

那是你的共享知识库,每个 agent 都可以引用的文档。

当一个客户带着新问题回来时,知道之前已经尝试过什么可以为大家节省时间。你可以通过为每个客户创建一个 AI Search 实例来追踪它。每次解决一个问题后,agent 会保存一份关于哪里出了问题以及如何修复的总结。随着时间推移,这构建了一份过去解决方案的可搜索日志。你可以使用 namespace binding 动态创建实例:

// create a per-customer instance when they first show up 
await env.SUPPORT_KB.create({
  id: `customer-${customerId}`,
  index_method:{ keyword: true, vector: true }
});

每个实例得到自己的内置存储和向量索引——由 R2Vectorize 提供支持。实例从空开始,随时间累积上下文。下次客户回来时,所有这些都是可搜索的。

下面是几个客户之后这个 namespace 的样子:

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)

现在是 agent 本身。它扩展了 Agents SDK 的 AIChatAgent,定义了两个工具。我们使用 Kimi K2.5 作为 LLM,通过 Workers AI。模型根据对话决定何时调用工具:

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 运行一个多步骤的检索管线,其中每一步都是可配置的。

Hybrid Search:理解意图并匹配术语的搜索

到目前为止,AI Search 只提供向量搜索。向量搜索擅长理解意图,但可能丢失具体细节。在查询 “ERR_CONNECTION_REFUSED timeout” 中,embedding 捕获了连接故障的宽泛概念。但用户不是在找一般的网络文档。他们要找的是提到 “ERR_CONNECTION_REFUSED” 的具体那篇文档。向量搜索可能返回关于故障排除的结果,而不必浮出包含该确切错误字符串的页面。

关键词搜索填补了这个空白。AI Search 现在支持 BM25,这是最广泛使用的检索打分函数之一。BM25 根据查询词在文档中出现的频率、这些词在整个语料库中的稀有程度,以及文档的长度来给文档打分。它奖励特定术语的匹配,惩罚常见填充词,并按文档长度归一化。当你搜索 “ERR_CONNECTION_REFUSED timeout” 时,BM25 找到实际包含 “ERR_CONNECTION_REFUSED” 这个术语的文档。然而,BM25 可能会错过一篇关于“网络连接故障排除“的页面,即使它描述的是同一问题。这是向量搜索的强项,也是为什么你需要两者。

当你启用 hybrid search 时,它并行运行向量和 BM25,融合结果,可选地重排:

让我们看看 BM25 的新配置,以及它们如何组合在一起。

  1. Tokenizer 控制你的文档在索引时如何被切分成可匹配的术语。Porter stemmer(选项:porter)对单词进行词干提取,这样 “running” 匹配 “run”。Trigram(选项:trigram)匹配字符子串,这样 “conf” 匹配 “configuration”。你可以用 porter 处理像文档这样的自然语言内容,用 trigram 处理代码,因为代码中部分匹配很重要。

  2. Keyword match mode 控制查询时哪些文档作为 BM25 打分的候选。AND 要求所有查询词出现在一个文档中,OR 包含至少有一个匹配的任何文档。

  3. Fusion 控制查询时向量和关键词结果如何被组合成最终的结果列表。Reciprocal rank fusion(选项:rrf)按排名位置而非分数合并,这样避免比较两个不兼容的打分尺度,而 max fusion(选项:max)取较高分数。

  4. (可选)Reranking 添加一个 cross-encoder 通道,通过一起评估查询和文档作为一对来重新打分。它有助于捕获结果有正确术语但没有回答问题的情况。

每个选项在省略时都有合理的默认值。每次创建一个新实例时,你都可以灵活地配置重要的事情:

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 relevance:浮出重要的内容

检索给你相关的结果,但仅有相关性并不总是够的。例如,在新闻搜索中,上周的一篇文章和三年前的一篇文章在语义上可能都与“选举结果“相关,但大多数用户可能想要最近的那个。Boosting 让你可以在检索之上叠加业务逻辑,根据文档元数据微调排名。

你可以基于 timestamp(每个 item 都内置)或你定义的任何自定义元数据字段进行 boost。

// boost high priority docs
const results = await instance.search({
  query: "deployment guide",
  ai_search_options: {
    boost_by: [
      { field: "timestamp", direction: "desc" }
    ]
  }
});

Cross-instance search:跨边界查询

在 support agent 例子中,产品文档和客户解决方案历史按设计存放在独立的实例中。但当 agent 回答问题时,它需要同时来自两个地方的上下文。没有 cross-instance search,你需要做两次单独的调用并自己合并结果。

Namespace binding 暴露了一个 search() 方法为你处理这件事。传入一个实例名数组,得到一个排名好的列表:

const results = await env.SUPPORT_KB.search({
  query: "billing error",
  ai_search_options: {
    instance_ids: ["product-knowledge", "customer-abc123"]
  }
});

结果在不同实例间合并和排名。Agent 不需要知道或关心共享文档和客户解决方案历史存放在不同的地方。

AI Search 实例如何工作

到此,我们覆盖了 AI Search 如何找到正确的结果。现在让我们看看你如何创建和管理你的搜索实例。

如果你在这次发布之前用过 AI Search,你知道这个流程:创建一个 R2 bucket,把它链接到一个 AI Search 实例,AI Search 为你生成一个 service API token,你管理在你账户上配给的 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 bucket 或网站),并按同步计划运行。它可以与提供的内置存储并存。在 support agent 例子中,product-knowledge 由一个 R2 bucket 支持用于共享文档,而每个客户的实例使用内置存储用于即时上传的上下文。

Namespaces:在运行时创建搜索实例

ai_search_namespaces 是一个新的 binding,你可以利用它在运行时动态创建搜索实例。它取代了之前的 env.AI.autorag() API,后者通过 AI binding 访问 AI Search。旧的 binding 将通过 Workers compatibility dates 继续工作。

// wrangler.jsonc 
{
  "ai_search_namespaces": [
    { "binding": "AI_SEARCH", "namespace": "example" },
  ]
}

Namespace binding 在 namespace 层提供 create()delete()list()search() 等 API。如果你正在动态创建实例(例如,per agent、per customer、per tenant),这就是要使用的 binding。

// 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 处于 open beta 期间使用免费,有以下限制。当使用网站作为数据源时,使用 Browser Run(原 Browser Rendering) 进行的网站爬取现在也是一项内置服务,意味着你不会因此被单独计费。Beta 之后,目标是为 AI Search 作为单一服务提供统一定价,而不是为每个底层组件单独计费。Workers AI 和 AI Gateway 用量将继续单独计费。

我们将至少提前 30 天通知,并在任何计费开始之前传达定价细节。

限制

Workers Free

Workers Paid

每账户的 AI Search 实例数

100

5,000

每实例的文件数

100,000

1M,或 hybrid search 时 500K

最大文件大小

4MB

4MB

每月查询次数

20,000

无限

每天最大爬取页数

500

无限

已存在的实例怎么办?

如果你在这次发布之前创建了实例,它们继续像今天一样工作。你的 R2 bucket、Vectorize 索引和 Browser Run 用量留在你的账户上,按以前的方式计费。我们将很快分享已存在实例的迁移细节。

今天就开始

搜索是 agent 能做的最基本的事情之一。有了 AI Search,你不必构建实现它的基础设施。创建一个实例,把数据交给它,让你的 agent 搜索它。

通过运行这个命令创建你的第一个实例,今天就开始:

npx wrangler ai-search create my-search

查看文档,在 Cloudflare Developer Discord 上告诉我们你正在构建什么。