← Cloudflare Workflows / workflows / get-started
Создайте устойчивого ИИ-агента
В этом руководстве вы создадите ИИ-агента, который исследует репозитории GitHub. Дайте ему задачу, например «Сравнить проекты LLM с открытым исходным кодом», и он:
- Поиск подходящих репозиториев на GitHub
- Получите подробную информацию по каждому из них (звёзды, форки, активность)
- Проанализируйте и сравните их
- Вернуть рекомендацию
Каждый вызов 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-агента с нуля.
Предварительные требования
- Зарегистрируйтесь для получения Аккаунт Cloudflare ↗.
- Установка
Node.js↗.
менеджер версий Node.js
Используйте менеджер версий Node, например Volta ↗ или nvm ↗ чтобы избежать проблем с правами доступа и переключать версии Node.js. Wrangler, о котором пойдёт речь далее в этом руководстве, требует версию Node 16.17.0 или более поздней версии.
Вам также понадобится Ключ API Anthropic ↗ для вызовов LLM. Новым аккаунтам начисляются бесплатные кредиты.
1. Создайте новый проект Worker
-
Создайте новый проект Worker с помощью следующей команды:
npm create cloudflare@latest -- durable-ai-agentДля настройки выберите следующие параметры:
- Для С чего вы хотите начать?, выберите
Hello World example. - Для Какой шаблон вы хотите использовать?, выберите
Worker only. - Для Какой язык вы хотите использовать?, выберите
TypeScript. - Для Хотите использовать git для контроля версий?, выберите
Yes. - Для Хотите развернуть приложение?, выберите
No(мы внесём некоторые изменения перед развёртыванием).
- Для С чего вы хотите начать?, выберите
-
Перейдите в каталог проекта:
cd durable-ai-agent -
Установите зависимости:
npm install agents @anthropic-ai/sdk
2. Определите свои инструменты
Инструменты представляют собой функции, которые LLM может вызывать для взаимодействия с внешними системами. Вы определяете схему (какие входные данные принимает инструмент) и реализацию (что именно он делает). LLM самостоятельно решает, когда использовать тот или иной инструмент, в зависимости от задачи.
-
Создайте
src/tools.tsдвумя взаимодополняющими инструментами:src/tools.tsexport 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.
-
stepобъект предоставляет методы для определения устойчивых шагов. step.do(name, callback)выполняет код и сохраняет результат. Если выполнение Workflow прерывается, он возобновляется с последнего успешно выполненного шага.this.reportProgress()отправляет обновления о ходе выполнения в Agent (не является устойчивой операцией).this.broadcastToClients()отправляет сообщения всем подключённым клиентам WebSocket (не является устойчивой операцией).
Для более простого введения в тему обратитесь к Создайте свой первый Workflow.
Создайте 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 перезапускается:
- После шага LLM: ответ сохраняется. При возобновлении пропускается вызов LLM, и выполнение переходит к запуску инструмента.
- После шага инструмента: результат сохраняется. Если один из последующих инструментов завершится ошибкой, предыдущие инструменты не будут выполнены повторно.
Это особенно важно для:
- Вызовы LLM: дорого и медленно, не должно повторяться без необходимости
- Внешние API: может иметь ограничения по частоте запросов или побочные эффекты
- Идемпотентность: некоторые инструменты (например, отправка писем) не должны выполняться дважды
4. Напишите своего Agent
Agent обрабатывает HTTP-запросы, WebSocket-соединения и события жизненного цикла Workflow. Он запускает экземпляр Workflow runWorkflow() и получает обновления о ходе выполнения через обратные вызовы.
Создайте 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. Настройте свой проект
-
Откройте
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" ] -
Сгенерируйте типы для своих привязок:
npx wrangler typesЭто создаёт
worker-configuration.d.tsфайл сEnvтип, который включает ваши привязки.
6. Напишите свой API
Worker передаёт запросы в Agent, который управляет жизненным циклом Workflow. Используйте routeAgentRequest() для подключений WebSocket и getAgentByName() для серверных вызовов RPC.
Замените 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. Разрабатывайте локально
-
Создайте
.envфайл для локальной разработки:.envANTHROPIC_API_KEY=your-api-key-here -
Запустите dev-сервер:
npx wrangler dev -
Запустите задачу исследования:
curl -X POST http://localhost:8787/research \ -H "Content-Type: application/json" \ -d '{"task": "Compare open-source LLM projects"}'{ "instanceId": "abc-123-def" } -
Проверьте прогресс (это может занять несколько секунд):
curl "http://localhost:8787/status?instanceId=abc-123-def"
Agent найдёт репозитории, получит подробную информацию и вернёт сравнение. Обновления о ходе выполнения рассылаются всем подключённым WebSocket-клиентам.
8. Разверните
-
Разверните Worker:
npx wrangler deploy -
Добавьте свой API-ключ как секрет:
npx wrangler secret put ANTHROPIC_API_KEY -
Запустите задачу исследования на развёрнутом 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"}' -
Просмотр запусков 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).