INTEGRITY Dokumentace

Aktualizace znalostní báze s lidským dohledem

Tento tutoriál sestaví agenta, který vyhledává ve znalostní bázi a doplňuje ji, přičemž každý zápis schvaluje člověk. Umožnit agentovi upravovat vaše data je riskantní, proto se každé uložení před provedením pozastaví a čeká na schválení, a pokud se uložení ukáže jako chybné, můžete jej vrátit zpět.

Co vytvoříte

Cloudflare Agent, který prohledává instanci AI Search, navrhuje nové dokumenty k zaindexování, čeká na vaše schválení každého z nich a dokáže schválené uložení vrátit zpět.

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ší.

Nic dalšího nepotřebujete. Agent si při prvním spuštění vytvoří vlastní instanci AI Search.

Jak to funguje

Agent používá Code Mode, vzor použití nástrojů, kdy model místo jednotlivého vyžádání každého volání napíše krátký program, který nástroje zavolá sám. trvalý runtime zaznamenává každé volání, které program provede, před citlivými voláními se pozastaví, aby je mohl schválit člověk, a uplatněná volání dokáže kompenzovat spuštěním revert. Tento trvalý stav žije v Durable Object agenta, takže schválení může čekat napříč požadavky i hibernací.

AI Search zpřístupníte runtime prostřednictvím konektor: obyčejná třída, která mění operace AI Search na metody, jež může model volat. Tento tutoriál dává modelu přístup pouze pro čtení k search metoda a saveDocument metoda, která vyžaduje schválení.

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 kb-agent spuštěním:

npm create cloudflare@latest -- kb-agent

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

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

cd kb-agent

Nainstalujte závislosti. ai a zod verze jsou fixovány na rozsahy, které Agents SDK očekává jako peer dependencies:

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

Tento tutoriál používá vazby AI Search a Worker Loader, které vyžadují Wrangler v4. Pokud create-cloudflare jste projekt nastavili se starší verzí, aktualizujte ho:

npm i -D wrangler@4

2. Nakonfigurujte Wrangler

Nahraďte své Konfigurační soubor Wrangler s následujícím. Tím se přidá vazba AI Search, vazba Workers AI pro model, vazba Worker Loader, která spouští kód modelu v izolovaném Workeru, a Durable Object, který ukládá historii chatu agenta a jeho trvalý stav runtime.

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "kb-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
    }
  ],
  "worker_loaders": [
    {
      "binding": "LOADER"
    }
  ],
  "durable_objects": {
    "bindings": [
      {
        "name": "Chat",
        "class_name": "Chat"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": [
        "Chat"
      ]
    }
  ]
}
name = "kb-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

[[worker_loaders]]
binding = "LOADER"

[[durable_objects.bindings]]
name = "Chat"
class_name = "Chat"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["Chat"]

AI Search nemá lokální emulátor, takže binding vždy komunikuje se vzdálenou službou (remote = true). Proto agenta testujete jeho nasazením, nikoli pomocí wrangler dev. AIChatAgent ukládá zprávy trvale do SQLite, proto musí být jeho třída uvedena v new_sqlite_classes.

3. Vytvořte konektor AI Search

Vytvořit src/ai-search-connector.ts. Konektor volá binding AI Search přímo, takže požadavky zůstávají v rámci procesu a není potřeba žádný veřejný endpoint.

Poskytněte modelu přístup pouze pro čtení k search metoda a saveDocument metoda. Protože saveDocument zapisuje obsah, označte to requiresApproval a přidejte revert aby ho mohl runtime vrátit zpět.

src/ai-search-connector.js
import { CodemodeConnector } from "@cloudflare/codemode";

// The instance this connector reads from and writes to.
const INSTANCE_ID = "knowledge-base";

// A connector turns AI Search operations into methods the model can call from
// its generated code. Each connector becomes one named object in the sandbox.
export class AISearchConnector extends CodemodeConnector {
	// The sandbox global. The model calls `aiSearch.search()` and
	// `aiSearch.saveDocument()`.
	name() {
		return "aiSearch";
	}

	// Shown to the model so it knows what this connector is for.
	instructions() {
		return "Use this connector to search indexed content and save new documents.";
	}

