跳转到内容
搜索文档

构建持久 AI Agent

最后更新 查看 MarkdownAgent 设置

在本指南中,你将构建一个研究 GitHub 仓库的 AI agent。给它一个任务,例如「比较开源 LLM 项目」,它将:

  1. 在 GitHub 上搜索相关仓库
  2. 获取每个仓库的详细信息(star、fork、活跃度)
  3. 分析并比较它们
  4. 返回推荐结果

每次 LLM 调用和工具调用都会成为一个步骤——自包含、可单独重试的工作单元。如果任何步骤失败,Workflows 会自动重试。如果整个 Workflow 在任务中途崩溃,它将从最后一个成功的步骤恢复。

挑战 Workflows 解决方案
长时间运行的 agent 循环 持久执行,可承受任何中断
不可靠的 LLM 和 API 调用 自动重试,独立检查点
等待人工审批 waitForEvent() 可暂停数小时或数天
轮询任务完成 检查之间使用 step.sleep(),不消耗资源

本指南将 Agents SDK 与 Workflows 结合使用,实现实时进度更新,并使用 Anthropic SDK 进行 LLM 调用。相同的模式适用于任何 LLM SDK(OpenAI、Google AI、Mistral 等)。

快速入门

如果你想跳过步骤并拉取完整的 agent(使用 AI Gateway),请运行以下命令:

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

如果你熟悉 Cloudflare Workflows 或希望先探索代码,请使用此选项。

按照以下步骤学习如何从头构建持久 AI agent。

前提条件

  1. 注册 Cloudflare 账户
  2. 安装 Node.js

Node.js 版本管理器

使用 Voltanvm 等 Node 版本管理器,以避免权限问题并切换 Node.js 版本。本指南后续将介绍的 Wrangler 需要 Node 版本 16.17.0 或更高。

你还需要一个 Anthropic API 密钥用于 LLM 调用。新账户包含免费额度。

1. 创建新的 Worker 项目

  1. 运行以下命令创建新的 Worker 项目:

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

    进行设置时,请选择以下选项:

    • 对于 What would you like to start with?,选择 Hello World example
    • 对于 Which template would you like to use?,选择 Worker only
    • 对于 Which language do you want to use?,选择 TypeScript
    • 对于 Do you want to use git for version control?,选择 Yes
    • 对于 Do you want to deploy your application?,选择 No(部署前我们还会做一些修改)。
  2. 进入项目目录:

    cd durable-ai-agent
  3. 安装依赖:

    npm install agents @anthropic-ai/sdk

2. 定义工具

工具是 LLM 可以调用的函数,用于与外部系统交互。你定义 schema(工具接受哪些输入)和实现(它做什么)。LLM 根据任务决定何时使用每个工具。

  1. 创建 src/tools.ts,包含两个互补的工具:

    src/tools.tsts
    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

Agents SDK 中的 AgentWorkflow 类扩展了 Cloudflare Workflows,支持双向 Agent 通信。你的 Workflow 可以报告进度、向 WebSocket 客户端广播,并通过 RPC 调用 Agent 方法。

  • step 对象提供定义持久步骤的方法。
  • step.do(name, callback) 执行代码并持久化结果。如果 Workflow 被中断,它将从最后一个成功的步骤恢复。
  • this.reportProgress() 向 Agent 发送进度更新(非持久)。
  • this.broadcastToClients() 向所有已连接的 WebSocket 客户端发送消息(非持久)。

如需更温和的入门介绍,请参阅构建你的第一个 Workflow

创建 src/workflow.ts

src/workflow.tsts
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 生命周期事件。它通过 runWorkflow() 触发 workflow 实例,并通过回调接收进度更新。

创建 src/agent.ts

src/agent.tsts
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-17",
    	"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-17"
    
    [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,Agent 管理 workflow 生命周期。使用 routeAgentRequest() 处理 WebSocket 连接,使用 getAgentByName() 进行服务端 RPC 调用。

替换 src/index.ts

src/index.tsts
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 文件

    .envsh
    ANTHROPIC_API_KEY=your-api-key-here
  2. 启动开发服务器:

    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 密钥添加为 secret:

    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. 使用 CLI 检查 workflow 运行:

    npx wrangler workflows instances describe research-workflow latest

    这将显示 agent 执行的每个步骤,包括 LLM 调用、工具执行、计时和任何重试。

    你也可以在 Cloudflare 仪表板的 research-workflow 下查看。

    Go to Workflows ↗

实时客户端集成

通过 WebSocket 连接到你的 Agent 以接收实时进度更新。useAgent hook 连接到 /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(ResearchAgentresearch-agent)。

了解更多

事件与参数

向 Workflows 传递数据,并使用 waitForEvent 暂停等待外部事件。

Workers API

探索用于编程控制的完整 Workflows API。

Agents SDK

适用于具有实时聊天和 WebSocket 连接的交互式 agent。

这篇文档对您有帮助吗?