INTEGRITY Документация

Обновления базы знаний с участием человека

В этом руководстве вы создадите агента, который ищет информацию в базе знаний и пополняет её, причём каждую запись подтверждает человек. Разрешать агенту изменять данные рискованно, поэтому перед выполнением каждое сохранение приостанавливается для подтверждения, а сохранение, которое оказалось ошибочным, можно откатить.

Что вы создадите

Cloudflare Agent, который выполняет поиск в экземпляре AI Search, предлагает новые документы для индексирования, ожидает вашего подтверждения для каждого из них и может отменить уже подтверждённое сохранение.

Предварительные требования

  1. Зарегистрируйтесь для получения Аккаунт Cloudflare ↗.
  2. Установка Node.js ↗.

менеджер версий Node.js

Используйте менеджер версий Node, например Volta ↗ или nvm ↗ чтобы избежать проблем с правами доступа и переключать версии Node.js. Wrangler, о котором пойдёт речь далее в этом руководстве, требует версию Node 16.17.0 или более поздней версии.

Больше ничего не требуется: агент сам создаёт свой экземпляр AI Search при первом запуске.

Как это работает

Агент использует Code Mode, паттерн использования инструментов, при котором модель сама пишет небольшую программу, вызывающую нужные инструменты, вместо того чтобы запрашивать каждый вызов по отдельности. устойчивый рантайм записывает каждый вызов, который выполняет программа, приостанавливается перед чувствительными вызовами, чтобы их мог подтвердить человек, а применённые вызовы способна компенсировать запуском revert. Это устойчивое состояние хранится в Durable Object агента, поэтому подтверждение может ожидать между запросами и во время гибернации.

Вы предоставляете AI Search доступ к рантайму через коннектор: простой класс, который превращает операции AI Search в методы, вызываемые моделью. В этом руководстве модель получает доступ только для чтения к search метод и saveDocument метод, требующий подтверждения.

1. Создайте проект Worker

Создайте новый проект Worker с помощью create-cloudflare CLI (C3). C3 ↗ это инструмент командной строки, который помогает настраивать и развёртывать новые приложения в Cloudflare.

Создайте новый проект с именем kb-agent выполнив:

npm create cloudflare@latest -- kb-agent

Для настройки выберите следующие параметры:

Перейдите в каталог вашего приложения:

cd kb-agent

Установите зависимости. ai и zod версии зафиксированы в диапазонах, которые Agents SDK ожидает как peer dependencies:

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

В этом руководстве используются привязки AI Search и Worker Loader, для которых требуется Wrangler v4. Если create-cloudflare вы настроили проект с более ранней версией, обновите его:

npm i -D wrangler@4

2. Настройте Wrangler

Замените ваш конфигурационный файл Wrangler со следующим. Это добавляет привязку AI Search, привязку Workers AI для модели, привязку Worker Loader, которая запускает код модели в изолированном Worker, и Durable Object, который хранит историю чата агента и его долговременное состояние 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 нет локального эмулятора, поэтому привязка всегда обращается к удалённой службе (remote = true). Поэтому проверяете агента, развёртывая его, а не с помощью wrangler dev. AIChatAgent сохраняет сообщения в SQLite на постоянной основе, поэтому его класс должен быть указан в new_sqlite_classes.

3. Создайте коннектор AI Search

Создайте src/ai-search-connector.ts. Коннектор вызывает привязку AI Search напрямую, поэтому запросы остаются внутри процесса, а публичная конечная точка не требуется.

Предоставьте модели доступ только для чтения к search метод и saveDocument метод. Поскольку saveDocument записывает содержимое, помечайте его requiresApproval и добавьте revert чтобы runtime мог откатить изменения.

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() результат (aiSearch) становится глобальным объектом, который вызывает код модели, поэтому методы доступны как aiSearch.search() и aiSearch.saveDocument().

4. Соберите агента

Создайте src/server.ts. Агент создаёт экземпляр AI Search с гибридный поиск включается при первом запуске, затем создаёт среду выполнения Code Mode с коннектором и предоставляет её модели в виде единого codemode инструмент. @callable() методы позволяют клиенту получать список ожидающих подтверждения запросов и подтверждать, отклонять или откатывать запись.

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>;

Сгенерируйте типы:

npx wrangler types

5. Разверните

Поскольку AI Search работает удалённо, для запуска агента вы развёртываете Worker.

Войдите в свою учётную запись Cloudflare:

npx wrangler login

Разверните свой Worker, чтобы сделать его доступным в интернете:

npx wrangler deploy

Wrangler выводит URL-адрес вашего Worker, например https://kb-agent.<your-subdomain>.workers.dev. Вы используете его на следующем шаге.

6. Опробуйте процесс подтверждения и отката

Модель получает один codemode инструмент. Когда вы просите его найти и сохранить содержимое, он пишет небольшую программу, которая вызывает методы коннектора:

// 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() запускается немедленно. Как только программа достигает aiSearch.saveDocument(), рантайм фиксирует вызов как отложенный и приостанавливает выполнение перед запуском загрузки.

Ваш клиент отправляет сообщение чата, которое запускает выполнение, а затем управляет подтверждением с помощью @callable() методы. Следующий скрипт использует Клиент Agents SDK для обоих действий. Сохраните его как client.mjs, задайте HOST в развёрнутый Worker и запустите его с помощью 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();

Каждый PendingAction от pendingApprovals() включает в себя executionId, seq номер, а также метод и аргументы, поэтому вы можете показать пользователю ожидающий подтверждения документ до принятия решения. Методы подтверждения работают так:

Что вы создали

Теперь ваш агент может:

Следующие шаги

Создайте устойчивый рантайм Code Mode

Полный runtime API: отклонение, история выполнения и переиспользуемые фрагменты.

AI Search как инструмент агента

Дайте Cloudflare Agent возможность поиска через AI Search.

Гибридный поиск

Объедините векторный поиск и поиск по ключевым словам с настраиваемым слиянием.

Привязка Workers для элементов

Полный справочник по загрузке, получению списка и удалению документов.