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

Создайте устойчивого ИИ-агента

В этом руководстве вы создадите ИИ-агента, который исследует репозитории GitHub. Дайте ему задачу, например «Сравнить проекты LLM с открытым исходным кодом», и он:

  1. Поиск подходящих репозиториев на GitHub
  2. Получите подробную информацию по каждому из них (звёзды, форки, активность)
  3. Проанализируйте и сравните их
  4. Вернуть рекомендацию

Каждый вызов LLM и вызов инструмента становится шаг : самостоятельная единица работы, которую можно повторить независимо от остальных. Если какой-либо шаг завершается с ошибкой, Workflows автоматически повторяет его. Если весь Workflow аварийно завершается посреди выполнения, он возобновляется с последнего успешного шага.

Challenge Решение на основе Workflows
Долгоживущие циклы агентов Устойчивое выполнение, сохраняющееся при любом прерывании
Ненадёжные вызовы LLM и API Автоматический повтор с независимыми контрольными точками
Ожидание подтверждения от человека waitForEvent() приостанавливается на часы или дни
Опрос статуса завершения задания step.sleep() между проверками, не потребляя ресурсы

В этом руководстве используется Agents SDK с Workflows для обновления статуса в реальном времени и Anthropic SDK для вызовов LLM. Те же принципы применимы к любому SDK для LLM (OpenAI, Google AI, Mistral и другим).

быстрый старт

Если вы хотите пропустить шаги и сразу получить готового агента, использующего AI Gateway, выполните следующую команду:

npm create cloudflare@latest -- --template cloudflare/docs-examples/workflows/durableAgent

Выберите этот вариант, если вы уже знакомы с Cloudflare Workflows или хотите сначала изучить код.

Выполните шаги ниже, чтобы узнать, как создать устойчивого AI-агента с нуля.

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

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

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

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

Вам также понадобится Ключ API Anthropic ↗ для вызовов LLM. Новым аккаунтам начисляются бесплатные кредиты.

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

  1. Создайте новый проект Worker с помощью следующей команды:

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

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

    • Для С чего вы хотите начать?, выберите Hello World example.
    • Для Какой шаблон вы хотите использовать?, выберите Worker only.
    • Для Какой язык вы хотите использовать?, выберите TypeScript.
    • Для Хотите использовать git для контроля версий?, выберите Yes.
    • Для Хотите развернуть приложение?, выберите No (мы внесём некоторые изменения перед развёртыванием).
  2. Перейдите в каталог проекта:

    cd durable-ai-agent
  3. Установите зависимости:

    npm install agents @anthropic-ai/sdk

2. Определите свои инструменты

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

  1. Создайте src/tools.ts двумя взаимодополняющими инструментами:

    src/tools.ts
    export interface SearchReposInput {
    	query: string;
    	limit?: number;
    }
    
    export interface GetRepoInput {
    	owner: string;
    	repo: string;
    }
    
    interface GitHubSearchResponse {
    	items: Array<{ full_name: string; stargazers_count: number }>;
    }
    
    interface GitHubRepoResponse {
    	full_name: string;
    	description: string;
    	stargazers_count: number;
    	forks_count: number;
    	open_issues_count: number;
    	language: string;
    	license: { name: string } | null;
    	updated_at: string;
    }
    
    export const searchReposTool = {
    	name: "search_repos" as const,
    	description:
    		"Search GitHub repositories by keyword. Returns top results. Use get_repo for details.",
    	input_schema: {
    		type: "object" as const,
    		properties: {
    			query: {
    				type: "string",
    				description: "Search query (e.g., 'typescript orm')",
    			},
    			limit: { type: "number", description: "Max results (default 5)" },
    		},
    		required: ["query"],
    	},
    	run: async (input: SearchReposInput): Promise<string> => {
    		const response = await fetch(
    			`https://api.github.com/search/repositories?q=${encodeURIComponent(input.query)}&sort=stars&per_page=${input.limit ?? 5}`,
    			{
    				headers: {
    					Accept: "application/vnd.github+json",
    					"User-Agent": "DurableAgent/1.0",
    				},
    			},
    		);
    		if (!response.ok) return `Search failed: ${response.status}`;
    		const data = await response.json<GitHubSearchResponse>();
    		return JSON.stringify(
    			data.items.map((r) => ({
    				name: r.full_name,
    				stars: r.stargazers_count,
    			})),
    		);
    	},
    };
    
    export const getRepoTool = {
    	name: "get_repo" as const,
    	description:
    		"Get detailed info about a GitHub repository including stars, forks, and description.",
    	input_schema: {
    		type: "object" as const,
    		properties: {
    			owner: {
    				type: "string",
    				description: "Repository owner (e.g., 'cloudflare')",
    			},
    			repo: {
    				type: "string",
    				description: "Repository name (e.g., 'workers-sdk')",
    			},
    		},
    		required: ["owner", "repo"],
    	},
    	run: async (input: GetRepoInput): Promise<string> => {
    		const response = await fetch(
    			`https://api.github.com/repos/${input.owner}/${input.repo}`,
    			{
    				headers: {
    					Accept: "application/vnd.github+json",
    					"User-Agent": "DurableAgent/1.0",
    				},
    			},
    		);
    		if (!response.ok) return `Repo not found: ${input.owner}/${input.repo}`;
    		const data = await response.json<GitHubRepoResponse>();
    		return JSON.stringify({
    			name: data.full_name,
    			description: data.description,
    			stars: data.stargazers_count,
    			forks: data.forks_count,
    			issues: data.open_issues_count,
    			language: data.language,
    			license: data.license?.name ?? "None",
    			updated: data.updated_at,
    		});
    	},
    };
    
    export const tools = [searchReposTool, getRepoTool];