	// Every method the model can call. `inputSchema` is JSON Schema; the runtime
	// validates the model's arguments against it before calling `execute`.
	tools() {
		return {
			// Read-only, so it runs without approval.
			search: {
				description: "Search indexed content and return the matching chunks.",
				inputSchema: {
					type: "object",
					properties: { query: { type: "string" } },
					required: ["query"],
				},
				execute: async (input) => {
					const { query } = input;
					return this.env.AI_SEARCH.get(INSTANCE_ID).search({
						query,
						ai_search_options: { retrieval: { max_num_results: 5 } },
					});
				},
			},
			// Writes content, so it is gated behind approval and made reversible.
			saveDocument: {
				description: "Save a new document to the knowledge base.",
				inputSchema: {
					type: "object",
					properties: {
						name: { type: "string" },
						content: { type: "string" },
					},
					required: ["name", "content"],
				},
				// Pauses the run before this method executes so a human can approve it.
				requiresApproval: true,
				execute: async (input) => {
					const { name, content } = input;
					// upload() queues the document for indexing and returns right away.
					// The item becomes searchable once indexing finishes, a few seconds later.
					const item = await this.env.AI_SEARCH.get(INSTANCE_ID).items.upload(
						name,
						content,
					);
					// This return value is passed to `revert` if the call is rolled back.
					return { id: item.id, key: item.key, status: item.status };
				},
				// Compensating action for rollback: delete the document this call added.
				revert: async (_input, result) => {
					const { id } = result;
					await this.env.AI_SEARCH.get(INSTANCE_ID).items.delete(id);
				},
			},
		};
	}
}
src/ai-search-connector.ts
import { CodemodeConnector, type ConnectorTools } from "@cloudflare/codemode";

// The instance this connector reads from and writes to.
const INSTANCE_ID = "knowledge-base";

// A connector turns AI Search operations into methods the model can call from
// its generated code. Each connector becomes one named object in the sandbox.
export class AISearchConnector extends CodemodeConnector<Env> {
	// The sandbox global. The model calls `aiSearch.search()` and
	// `aiSearch.saveDocument()`.
	override name() {
		return "aiSearch";
	}

	// Shown to the model so it knows what this connector is for.
	protected override instructions() {
		return "Use this connector to search indexed content and save new documents.";
	}

	// Every method the model can call. `inputSchema` is JSON Schema; the runtime
	// validates the model's arguments against it before calling `execute`.
	protected override tools(): ConnectorTools {
		return {
			// Read-only, so it runs without approval.
			search: {
				description: "Search indexed content and return the matching chunks.",
				inputSchema: {
					type: "object",
					properties: { query: { type: "string" } },
					required: ["query"],
				},
				execute: async (input) => {
					const { query } = input as { query: string };
					return this.env.AI_SEARCH.get(INSTANCE_ID).search({
						query,
						ai_search_options: { retrieval: { max_num_results: 5 } },
					});
				},
			},
			// Writes content, so it is gated behind approval and made reversible.
			saveDocument: {
				description: "Save a new document to the knowledge base.",
				inputSchema: {
					type: "object",
					properties: {
						name: { type: "string" },
						content: { type: "string" },
					},
					required: ["name", "content"],
				},
				// Pauses the run before this method executes so a human can approve it.
				requiresApproval: true,
				execute: async (input) => {
					const { name, content } = input as { name: string; content: string };
					// upload() queues the document for indexing and returns right away.
					// The item becomes searchable once indexing finishes, a few seconds later.
					const item = await this.env.AI_SEARCH.get(INSTANCE_ID).items.upload(
						name,
						content,
					);
					// This return value is passed to `revert` if the call is rolled back.
					return { id: item.id, key: item.key, status: item.status };
				},
				// Compensating action for rollback: delete the document this call added.
				revert: async (_input, result) => {
					const { id } = result as { id: string };
					await this.env.AI_SEARCH.get(INSTANCE_ID).items.delete(id);
				},
			},
		};
	}
}

name() výsledek (aiSearch) se stane globálním objektem, který volá kód modelu, takže metody jsou dostupné jako aiSearch.search() a aiSearch.saveDocument().

