创建具名支持 Agent

CloudflareBeginner
立即练习

简介

通常,人们会把 AI Agent 描述为能够进行推理或使用工具的模型。不过,在添加模型之前,应用首先需要可靠地回答一个更简单的问题:这次请求应该交给哪个持续存在的会话? 支持应用必须将 planning 的每次交互都发回同一个逻辑会话,同时让 billing 保持独立。

Cloudflare 的 Agents SDK 提供了更高级的 Agent 类来完成这项工作。每个具名 Agent 都由一个 SQLite Durable Object 实例提供支持。SDK 管理保存的状态和请求路由,而 Durable Objects 在底层提供稳定的身份与存储。你将同时看到这两层,而不是把 SDK 当成黑盒。

你将构建一个小型、刻意不使用 LLM 的支持应用:

  1. SupportAgent 定义单个支持会话需要保存和执行的内容。
  2. SupportAgent 绑定代表该类的命名空间。
  3. /agents/support-agent/planning 选择名为 planning 的实例。
  4. initialStatethis.statesetState() 让 SDK 持久化该实例的少量状态。

你将向一个具名会话写入两条备注,证明另一个会话保持隔离;停止并重新启动完整的本地运行时;将相同代码部署到 Cloudflare;在 Dashboard 中检查真实的绑定和命名空间;最后删除所有临时资源。

开始本课程前,请完成将 LabEx 连接到你的 Cloudflare 账户 该实验会介绍 LabEx VM 终端、Wrangler 设备授权、账户确认和账户 ID 配置。你应该已经通过 O01–O06 了解基本的 TypeScript Worker 和 Durable Object 身份模型。本实验不要求你掌握 Agents SDK、React 或模型相关知识。

当前官方文档显示,SQLite-backed Durable Objects 可在 Workers Free 计划中使用。本实验只创建一个临时类命名空间、少量微型 Agent 实例,并且只发送数量受限的请求。实验不会调用模型,也不需要 Workers Paid。环境设置会在 /home/labex/project/named-support-agent 中安装 Node.js 22.22.0、Agents SDK 0.23.0 和项目本地的 Wrangler 4.134.0;设置过程不会登录、创建云端状态、部署代码或完成学习者的实现。

授权 VM 并配置 Agent

在本步骤中,你将授权 Wrangler、确认用于学习的目标账户,并描述一个 Agent 类,但暂时不会部署任何内容。这个全新的 VM 拥有独立的文件系统,因此,即使你已经登录 Cloudflare Dashboard,终端也不会因此获得授权。

进入准备好的项目目录,并确认固定版本:

cd /home/labex/project/named-support-agent
node --version
npx wrangler --version
npm list agents --depth=0

预期版本为 Node.js v22.22.0、Wrangler 4.134.0agents@0.23.0。固定版本很重要,因为 Agents SDK 的变化速度比基础 Worker API 更快。

启动设备授权流程:

npx wrangler login --device --browser=false

Wrangler 会输出浏览器 URL 和一段较短的设备代码。打开该 URL,输入代码,确认选中的账户是专用学习账户,并在授权前检查请求的权限。不要在终端中输入 Cloudflare 密码或 API token。

浏览器报告成功后,返回终端并等待 Wrangler 完成操作。请求结构化的身份信息:

npx wrangler whoami --json

确认输出中包含 loggedIn: true。然后只显示账户名称,并私下选择属于 LabEx Learning 的 ID:

WHOAMI="$(npx wrangler whoami --json)"
printf '%s\n' "$WHOAMI" | jq '{loggedIn, authType, accounts: [.accounts[] | {name}]}'
ACCOUNT_ID="$(printf '%s\n' "$WHOAMI" | jq -r '.accounts[] | select(.name == "LabEx Learning") | .id')"
test -n "$ACCOUNT_ID"

如果你的专用学习账户使用了不同的显示名称,请先确认正确名称,再仅替换 LabEx Learning。账户 ID 是配置信息,不是秘密信息,但上述命令可以避免不必要地打印它。