Эти инструменты дополняют друг друга: search_repos находит репозитории и get_repo получает подробную информацию о конкретных из них.

3. Напишите свой Workflow

AgentWorkflow класс из Agents SDK расширяет Cloudflare Workflows двусторонним взаимодействием с Agent. Ваш Workflow может сообщать о прогрессе, рассылать сообщения WebSocket-клиентам и вызывать методы Agent через RPC.

Для более простого введения в тему обратитесь к Создайте свой первый Workflow.

Создайте src/workflow.ts:

src/workflow.ts
import { AgentWorkflow } from "agents/workflows";
import type { AgentWorkflowEvent, AgentWorkflowStep } from "agents/workflows";
import Anthropic from "@anthropic-ai/sdk";
import {
	tools,
	searchReposTool,
	getRepoTool,
	type SearchReposInput,
	type GetRepoInput,
} from "./tools";
import type { ResearchAgent } from "./agent";

type Params = { task: string };

export class ResearchWorkflow extends AgentWorkflow<ResearchAgent, Params> {
	async run(event: AgentWorkflowEvent<Params>, step: AgentWorkflowStep) {
		const client = new Anthropic({ apiKey: this.env.ANTHROPIC_API_KEY });

		const messages: Anthropic.MessageParam[] = [
			{ role: "user", content: event.payload.task },
		];

		const toolDefinitions = tools.map(({ run, ...rest }) => rest);

		// Durable agent loop - each turn is checkpointed
		for (let turn = 0; turn < 10; turn++) {
			// Report progress to Agent and connected clients
			await this.reportProgress({
				step: `llm-turn-${turn}`,
				status: "running",
				percent: turn / 10,
				message: `Processing turn ${turn + 1}...`,
			});

			const response = (await step.do(
				`llm-turn-${turn}`,
				{ retries: { limit: 3, delay: "10 seconds", backoff: "exponential" } },
				async () => {
					const msg = await client.messages.create({
						model: "claude-sonnet-4-5-20250929",
						max_tokens: 4096,
						tools: toolDefinitions,
						messages,
					});
					// Serialize for Workflow state
					return JSON.parse(JSON.stringify(msg));
				},
			)) as Anthropic.Message;

			if (!response || !response.content) continue;

			messages.push({ role: "assistant", content: response.content });

			if (response.stop_reason === "end_turn") {
				const textBlock = response.content.find(
					(b): b is Anthropic.TextBlock => b.type === "text",
				);
				const result = {
					status: "complete",
					turns: turn + 1,
					result: textBlock?.text ?? null,
				};

				// Report completion (durable)
				await step.reportComplete(result);
				return result;
			}

			const toolResults: Anthropic.ToolResultBlockParam[] = [];

			for (const block of response.content) {
				if (block.type !== "tool_use") continue;

				// Broadcast tool execution to clients
				this.broadcastToClients({
					type: "tool_call",
					tool: block.name,
					turn,
				});

				const result = await step.do(
					`tool-${turn}-${block.id}`,
					{ retries: { limit: 2, delay: "5 seconds" } },
					async () => {
						switch (block.name) {
							case "search_repos":
								return searchReposTool.run(block.input as SearchReposInput);
							case "get_repo":
								return getRepoTool.run(block.input as GetRepoInput);
							default:
								return `Unknown tool: ${block.name}`;
						}
					},
				);

				toolResults.push({
					type: "tool_result",
					tool_use_id: block.id,
					content: result,
				});
			}

			messages.push({ role: "user", content: toolResults });
		}

		return { status: "max_turns_reached", turns: 10 };
	}
}