4. Sestavte 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í, poté vytvoří runtime Code Mode s konektorem a zpřístupní jej modelu jako jediný codemode nástroj. @callable() metody umožňují klientovi zobrazit čekající schválení a zápis schválit, zamítnout nebo vrátit zpět.

src/server.js
import { AIChatAgent } from "@cloudflare/ai-chat";
import {
	createCodemodeRuntime,
	DynamicWorkerExecutor,
} from "@cloudflare/codemode";
import { callable, routeAgentRequest } from "agents";
import { createWorkersAI } from "workers-ai-provider";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import { AISearchConnector } from "./ai-search-connector";

// Code Mode stores its durable state (execution log, pending approvals) in a
// facet exported from the Worker entry. The runtime requires this export.
export { CodemodeRuntime } from "@cloudflare/codemode";

const INSTANCE_ID = "knowledge-base";

// Seed content, so the agent has something to find on the first query.
const SEED_DOC = `# Getting started
AI Search indexes your content so an agent can search it and add to it.`;

export class Chat extends AIChatAgent {
	// In-memory guard, so the one-time setup runs once per instance lifetime.
	ready = false;

	// Create the AI Search instance with hybrid search enabled, then seed it.
	// create() throws if the instance already exists, so the try/catch makes
	// this safe to call on every message.
	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 },
			});
			// Queue the seed document for indexing so the first search has content.
			await this.env.AI_SEARCH.get(INSTANCE_ID).items.upload(
				"getting-started.md",
				SEED_DOC,
			);
		} catch (err) {
			// create() throws if the instance already exists, which is expected on
			// every run after the first. Log anything else so real failures surface.
			console.error("ensureInstance:", err);
		}
		this.ready = true;
	}

	// Build the Code Mode runtime for this request. The handle is cheap to
	// create; the durable state lives in the Durable Object, not the handle.
	#runtime() {
		return createCodemodeRuntime({
			ctx: this.ctx,
			// Runs the model's generated code in an isolated Worker.
			executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }),
			connectors: [new AISearchConnector(this.ctx, this.env)],
		});
	}

	// Runs on every chat message from the client.
	async onChatMessage() {
		await this.ensureInstance();

		const workersai = createWorkersAI({ binding: this.env.AI });

		const result = streamText({
			model: workersai("@cf/zai-org/glm-5.2"),
			system:
				"You help maintain a knowledge base. Use codemode to search existing " +
				"content and to save new documents. Search before you answer.",
			messages: await convertToModelMessages(this.messages),
			// The model sees one `codemode` tool and writes code that calls the connector.
			tools: { codemode: this.#runtime().tool() },
			// Cap the agent's tool-use loop.
			stopWhen: stepCountIs(10),
		});

		return result.toUIMessageStreamResponse();
	}

	// The methods below are called from your client to drive the approval flow.

	// List the writes that are paused waiting for approval.
	@callable()
	async pendingApprovals() {
		return this.#runtime().pending();
	}

	// Approve a paused write. The runtime resumes the program and runs it.
	@callable()
	async approveExecution(executionId) {
		return this.#runtime().approve({ executionId });
	}

	// Decline a paused write. The execution ends without saving.
	@callable()
	async rejectExecution(executionId, seq) {
		return this.#runtime().reject({ executionId, seq });
	}

	// Undo an applied write by running the connector's revert.
	@callable()
	async rollbackExecution(executionId) {
		await this.#runtime().rollback({ executionId });
	}
}

export default {
	async fetch(request, env) {
		return (
			(await routeAgentRequest(request, env)) ||
			new Response("Not found", { status: 404 })
		);
	},
};
src/server.ts
import { AIChatAgent } from "@cloudflare/ai-chat";
import {
	createCodemodeRuntime,
	DynamicWorkerExecutor,
	type CodemodeRuntimeHandle,
	type PendingAction,
} from "@cloudflare/codemode";
import { callable, routeAgentRequest } from "agents";
import { createWorkersAI } from "workers-ai-provider";
import { convertToModelMessages, stepCountIs, streamText } from "ai";
import { AISearchConnector } from "./ai-search-connector";