创建一个唯一的临时 Worker 名称:

RUN="labex-c11-s01-$(openssl rand -hex 6)"
printf '%s\n' "$RUN"

创建 wrangler.jsonc

cat > wrangler.jsonc <<JSON
{
  "\$schema": "./node_modules/wrangler/config-schema.json",
  "name": "$RUN",
  "account_id": "$ACCOUNT_ID",
  "main": "src/index.ts",
  "compatibility_date": "2026-09-18",
  "compatibility_flags": ["nodejs_compat"],
  "workers_dev": true,
  "preview_urls": false,
  "observability": {
    "enabled": true,
    "head_sampling_rate": 1
  },
  "durable_objects": {
    "bindings": [
      { "name": "SupportAgent", "class_name": "SupportAgent" }
    ]
  },
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["SupportAgent"] }
  ]
}
JSON

SupportAgent 绑定是 Worker 访问该类命名空间的句柄。v1 迁移会告知 Cloudflare 创建这个使用 SQLite 存储的类。Agents 使用 Durable Object 这一基础资源层;SDK 不会移除该资源层。nodejs_compat 是 SDK 当前所需的配置。在部署之前,这些声明都不会创建云端资源。

实现具名支持 Agent

在本步骤中,你将实现所有具名支持会话共用的状态和 HTTP 行为。Agent 类是可复用的行为定义,而 Agent 实例是一个具名会话,例如 planning。Cloudflare 可以运行同一个类的多个实例,并且每个实例都拥有独立状态。

创建 src/index.ts

cat > src/index.ts <<'TS'
import { Agent, routeAgentRequest } from "agents";

export interface SupportState {
  status: "new" | "active";
  noteCount: number;
  lastNote: string | null;
}

interface Env {
  SupportAgent: DurableObjectNamespace<SupportAgent>;
}

function json(value: unknown, init: ResponseInit = {}): Response {
  const headers = new Headers(init.headers);
  headers.set("content-type", "application/json; charset=utf-8");
  return new Response(JSON.stringify(value, null, 2), { ...init, headers });
}

export class SupportAgent extends Agent<Env, SupportState> {
  initialState: SupportState = {
    status: "new",
    noteCount: 0,
    lastNote: null
  };

  async onRequest(request: Request): Promise<Response> {
    if (request.method === "GET") {
      console.log(JSON.stringify({ event: "support_agent_read", instance: this.name, noteCount: this.state.noteCount }));
      return json({ instance: this.name, ...this.state });
    }

    if (request.method === "POST") {
      const body = await request.json<{ note?: unknown }>().catch(() => null);
      const note = typeof body?.note === "string" ? body.note.trim() : "";
      if (note.length < 1 || note.length > 120) {
        return json({ error: "note must contain 1-120 characters" }, { status: 400 });
      }

      this.setState({
        status: "active",
        noteCount: this.state.noteCount + 1,
        lastNote: note
      });
      console.log(JSON.stringify({ event: "support_agent_updated", instance: this.name, noteCount: this.state.noteCount }));
      return json({ instance: this.name, ...this.state });
    }

    return json({ error: "method not allowed" }, { status: 405 });
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname === "/health") {
      return json({ status: "ok" });
    }

    const agentResponse = await routeAgentRequest(request, env, {
      onBeforeRequest(incoming, { name }) {
        if (!/^[a-z][a-z0-9-]{1,31}$/.test(name)) {
          return json({ error: "invalid support session name" }, { status: 400 });
        }
        return incoming;
      }
    });
    return agentResponse ?? json({ error: "not found" }, { status: 404 });
  }
} satisfies ExportedHandler<Env>;
TS

从内到外阅读以下重要部分:

  • initialState 是全新具名实例首次看到的值。
  • this.state 读取该实例当前由 SDK 管理的状态。
  • setState() 会同步验证并将替换后的状态保存到该实例的 SQLite 存储中;后续实验还会使用它将状态同步到已连接的客户端。
  • this.name 是由路由选择的稳定实例名称。它不是类名,也不是随机的进程 ID。
  • routeAgentRequest()/agents/<binding>/<name> 映射到正确的 Agent。SupportAgent 绑定会在 URL 中变成 support-agent
  • onBeforeRequest 会在选择 Durable Object 实例之前拒绝格式错误的名称,避免产生不需要的持久身份。