Почему для LLM и инструментов нужны отдельные шаги?

Каждый step.do() создаёт контрольную точку. Если в вашем Workflow происходит сбой или Worker перезапускается:

Это особенно важно для:

4. Напишите своего Agent

Agent обрабатывает HTTP-запросы, WebSocket-соединения и события жизненного цикла Workflow. Он запускает экземпляр Workflow runWorkflow() и получает обновления о ходе выполнения через обратные вызовы.

Создайте src/agent.ts:

src/agent.ts
import { Agent } from "agents";

type State = {
	currentWorkflow?: string;
	status?: string;
};

export class ResearchAgent extends Agent<Env, State> {
	initialState: State = {};

	// Start a research task - called via HTTP or WebSocket
	async startResearch(task: string) {
		const instanceId = await this.runWorkflow("RESEARCH_WORKFLOW", { task });
		this.setState({
			...this.state,
			currentWorkflow: instanceId,
			status: "running",
		});
		return { instanceId };
	}

	// Get status of a workflow
	async getResearchStatus(instanceId: string) {
		return this.getWorkflow(instanceId);
	}

	// Called when workflow reports progress
	async onWorkflowProgress(
		workflowName: string,
		instanceId: string,
		progress: unknown,
	) {
		// Broadcast to all connected WebSocket clients
		this.broadcast(JSON.stringify({ type: "progress", instanceId, progress }));
	}

	// Called when workflow completes
	async onWorkflowComplete(
		workflowName: string,
		instanceId: string,
		result?: unknown,
	) {
		this.setState({ ...this.state, status: "complete" });
		this.broadcast(JSON.stringify({ type: "complete", instanceId, result }));
	}

	// Called when workflow errors
	async onWorkflowError(
		workflowName: string,
		instanceId: string,
		error: string,
	) {
		this.setState({ ...this.state, status: "error" });
		this.broadcast(JSON.stringify({ type: "error", instanceId, error }));
	}
}

5. Настройте свой проект

  1. Откройте wrangler.jsonc и добавьте конфигурацию Agent и Workflow:

    {
    	"$schema": "node_modules/wrangler/config-schema.json",
    	"name": "durable-ai-agent",
    	"main": "src/index.ts",
    	// Set this to today's date
    	"compatibility_date": "2026-08-28",
    	"observability": {
    		"enabled": true
    	},
    	"durable_objects": {
    		"bindings": [
    			{
    				"name": "ResearchAgent",
    				"class_name": "ResearchAgent"
    			}
    		]
    	},
    	"workflows": [
    		{
    			"name": "research-workflow",
    			"binding": "RESEARCH_WORKFLOW",
    			"class_name": "ResearchWorkflow"
    		}
    	],
    	"migrations": [
    		{
    			"tag": "v1",
    			"new_sqlite_classes": ["ResearchAgent"]
    		}
    	]
    }
    "$schema" = "node_modules/wrangler/config-schema.json"
    name = "durable-ai-agent"
    main = "src/index.ts"
    # Set this to today's date
    compatibility_date = "2026-08-28"
    
    [observability]
    enabled = true
    
    [[durable_objects.bindings]]
    name = "ResearchAgent"
    class_name = "ResearchAgent"
    
    [[workflows]]
    name = "research-workflow"
    binding = "RESEARCH_WORKFLOW"
    class_name = "ResearchWorkflow"
    
    [[migrations]]
    tag = "v1"
    new_sqlite_classes = [ "ResearchAgent" ]
  2. Сгенерируйте типы для своих привязок:

    npx wrangler types

    Это создаёт worker-configuration.d.ts файл с Env тип, который включает ваши привязки.

6. Напишите свой API

Worker передаёт запросы в Agent, который управляет жизненным циклом Workflow. Используйте routeAgentRequest() для подключений WebSocket и getAgentByName() для серверных вызовов RPC.

Замените src/index.ts:

src/index.ts
import { getAgentByName, routeAgentRequest } from "agents";