// Code Mode stores its durable state (execution log, pending approvals) in a
// facet exported from the Worker entry. The runtime requires this export.
export { CodemodeRuntime } from "@cloudflare/codemode";

const INSTANCE_ID = "knowledge-base";

// Seed content, so the agent has something to find on the first query.
const SEED_DOC = `# Getting started
AI Search indexes your content so an agent can search it and add to it.`;

export class Chat extends AIChatAgent<Env> {
	// In-memory guard, so the one-time setup runs once per instance lifetime.
	private ready = false;

	// Create the AI Search instance with hybrid search enabled, then seed it.
	// create() throws if the instance already exists, so the try/catch makes
	// this safe to call on every message.
	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 },
			});
			// Queue the seed document for indexing so the first search has content.
			await this.env.AI_SEARCH.get(INSTANCE_ID).items.upload(
				"getting-started.md",
				SEED_DOC,
			);
		} catch (err) {
			// create() throws if the instance already exists, which is expected on
			// every run after the first. Log anything else so real failures surface.
			console.error("ensureInstance:", err);
		}
		this.ready = true;
	}

	// Build the Code Mode runtime for this request. The handle is cheap to
	// create; the durable state lives in the Durable Object, not the handle.
	#runtime(): CodemodeRuntimeHandle {
		return createCodemodeRuntime({
			ctx: this.ctx,
			// Runs the model's generated code in an isolated Worker.
			executor: new DynamicWorkerExecutor({ loader: this.env.LOADER }),
			connectors: [new AISearchConnector(this.ctx, this.env)],
		});
	}

	// Runs on every chat message from the client.
	async onChatMessage() {
		await this.ensureInstance();

		const workersai = createWorkersAI({ binding: this.env.AI });

		const result = streamText({
			model: workersai("@cf/zai-org/glm-5.2"),
			system:
				"You help maintain a knowledge base. Use codemode to search existing " +
				"content and to save new documents. Search before you answer.",
			messages: await convertToModelMessages(this.messages),
			// The model sees one `codemode` tool and writes code that calls the connector.
			tools: { codemode: this.#runtime().tool() },
			// Cap the agent's tool-use loop.
			stopWhen: stepCountIs(10),
		});

		return result.toUIMessageStreamResponse();
	}

	// The methods below are called from your client to drive the approval flow.

	// List the writes that are paused waiting for approval.
	@callable()
	async pendingApprovals(): Promise<PendingAction[]> {
		return this.#runtime().pending();
	}

	// Approve a paused write. The runtime resumes the program and runs it.
	@callable()
	async approveExecution(executionId: string) {
		return this.#runtime().approve({ executionId });
	}

	// Decline a paused write. The execution ends without saving.
	@callable()
	async rejectExecution(executionId: string, seq: number): Promise<boolean> {
		return this.#runtime().reject({ executionId, seq });
	}

	// Undo an applied write by running the connector's revert.
	@callable()
	async rollbackExecution(executionId: string): Promise<void> {
		await this.#runtime().rollback({ executionId });
	}
}