日志只记录合成的实例名称和计数。日志会刻意排除备注文本,以便后续 Dashboard 实验不会保留支持内容。

生成类型并在运行前构建

在本步骤中,你将生成与配置相关的类型,并在不部署的情况下构建 Worker。生成的 Worker 类型会将配置连接到 TypeScript,在本地进程或云端部署消耗时间之前,提前发现拼写错误的绑定或类名。

根据 wrangler.jsonc 生成类型:

npx wrangler types

Wrangler 会写入 worker-configuration.d.ts。确认其中包含已配置的 Agent 绑定,但不要打印其他无关的生成内容:

grep -n "SupportAgent" worker-configuration.d.ts | head

运行 TypeScript 编译器:

npm run check

脚本标题之后没有其他输出,表示编译器没有发现错误。现在让 Wrangler 构建部署包,但不要连接 Cloudflare 或创建资源:

npx wrangler deploy --dry-run --outdir .labex/dry-run

预期会看到成功的上传大小摘要和 SupportAgent Durable Object 绑定。dry run 只能在本地验证打包和配置;它不能证明授权、远程存储或边缘运行时行为正常。

验证本地身份和重启后的持久化

在本步骤中,你将验证三个不同的属性:重复使用同一个名称会访问同一份状态,使用不同名称会保持隔离,保存的状态在完整重启开发进程后仍然存在。

在后台启动本地 Workers 运行时:

npm run dev > .labex/dev.log 2>&1 &
echo $! > .labex/dev.pid
for attempt in $(seq 1 30); do
  if curl --silent --fail http://127.0.0.1:8787/health; then
    break
  fi
  sleep 1
done

预期输出为 {"status":"ok"}。读取新的 planning Agent:

curl --silent http://127.0.0.1:8787/agents/support-agent/planning | jq

它初始应包含 status: "new"noteCount: 0lastNote: null。添加两条合成备注:

curl --silent --request POST --header 'content-type: application/json' \
  --data '{"note":"Customer cannot open the invoice"}' \
  http://127.0.0.1:8787/agents/support-agent/planning | jq
curl --silent --request POST --header 'content-type: application/json' \
  --data '{"note":"Asked customer to retry"}' \
  http://127.0.0.1:8787/agents/support-agent/planning | jq

第二次响应应报告 instance: "planning"status: "active"noteCount: 2,以及第二条备注。两次请求使用了相同的 URL 名称,因此访问的是同一个逻辑 Agent。

读取另一个实例:

curl --silent http://127.0.0.1:8787/agents/support-agent/support | jq

support 仍然拥有自己的初始状态,计数为 0。两个名称共用同一个类的行为,但不共用保存的值。

拒绝格式错误的名称:

curl --silent --write-out '\nHTTP %{http_code}\n' \
  http://127.0.0.1:8787/agents/support-agent/INVALID

预期会看到 invalid support session name 和 HTTP 400

停止刚才启动的准确进程,然后使用相同的本地持久化目录启动新进程:

kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true
npm run dev > .labex/dev-restart.log 2>&1 &
echo $! > .labex/dev.pid
for attempt in $(seq 1 30); do
  if curl --silent --fail http://127.0.0.1:8787/health; then
    break
  fi
  sleep 1
done
curl --silent http://127.0.0.1:8787/agents/support-agent/planning | jq
curl --silent http://127.0.0.1:8787/agents/support-agent/support | jq

完整重启 Wrangler 后,planning 仍应为 2,而 support 仍应为 0。这比在同一个 JavaScript 进程中读取两次更有说服力:数据来自本地 Durable Object 持久化目录。

部署并操作云端 Agent 实例

