INTEGRITY Dokumentace

Agents SDK

Cloudflare Agents SDK umožňuje vytvářet stavové AI agenty běžící na Workers. Tento návod ukazuje sestavení chatovacího agenta, který si vytvoří vlastní instanci AI Search, zaindexuje dokument a poté tento obsah před odpovědí prohledá pomocí nástroje.

Tento návod používá doporučený vzor agenta a zpřístupňuje AI Search search() modelu jako nástroj. Více o tomto vzoru najdete v AI Search jako nástroj agenta.

Předpoklady

  1. Zaregistrujte si účet Cloudflare ↗.
  2. Nainstalujte Node.js ↗.

Správce verzí Node.js

Použijte správce verzí Node, jako je Volta ↗ nebo nvm ↗ abyste se vyhnuli problémům s oprávněními a mohli měnit verze Node.js. Wrangler, o kterém se dozvíte dále v této příručce, vyžaduje verzi Node 16.17.0 nebo novější.

1. Vytvořte projekt Worker

Vytvořte nový projekt Worker pomocí create-cloudflare CLI (C3). C3 ↗ je nástroj příkazové řádky, který vám pomůže nastavit a nasadit nové aplikace na Cloudflare.

Vytvořte nový projekt s názvem ai-search-agent spuštěním:

npm create cloudflare@latest -- ai-search-agent

Při nastavení vyberte následující možnosti:

Přejděte do adresáře své aplikace:

cd ai-search-agent

2. Nainstalujte balíčky Agents SDK

Nainstalujte Agents SDK, AI SDK a poskytovatele Workers AI:

npm i agents @cloudflare/ai-chat ai workers-ai-provider zod

Nahraďte své Konfigurační soubor Wrangler s následujícím. Tím se přidá vazba jmenného prostoru pro AI Search, binding Workers AI pro generování odpovědí a Durable Object, který u agenta uchovává historii konverzace.

{
  "$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"]

Vazba jmenného prostoru (ai_search_namespaces), nikoli pro jednu instanci ai_search vazba, je nutná, protože agent volá create() za běhu. remote volba umožňuje wrangler dev předávají požadavky na vaši nasazenou instanci, protože AI Search neběží lokálně. AIChatAgent ukládá zprávy trvale do SQLite, proto musí být jeho třída uvedena v new_sqlite_classes.

4. Napište agenta

Vytvořit src/server.ts. Agent vytvoří instanci AI Search s hybridní vyhledávání se povolí při prvním spuštění, naplní ji jedním dokumentem a zpřístupní dva nástroje: search_knowledge_base načte obsah a save_resolution zapisuje nový obsah zpět.

src/server.js
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 })
		);
	},
};
src/server.ts
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) je synchronní a vyhodnocuje se líně. Nevytváří instanci, takže ensureInstance ji nejprve vytvoří. Chcete-li prohledat několik instancí v jednom volání, použijte vyhledávání na úrovni jmenného prostoru pomocí ai_search_options.instance_ids. Viz Jmenné prostory.

Jak nástroje fungují

search_knowledge_base volání nástrojů search() na instanci. Protože instance indexuje vektory i klíčová slova, vyhledávání ve výchozím nastavení používá hybridní vyhledávání.

save_resolution volání nástrojů items.upload(), který nahraje dokument do vestavěného úložiště a zařadí ho do fronty k indexování. Volání se vrátí rychle a obsah se stane prohledatelným, jakmile dokončí indexování na pozadí. Nahrání souboru se stejným názvem ho přepíše a znovu zaindexuje.

5. Otestujte to lokálně

Vygenerujte typy a spusťte vývojový server:

npx wrangler types
npm run dev

AIChatAgent komunikuje svým chatovým protokolem přes WebSocket, proto ho ovládejte z chatovacího klienta, ne pomocí obyčejného curl požadavek. Nejrychlejší cestou je nasměrovat uživatelské rozhraní vytvořené pomocí Agents SDK useAgentChat hook na vašem lokálním serveru.

Odešlete zprávu, například How do I get started?. Při první zprávě agent vytvoří instanci a naplní ji počátečními daty, takže první odpověď může trvat minutu až dvě, než se počáteční dokument zaindexuje.

Poznáte, že integrace funguje, když:

6. Nasaďte

Přihlaste se ke svému účtu Cloudflare:

npx wrangler login

Nasaďte svůj Worker, aby byl dostupný na internetu:

npx wrangler deploy

Další kroky

AI Search jako nástroj agenta

Referenční dokumentace pro stranu agenta k načítání obsahu pomocí AI Search.

Hybridní vyhledávání

Kombinace vektorového a klíčového vyhledávání s konfigurovatelnou fúzí.

Vyhledávání na úrovni tenanta

Poskytněte každému tenantovi nebo agentovi vlastní izolovanou instanci.

Instances Workers binding

Úplná referenční dokumentace k vytváření, aktualizaci, výpisu a mazání.