在本指南中,你将构建一个研究 GitHub 仓库的 AI agent。给它一个任务,例如「比较开源 LLM 项目」,它将:
- 在 GitHub 上搜索相关仓库
- 获取每个仓库的详细信息(star、fork、活跃度)
- 分析并比较它们
- 返回推荐结果
每次 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。
- 注册 Cloudflare 账户 ↗。
- 安装
Node.js↗。
Node.js 版本管理器
使用 Volta ↗ 或 nvm ↗ 等 Node 版本管理器,以避免权限问题并切换 Node.js 版本。本指南后续将介绍的 Wrangler 需要 Node 版本 16.17.0 或更高。
你还需要一个 Anthropic API 密钥 ↗用于 LLM 调用。新账户包含免费额度。
-
运行以下命令创建新的 Worker 项目:
npm create cloudflare@latest -- durable-ai-agentyarn create cloudflare durable-ai-agentpnpm 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(部署前我们还会做一些修改)。
- 对于 What would you like to start with?,选择
-
进入项目目录:
cd durable-ai-agent -
安装依赖:
npm install agents @anthropic-ai/sdk
工具是 LLM 可以调用的函数,用于与外部系统交互。你定义 schema(工具接受哪些输入)和实现(它做什么)。LLM 根据任务决定何时使用每个工具。
-
创建
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 获取特定仓库的详细信息。
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:
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:可能有速率限制或副作用
- 幂等性:某些工具(如发送邮件)不应运行两次
Agent 处理 HTTP 请求、WebSocket 连接和 Workflow 生命周期事件。它通过 runWorkflow() 触发 workflow 实例,并通过回调接收进度更新。
创建 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 }));
}
}-
打开
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" ] -
为绑定生成类型:
npx wrangler types这将创建
worker-configuration.d.ts文件,其中包含带有绑定的Env类型。
Worker 将请求路由到 Agent,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>;-
为本地开发创建
.env文件:.envsh ANTHROPIC_API_KEY=your-api-key-here -
启动开发服务器:
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 客户端。
-
部署 Worker:
npx wrangler deploy -
将 API 密钥添加为 secret:
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"}' -
使用 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(ResearchAgent → research-agent)。