在本步骤中,你将部署未修改的应用,并操作真正由云端管理的 Agent 实例。本地证据无法证明所选 Cloudflare 账户拥有该资源,也无法证明边缘运行时提供相同的具名身份。

停止本地进程并部署:

kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true
npx wrangler deploy

Wrangler 会应用 v1 迁移,创建使用 SQLite 的 SupportAgent 类命名空间,并输出公开的 workers.dev URL。将示例替换为实际 URL,并保存该准确 URL:

WORKER_URL="https://YOUR_WORKER_URL"

等待无状态健康检查路由可用:

for attempt in $(seq 1 30); do
  if curl --silent --fail "$WORKER_URL/health"; then
    break
  fi
  sleep 2
done

现在使用合成数据操作云端实例:

curl --silent --request POST --header 'content-type: application/json' \
  --data '{"note":"Cloud planning note one"}' \
  "$WORKER_URL/agents/support-agent/planning" | jq
curl --silent --request POST --header 'content-type: application/json' \
  --data '{"note":"Cloud planning note two"}' \
  "$WORKER_URL/agents/support-agent/planning" | jq
curl --silent --request POST --header 'content-type: application/json' \
  --data '{"note":"Independent support note"}' \
  "$WORKER_URL/agents/support-agent/support" | jq

读取两个实例:

curl --silent "$WORKER_URL/agents/support-agent/planning" | jq
curl --silent "$WORKER_URL/agents/support-agent/support" | jq

云端的 planning Agent 计数应为 2;独立的 support Agent 计数应为 1。本地和云端存储有意保持分离,但两个环境都实现了相同的名称到实例映射契约。

验证器还会创建两个本次运行唯一的 Agent 名称,并重复验证初始状态、同名持久化、不同名称隔离和无效名称拒绝。它绝不会把本地文件或命令历史当作远程行为的证明。

将运行时证据连接到 Dashboard

在本步骤中,你将把终端中的行为与 Dashboard 中可见的绑定、命名空间和日志对应起来。截图中的名称、时间戳和总数来自测试运行示例;请使用你自己终端生成的唯一 labex-c11-s01-... 名称。

在 Cloudflare Dashboard 中打开 Workers & Pages,选择你的临时 Worker。概览页面会显示已部署的应用和近期流量。

Workers and Pages 中已部署的具名支持 Agent Worker

打开 Worker 的 Bindings 标签页。找到连接到 SupportAgent Durable Object 类的 SupportAgent。第一个标签是 Worker 代码和路由中使用的名称;类名称标识从 src/index.ts 导出的实现。

连接到 Durable Object 类的 SupportAgent 绑定

从 Developer Platform 导航中打开 Durable Objects,选择由你的准确 Worker 所拥有的命名空间。确认类为 SupportAgent,并确认 Storage: SQL。命名空间是类级别的集合;planningsupport 和验证器使用的名称都是其中的独立实例。示例图片为保护隐私,省略了本次运行专用的命名空间 ID。

显示 SQL 存储的 SupportAgent 命名空间

返回 Worker 并打开 Observability → Logs。找到并展开一个 support_agent_readsupport_agent_updated 应用事件。将其中合成的 instancenoteCount 与某个受限请求对应起来。应用不会记录任何备注文本。

包含实例名称和备注计数的结构化支持 Agent 事件

Dashboard 指标和日志可能会延迟到达,因此最近图表为空并不能说明操作失败。经过身份验证的 API、命名空间所属关系和实时运行时检查仍然是权威依据。截图用于展示这些关系在界面中的位置,不是学习者提交内容。

删除 Agent 命名空间和 Worker

在本步骤中,VM 仍处于授权状态,你将永久删除准确的 Agent 命名空间和 Worker。Agent 状态属于 Durable Object 类命名空间,因此只删除 Worker 脚本并不等于明确请求删除已保存的状态。Cloudflare 迁移是追加式的:保留 v1,然后为准确的类添加 v2 删除迁移。

创建一个不导出 Agent 的最小清理入口点:

