← Обзор / ai-search / agent-sdks
Agents SDK
Cloudflare Agents SDK позволяет создавать ИИ-агентов с отслеживанием состояния, которые работают на Workers. В этом руководстве вы соберёте чат-агента, который создаёт собственный экземпляр AI Search, индексирует документ и перед ответом ищет в этом содержимом с помощью инструмента.
В этом руководстве используется рекомендуемый агентный паттерн, который предоставляет доступ к search() модели как инструмент. Подробнее об этом паттерне см. в AI Search как инструмент агента.
Предварительные требования
- Зарегистрируйтесь для получения Аккаунт Cloudflare ↗.
- Установка
Node.js↗.
менеджер версий Node.js
Используйте менеджер версий Node, например Volta ↗ или nvm ↗ чтобы избежать проблем с правами доступа и переключать версии Node.js. Wrangler, о котором пойдёт речь далее в этом руководстве, требует версию Node 16.17.0 или более поздней версии.
1. Создайте проект Worker
Создайте новый проект Worker с помощью create-cloudflare CLI (C3). C3 ↗ это инструмент командной строки, который помогает настраивать и развёртывать новые приложения в Cloudflare.
Создайте новый проект с именем ai-search-agent выполнив:
npm create cloudflare@latest -- ai-search-agentДля настройки выберите следующие параметры:
- Для С чего вы хотите начать?, выберите
Hello World example. - Для Какой шаблон вы хотите использовать?, выберите
Worker only. - Для Какой язык вы хотите использовать?, выберите
TypeScript. - Для Хотите использовать git для контроля версий?, выберите
Yes. - Для Хотите развернуть приложение?, выберите
No(мы внесём некоторые изменения перед развёртыванием).
Перейдите в каталог вашего приложения:
cd ai-search-agent2. Установите пакеты Agents SDK
Установите Agents SDK, AI SDK и провайдер Workers AI:
npm i agents @cloudflare/ai-chat ai workers-ai-provider zod3. Привяжите Worker к AI Search
Замените ваш конфигурационный файл Wrangler со следующим. Это добавляет привязка пространства имён для AI Search, привязку Workers AI для генерации ответов и Durable Object, который хранит историю переписки агента.
{
"$schema": "./node_modules/wrangler/config-schema.json",
"name": "ai-search-agent",
"main": "src/server.ts",
// Set this to today's date
"compatibility_date": "2026-08-28",
"compatibility_flags": [
"nodejs_compat"
],
"ai": {
"binding": "AI"
},
"ai_search_namespaces": [
{
"binding": "AI_SEARCH",
"namespace": "default",
"remote": true
}
],
"durable_objects": {
"bindings": [
{
"name": "SearchAgent",
"class_name": "SearchAgent"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": [
"SearchAgent"
]
}
]
}name = "ai-search-agent"
main = "src/server.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = ["nodejs_compat"]
[ai]
binding = "AI"
[[ai_search_namespaces]]
binding = "AI_SEARCH"
namespace = "default"
remote = true
[[durable_objects.bindings]]
name = "SearchAgent"
class_name = "SearchAgent"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["SearchAgent"]Привязка пространства имён (ai_search_namespaces), а не для одного экземпляра ai_search привязка обязательна, потому что агент вызывает create() во время выполнения. remote опция позволяет wrangler dev перенаправляют запросы к вашему развёрнутому экземпляру, поскольку AI Search не выполняется локально. AIChatAgent сохраняет сообщения в SQLite на постоянной основе, поэтому его класс должен быть указан в new_sqlite_classes.
4. Напишите агента
Создайте src/server.ts. Агент создаёт экземпляр AI Search с гибридный поиск включается при первом запуске, наполняет её одним документом и предоставляет два инструмента: search_knowledge_base получает контент, а save_resolution записывает новое содержимое обратно.
import { AIChatAgent } from "@cloudflare/ai-chat";
import { routeAgentRequest } from "agents";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
import { z } from "zod";
const INSTANCE_ID = "knowledge-base";
const SEED_DOC = `# Getting started
AI Search indexes your content so an agent can retrieve it at query time.`;
// AIChatAgent stores the conversation history and calls onChatMessage() once
// for each user message.
export class SearchAgent extends AIChatAgent {
// Guard so the one-time instance setup runs only once per running agent.
ready = false;
// Create the agent's instance with hybrid search enabled, then seed it so
// the first query has content. create() throws if the instance already
// exists, so the try/catch makes this idempotent.
async ensureInstance() {
if (this.ready) return;
try {
// index_method with both vector and keyword enables hybrid search.
await this.env.AI_SEARCH.create({
id: INSTANCE_ID,
index_method: { vector: true, keyword: true },
});
// upload() queues the file; indexing runs in the background. Poll the
// item status until it is searchable so the first query has content.
const instance = this.env.AI_SEARCH.get(INSTANCE_ID);
const { id } = await instance.items.upload(
"getting-started.md",
SEED_DOC,
);
let info = await instance.items.get(id).info();
while (info.status === "queued" || info.status === "running") {
await new Promise((resolve) => setTimeout(resolve, 2_000));
info = await instance.items.get(id).info();
}
} catch {
// Instance already exists.
}
this.ready = true;
}
// Runs on every chat message: make sure the instance exists, then stream a
// tool-using response.
async onChatMessage() {
await this.ensureInstance();
// The agent exposes AI Search's search() as a tool instead of using AI
// Search's own chat model, so this model must support function calling to
// decide when to search and drive the tool calls.
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/zai-org/glm-5.2"),
system:
"You are a support assistant. Use search_knowledge_base to find " +
"relevant content before answering, and cite what you use.",
// this.messages is the stored chat history; convert it to the format
// the model expects.
messages: await convertToModelMessages(this.messages),
tools: {
search_knowledge_base: tool({
description: "Search the knowledge base for relevant content.",
inputSchema: z.object({
query: z.string().describe("The user's question or search terms"),
}),
// Hybrid search runs by default because the instance indexes
// both vectors and keywords.
execute: async ({ query }) => {
const instance = this.env.AI_SEARCH.get(INSTANCE_ID);
return await instance.search({
query,
ai_search_options: { retrieval: { max_num_results: 5 } },
});
},
}),
save_resolution: tool({
description:
"Save a resolved answer to the knowledge base for reuse.",
inputSchema: z.object({
title: z.string().describe("Short descriptive title"),
content: z.string().describe("The resolution to save"),
}),
// upload() returns as soon as the file is queued; indexing then
// finishes in the background.
execute: async ({ title, content }) => {
const instance = this.env.AI_SEARCH.get(INSTANCE_ID);
const item = await instance.items.upload(`${title}.md`, content);
return { key: item.key, status: item.status };
},
}),
},
// Cap the tool-call loop so the agent cannot run tools indefinitely.
stopWhen: stepCountIs(5),
});
// Stream the reply, including any tool activity, back to the client.
return result.toUIMessageStreamResponse();
}
}
export default {
async fetch(request, env) {
// Route the request to the matching agent instance, keyed by the URL.
return (
(await routeAgentRequest(request, env)) ||
new Response("Not found", { status: 404 })
);
},
};import { AIChatAgent } from "@cloudflare/ai-chat";
import { routeAgentRequest } from "agents";
import { createWorkersAI } from "workers-ai-provider";
import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
import { z } from "zod";
const INSTANCE_ID = "knowledge-base";
const SEED_DOC = `# Getting started
AI Search indexes your content so an agent can retrieve it at query time.`;
// AIChatAgent stores the conversation history and calls onChatMessage() once
// for each user message.
export class SearchAgent extends AIChatAgent {
// Guard so the one-time instance setup runs only once per running agent.
private ready = false;
// Create the agent's instance with hybrid search enabled, then seed it so
// the first query has content. create() throws if the instance already
// exists, so the try/catch makes this idempotent.
private async ensureInstance() {
if (this.ready) return;
try {
// index_method with both vector and keyword enables hybrid search.
await this.env.AI_SEARCH.create({
id: INSTANCE_ID,
index_method: { vector: true, keyword: true },
});
// upload() queues the file; indexing runs in the background. Poll the
// item status until it is searchable so the first query has content.
const instance = this.env.AI_SEARCH.get(INSTANCE_ID);
const { id } = await instance.items.upload(
"getting-started.md",
SEED_DOC,
);
let info = await instance.items.get(id).info();
while (info.status === "queued" || info.status === "running") {
await new Promise((resolve) => setTimeout(resolve, 2_000));
info = await instance.items.get(id).info();
}
} catch {
// Instance already exists.
}
this.ready = true;
}
// Runs on every chat message: make sure the instance exists, then stream a
// tool-using response.
async onChatMessage() {
await this.ensureInstance();
// The agent exposes AI Search's search() as a tool instead of using AI
// Search's own chat model, so this model must support function calling to
// decide when to search and drive the tool calls.
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/zai-org/glm-5.2"),
system:
"You are a support assistant. Use search_knowledge_base to find " +
"relevant content before answering, and cite what you use.",
// this.messages is the stored chat history; convert it to the format
// the model expects.
messages: await convertToModelMessages(this.messages),
tools: {
search_knowledge_base: tool({
description: "Search the knowledge base for relevant content.",
inputSchema: z.object({
query: z.string().describe("The user's question or search terms"),
}),
// Hybrid search runs by default because the instance indexes
// both vectors and keywords.
execute: async ({ query }) => {
const instance = this.env.AI_SEARCH.get(INSTANCE_ID);
return await instance.search({
query,
ai_search_options: { retrieval: { max_num_results: 5 } },
});
},
}),
save_resolution: tool({
description:
"Save a resolved answer to the knowledge base for reuse.",
inputSchema: z.object({
title: z.string().describe("Short descriptive title"),
content: z.string().describe("The resolution to save"),
}),
// upload() returns as soon as the file is queued; indexing then
// finishes in the background.
execute: async ({ title, content }) => {
const instance = this.env.AI_SEARCH.get(INSTANCE_ID);
const item = await instance.items.upload(`${title}.md`, content);
return { key: item.key, status: item.status };
},
}),
},
// Cap the tool-call loop so the agent cannot run tools indefinitely.
stopWhen: stepCountIs(5),
});
// Stream the reply, including any tool activity, back to the client.
return result.toUIMessageStreamResponse();
}
}
export default {
async fetch(request: Request, env: Env) {
// Route the request to the matching agent instance, keyed by the URL.
return (
(await routeAgentRequest(request, env)) ||
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;this.env.AI_SEARCH.get(INSTANCE_ID) синхронен и разрешается лениво. Он не создаёт экземпляр, поэтому ensureInstance сначала создаёт его. Чтобы искать сразу по нескольким экземплярам в одном вызове, используйте поиск на уровне пространства имён с помощью ai_search_options.instance_ids. См. Пространства имён.
Как работают инструменты
search_knowledge_base вызовов инструмента search() на экземпляре. Поскольку экземпляр индексирует и векторы, и ключевые слова, по умолчанию используется гибридный поиск.
save_resolution вызовов инструмента items.upload(), который загружает документ во встроенное хранилище и ставит его в очередь на индексирование. Вызов завершается быстро, а контент становится доступным для поиска после завершения фоновой индексации. Загрузка файла с тем же именем перезаписывает его и переиндексирует.
5. Протестируйте локально
Сгенерируйте типы и запустите сервер разработки:
npx wrangler types
npm run devAIChatAgent использует свой протокол чата поверх WebSocket, поэтому обращайтесь к нему через чат-клиент, а не через обычный curl запрос. Самый быстрый способ состоит в том, чтобы направить пользовательский интерфейс, созданный с помощью Agents SDK useAgentChat хук на вашем локальном сервере.
Отправьте сообщение, например How do I get started?. При первом сообщении агент создаёт экземпляр и наполняет его начальными данными, поэтому первый ответ может занять минуту-другую, пока индексируется исходный документ.
Вы поймёте, что интеграция работает, когда:
-
wrangler devжурналы показывают запрос к/agents/search-agent/<name>, а затемsearch_knowledge_baseперед формированием ответа. - Ответ агента опирается на предоставленный контент и содержит ссылки на него, а не является общим ответом.
6. Разверните
Войдите в свою учётную запись Cloudflare:
npx wrangler loginРазверните свой Worker, чтобы сделать его доступным в интернете:
npx wrangler deploy