export { ResearchAgent } from "./agent";
export { ResearchWorkflow } from "./workflow";

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		// Route WebSocket connections to /agents/research-agent/{name}
		const agentResponse = await routeAgentRequest(request, env);
		if (agentResponse) return agentResponse;

		// HTTP API for starting research tasks
		if (request.method === "POST" && url.pathname === "/research") {
			const { task, agentId } = await request.json<{
				task: string;
				agentId?: string;
			}>();

			// Get agent instance by name (creates if doesn't exist)
			const agent = await getAgentByName(
				env.ResearchAgent,
				agentId ?? "default",
			);

			// Start the research workflow via RPC
			const result = await agent.startResearch(task);
			return Response.json(result);
		}

		// Check workflow status
		if (url.pathname === "/status") {
			const instanceId = url.searchParams.get("instanceId");
			const agentId = url.searchParams.get("agentId") ?? "default";

			if (!instanceId) {
				return Response.json({ error: "instanceId required" }, { status: 400 });
			}

			const agent = await getAgentByName(env.ResearchAgent, agentId);
			const status = await agent.getResearchStatus(instanceId);

			return Response.json(status);
		}

		return new Response("POST /research with { task } to start", {
			status: 400,
		});
	},
} satisfies ExportedHandler<Env>;

7. Разрабатывайте локально

  1. Создайте .env файл для локальной разработки:

    .env
    ANTHROPIC_API_KEY=your-api-key-here
  2. Запустите dev-сервер:

    npx wrangler dev
  3. Запустите задачу исследования:

    curl -X POST http://localhost:8787/research \
      -H "Content-Type: application/json" \
      -d '{"task": "Compare open-source LLM projects"}'
    { "instanceId": "abc-123-def" }
  4. Проверьте прогресс (это может занять несколько секунд):

    curl "http://localhost:8787/status?instanceId=abc-123-def"

Agent найдёт репозитории, получит подробную информацию и вернёт сравнение. Обновления о ходе выполнения рассылаются всем подключённым WebSocket-клиентам.

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

  1. Разверните Worker:

    npx wrangler deploy
  2. Добавьте свой API-ключ как секрет:

    npx wrangler secret put ANTHROPIC_API_KEY
  3. Запустите задачу исследования на развёрнутом Worker:

    curl -X POST https://durable-ai-agent.<your-subdomain>.workers.dev/research \
      -H "Content-Type: application/json" \
      -d '{"task": "Compare open-source LLM projects"}'
  4. Просмотр запусков workflow с помощью CLI:

    npx wrangler workflows instances describe research-workflow latest

    Здесь показан каждый шаг, который выполнил Agent, включая вызовы LLM, выполнение инструментов, тайминг и любые повторные попытки.

    Это также можно посмотреть на панели управления Cloudflare в разделе research-workflow.

    Перейдите в Workflows ↗

Интеграция клиента в реальном времени

Подключитесь к своему Agent через WebSocket, чтобы получать обновления о ходе выполнения в реальном времени. useAgent хук подключается к /agents/{agent-name}/{instance-name}:

/agents/research-agent/default  → ResearchAgent instance "default"
/agents/research-agent/user-123 → ResearchAgent instance "user-123"
import { useState } from "react";
import { useAgent } from "agents/react";

function ResearchUI({ agentId = "default" }) {
	const [progress, setProgress] = useState(null);

	const { state } = useAgent({
		agent: "research-agent", // Maps to ResearchAgent class
		name: agentId, // Instance name
		onMessage: (message) => {
			const data = JSON.parse(message.data);
			if (data.type === "progress") {
				setProgress(data.progress);
			}
		},
	});

	return (
		<div>
			{progress && (
				<p>
					{progress.message} ({Math.round(progress.percent * 100)}%)
				</p>
			)}
		</div>
	);
}

Имена классов Agent автоматически преобразуются в kebab-case для URL (ResearchAgent → research-agent).

Подробнее

Agents SDK Workflows

Полный справочник API для AgentWorkflow, включая колбэки жизненного цикла и двустороннюю связь.

События и параметры

Передавайте данные в Workflows и приостанавливайте выполнение в ожидании внешних событий с помощью waitForEvent.

Паузы и повторные попытки

Настройте поведение повторных попыток и режимы sleep.

API Workers

Изучите полный Workflows API для программного управления.

Agents SDK

Для интерактивных агентов с чатом в реальном времени и подключениями через WebSocket.