export default {
	async fetch(request: Request, env: Env) {
		return (
			(await routeAgentRequest(request, env)) ||
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

Vygenerujte typy:

npx wrangler types

5. Nasaďte

Protože AI Search běží vzdáleně, nasadíte Worker, který agenta spouští.

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

npx wrangler login

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

npx wrangler deploy

Wrangler vypíše adresu URL vašeho Workeru, například https://kb-agent.<your-subdomain>.workers.dev. Použijete ho v dalším kroku.

6. Vyzkoušejte proces schválení a vrácení zpět

Model obdrží jeden codemode nástroj. Když ho požádáte, aby vyhledal a uložil obsah, napíše krátký program, který volá metody konektoru:

// The model writes this program. It runs inside the Code Mode sandbox.
async () => {
	// The read-only search runs immediately.
	const existing = await aiSearch.search({ query: "onboarding steps" });

	// Only save when the knowledge base has no matching content yet.
	if (existing.chunks.length === 0) {
		// saveDocument requires approval, so this call pauses the program here.
		await aiSearch.saveDocument({
			name: "onboarding.md",
			content: "# Onboarding\nStep 1: create an account.",
		});
	}

	return existing.chunks.length;
};

aiSearch.search() se spustí okamžitě. Jakmile program dosáhne aiSearch.saveDocument(), runtime zaznamená volání jako čekající a před spuštěním nahrávání pozastaví provádění.

Váš klient odešle chatovou zprávu, která zahájí běh, a poté řídí schvalování pomocí @callable() metody. Následující skript používá Klient Agents SDK k oběma úkonům. Uložte ho jako client.mjs, nastavte HOST do nasazeného Workeru a spusťte jej pomocí node client.mjs:

client.mjs
import { AgentClient } from "agents/client";

// Your deployed Worker, without the protocol.
const HOST = "kb-agent.<your-subdomain>.workers.dev";

const client = new AgentClient({ agent: "Chat", name: "default", host: HOST });
await client.ready;

// 1. Ask the agent to find and save content. It writes a Code Mode program;
//    the saveDocument call pauses for approval instead of running.
client.send(
	JSON.stringify({
		type: "cf_agent_use_chat_request",
		id: crypto.randomUUID(),
		init: {
			method: "POST",
			body: JSON.stringify({
				messages: [
					{
						id: crypto.randomUUID(),
						role: "user",
						parts: [
							{
								type: "text",
								text: "Search for onboarding steps. If there is none, save a document named onboarding.md with a short onboarding guide.",
							},
						],
					},
				],
			}),
		},
	}),
);

// Give the turn time to run and pause at saveDocument.
await new Promise((r) => setTimeout(r, 20_000));

// 2. List the writes waiting for approval.
const pending = await client.call("pendingApprovals");
console.log("Pending approvals:", pending);

// 3. Approve the first one. The runtime replays the program: completed calls
//    return their recorded results, and the approved saveDocument runs.
if (pending.length > 0) {
	await client.call("approveExecution", [pending[0].executionId]);
	console.log("Approved", pending[0].executionId);

	// To roll back later, delete the uploaded document:
	// await client.call("rollbackExecution", [pending[0].executionId]);
}

client.close();
client.mjs
import { AgentClient } from "agents/client";

// Your deployed Worker, without the protocol.
const HOST = "kb-agent.<your-subdomain>.workers.dev";

const client = new AgentClient({ agent: "Chat", name: "default", host: HOST });
await client.ready;

// 1. Ask the agent to find and save content. It writes a Code Mode program;
//    the saveDocument call pauses for approval instead of running.
client.send(
	JSON.stringify({
		type: "cf_agent_use_chat_request",
		id: crypto.randomUUID(),
		init: {
			method: "POST",
			body: JSON.stringify({
				messages: [
					{
						id: crypto.randomUUID(),
						role: "user",
						parts: [
							{
								type: "text",
								text: "Search for onboarding steps. If there is none, save a document named onboarding.md with a short onboarding guide.",
							},
						],
					},
				],
			}),
		},
	}),
);

// Give the turn time to run and pause at saveDocument.
await new Promise((r) => setTimeout(r, 20_000));

// 2. List the writes waiting for approval.
const pending = await client.call("pendingApprovals");
console.log("Pending approvals:", pending);

// 3. Approve the first one. The runtime replays the program: completed calls
//    return their recorded results, and the approved saveDocument runs.
if (pending.length > 0) {
	await client.call("approveExecution", [pending[0].executionId]);
	console.log("Approved", pending[0].executionId);

	// To roll back later, delete the uploaded document:
	// await client.call("rollbackExecution", [pending[0].executionId]);
}

client.close();

Každý PendingAction z pendingApprovals() zahrnuje executionId, seq číslo a metodu a argumenty, takže uživateli můžete před rozhodnutím zobrazit čekající dokument. Metody schvalování se chovají takto:

Co jste vytvořili

Váš agent nyní může:

Další kroky

Vytvořte trvalý runtime Code Mode

Úplné runtime API: zamítnutí, historie provádění a opakovaně použitelné úryvky.

AI Search jako nástroj agenta

Poskytněte Cloudflare Agentovi vyhledávání pomocí AI Search.

Hybridní vyhledávání

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

Items Workers binding

Úplná referenční dokumentace k nahrávání, výpisu a mazání dokumentů.