cat > src/cleanup.ts <<'TS'
export default {
  fetch() {
    return Response.json({ status: "cleanup" }, { status: 410 });
  }
};
TS

从原始配置中读取准确的名称和账户,然后创建 wrangler.cleanup.jsonc

RUN="$(node -e 'console.log(JSON.parse(require("fs").readFileSync("wrangler.jsonc", "utf8")).name)')"
ACCOUNT_ID="$(node -e 'console.log(JSON.parse(require("fs").readFileSync("wrangler.jsonc", "utf8")).account_id)')"
cat > wrangler.cleanup.jsonc <<JSON
{
  "\$schema": "./node_modules/wrangler/config-schema.json",
  "name": "$RUN",
  "account_id": "$ACCOUNT_ID",
  "main": "src/cleanup.ts",
  "compatibility_date": "2026-09-18",
  "compatibility_flags": ["nodejs_compat"],
  "workers_dev": true,
  "preview_urls": false,
  "migrations": [
    { "tag": "v1", "new_sqlite_classes": ["SupportAgent"] },
    { "tag": "v2", "deleted_classes": ["SupportAgent"] }
  ]
}
JSON

保留 v1 很重要:迁移历史是一个序列,而不是可以重写的描述。v2 会永久删除该类命名空间以及其中的所有临时具名实例。

部署删除迁移:

npx wrangler deploy --config wrangler.cleanup.jsonc

阅读迁移输出,确认删除的只有你的唯一 Worker 中的 SupportAgent。然后删除剩余的无状态清理 Worker:

npx wrangler delete --config wrangler.cleanup.jsonc --force

如果出现提示,请确认准确的 labex-c11-s01-... 应用。在 Workers & Pages 中确认准确的 Worker 已不存在。该测试账户不包含其他无关应用,因此通过测试时,整个列表会变为空。拥有其他项目的账户应保留那些无关条目。

删除临时 Worker 后显示没有项目的 Workers and Pages

打开 Durable Objects,确认被删除 Worker 所拥有的命名空间也已不存在。接受测试的账户不包含其他无关命名空间,因此列表会显示没有 Durable Objects。不要为了匹配示例而删除属于其他项目的命名空间。

移除 SupportAgent 类后显示没有命名空间的 Durable Objects

历史日志可能会暂时保留,但它们不是活动资源。

在退出登录前运行经过身份验证的资源不存在检查:

python3 .labex/verify.py deleted

只有 PASS: deleted 才能证明所选账户不再包含这两个归其所有的资源。由于授权丢失或网络错误导致的 404,不能作为删除证据。

撤销此 VM 的授权

在本步骤中,你将删除保存在这个临时 VM 中的 OAuth 授权。云端资源清理和本地凭据清理解决的是不同问题;Worker 和命名空间已经删除。

退出登录:

npx wrangler logout

让 Wrangler 返回结构化状态:

npx wrangler whoami --json

结果必须明确包含 "loggedIn": false。这个结构化值比友好提示更可靠,因为经过测试的 Wrangler 版本在多种身份验证状态下可能产生普通文本输出。网络失败无法说明是否已退出登录,应重试,而不是将其解释为退出成功。

你已经删除了本实验创建的两类状态:远程支持 Agent 命名空间和 Worker,以及 VM 的本地授权。

总结

你构建了本课程中的第一个 Cloudflare Agent,没有让 AI 术语掩盖底层基础。你了解到,一个 Agent 类定义行为;它的绑定暴露一个 SQLite Durable Object 命名空间;稳定的 URL 名称选择一个逻辑实例;Agents SDK 通过 this.statesetState() 持久化 initialState 的更新。

你在本地验证了同名持久化、不同名称隔离和进程重启后的持久性;随后在 Cloudflare 学习账户中重复验证了这一契约;将运行时证据与 Dashboard 中的绑定、命名空间和隐私受限日志对应起来;最后删除了云端资源和 VM 授权。下一实验将把浏览器客户端连接到这些状态,并介绍受控的实时同步。