Agent AtlasAI Agent 知识库
06.11教程 · Tutorials

部署到 Cloudflare

Worker + Durable Object + Vectorize + AI Gateway + Workflows,附 CI/CD 与成本监控。

这个教程把一个带 RAG 的客服 Agent 真正部署到 Cloudflare:Worker 接入、Durable Object 保存会话、Vectorize 检索、AI Gateway 代理模型、Workflows 跑长任务。

最终形态

text
https://support-agent.<account>.workers.dev/agents/support-agent/user_123
  • 同一 user_123 命中同一个 Durable Object;
  • 对话状态持久化;
  • /ingest 导入文档到 Vectorize;
  • 长任务交给 Workflow;
  • 模型调用经 AI Gateway,能看成本、缓存与错误。

前置

  • Cloudflare 账号
  • Node.js 20+
  • Wrangler 4+
  • OpenAI(或其他)API Key
bash
npm create cloudflare@latest -- support-agent
cd support-agent
npm install agents
npx wrangler login

Step 1:配置

wrangler.jsonc:

jsonc
{
  "name": "support-agent",
  "main": "src/index.ts",
  "compatibility_date": "2026-01-01",
  "observability": { "enabled": true },

  "durable_objects": {
    "bindings": [{ "name": "SupportAgent", "class_name": "SupportAgent" }]
  },
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["SupportAgent"] }],

  "ai": { "binding": "AI" },
  "vectorize": [
    { "binding": "VECTORIZE", "index_name": "support-docs" }
  ],
  "r2_buckets": [
    { "binding": "BUCKET", "bucket_name": "support-files" }
  ],
  "workflows": [
    { "name": "ESCALATION_WORKFLOW", "class_name": "EscalationWorkflow" }
  ]
}

创建资源:

bash
npx wrangler vectorize create support-docs --dimensions=1024 --metric=cosine
npx wrangler r2 bucket create support-files

bge-m3 的稠密向量是 1024 维,索引维度必须一致。换模型时重建索引。

Step 2:Agent 类

src/index.ts:

ts
import { Agent, routeAgentRequest } from "agents";

type Msg = { role: "user" | "assistant"; content: string };
type State = { messages: Msg[]; userId: string };

const SYSTEM = `你是 ACME 客服助理。
只根据检索到的政策和订单信息回答。
每个事实后标注来源编号,如 [1]。
资料不足时说「我需要转专员确认」,不要编造。
不承诺政策之外的赔付。`;

export class SupportAgent extends Agent<Env, State> {
  initialState: State = { messages: [], userId: "" };

  async onRequest(request: Request): Promise<Response> {
    const { message, userId } = await request.json<{ message: string; userId: string }>();

    if (!this.state.userId) this.setState({ ...this.state, userId });

    const sources = await this.search(message);
    const context = sources
      .map((s, i) => `[${i + 1}] ${s.title} · p.${s.page}\n${s.text}`)
      .join("\n\n");

    const messages = [
      { role: "system", content: SYSTEM },
      ...this.state.messages.slice(-6),
      {
        role: "user",
        content: `资料:\n${context || "(无)"}\n\n问题:${message}`,
      },
    ];

    const reply = await this.chat(messages);
    this.setState({
      ...this.state,
      messages: [...this.state.messages, { role: "user", content: message }, { role: "assistant", content: reply }],
    });

    return Response.json({ reply, sources });
  }

  async search(query: string) {
    const emb = await this.env.AI.run("@cf/baai/bge-m3", { text: [query] });
    const result = await this.env.VECTORIZE.query(emb.data[0], {
      topK: 6,
      returnMetadata: "all",
    });
    return result.matches.map((m) => ({
      id: m.id,
      score: m.score,
      title: String(m.metadata?.title ?? ""),
      page: Number(m.metadata?.page ?? 0),
      text: String(m.metadata?.text ?? ""),
    }));
  }

  async chat(messages: unknown[]): Promise<string> {
    const url = `https://gateway.ai.cloudflare.com/v1/${this.env.CF_ACCOUNT_ID}/${this.env.AI_GATEWAY}/openai/chat/completions`;
    const res = await fetch(url, {
      method: "POST",
      headers: {
        "content-type": "application/json",
        authorization: `Bearer ${this.env.OPENAI_API_KEY}`,
      },
      body: JSON.stringify({
        model: this.env.MODEL ?? "gpt-4.1-mini",
        messages,
        temperature: 0,
        max_tokens: 800,
      }),
    });
    if (!res.ok) throw new Error(`model error: ${res.status} ${await res.text()}`);
    const data = await res.json<{ choices: Array<{ message: { content: string } }> }>();
    return data.choices[0].message.content;
  }
}

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

    if (url.pathname === "/ingest" && request.method === "POST") {
      return ingest(request, env);
    }

    return (
      (await routeAgentRequest(request, env)) ??
      new Response("Not found", { status: 404 })
    );
  },
};

async function ingest(request: Request, env: Env): Promise<Response> {
  const token = request.headers.get("authorization")?.replace("Bearer ", "");
  if (token !== env.INGEST_TOKEN) return new Response("Forbidden", { status: 403 });

  const { docs } = await request.json<{ docs: Array<{ title: string; page: number; text: string }> }>();
  if (!Array.isArray(docs) || docs.length === 0) return Response.json({ ok: false, error: "empty" }, { status: 400 });

  const vectors = await env.AI.run("@cf/baai/bge-m3", { text: docs.map((d) => d.text) });
  await env.VECTORIZE.upsert(
    docs.map((d, i) => ({
      id: `${d.title}-${d.page}-${i}`,
      values: vectors.data[i],
      metadata: { title: d.title, page: d.page, text: d.text.slice(0, 1200) },
    }))
  );
  return Response.json({ ok: true, count: docs.length });
}

三个关键点:

  • 状态在 Durable Object:同一用户串行、持久化;
  • 检索结果带标题与页码:能引用;
  • 模型经 AI Gateway:统一看成本、缓存、错误。

Step 3:交互示例

bash
curl -X POST "https://support-agent.<account>.workers.dev/agents/support-agent/user_123" \
  -H "content-type: application/json" \
  -d '{"message":"A1024 签收 9 天了还能退吗?","userId":"user_123"}'

返回:

json
{
  "reply": "超过 7 天的退款需要人工审核。[1]",
  "sources": [{ "title": "退款政策", "page": 2, "score": 0.83 }]
}

Step 4:导入文档

分块后调用 /ingest:

bash
curl -X POST "https://support-agent.<account>.workers.dev/ingest" \
  -H "authorization: Bearer $INGEST_TOKEN" \
  -H "content-type: application/json" \
  -d '{"docs":[{"title":"退款政策","page":2,"text":"退款需在签收后 7 天内申请……"}]}'

大批量导入时用 Queue 分批,避免单次请求超时。每批 50–100 个片段,失败重试,记录进度。

Step 5:Workflows 跑长任务

客服升级、批量分析、报告生成可能超过请求时限:

src/workflow.ts:

ts
import { WorkflowEntrypoint, WorkflowStep } from "cloudflare:workers";

type Params = { ticketId: string; reason: string };

export class EscalationWorkflow extends WorkflowEntrypoint<Env, Params> {
  async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
    const brief = await step.do("build-brief", async () => {
      return await buildTicketBrief(event.payload.ticketId, this.env);
    });

    const decision = await step.do(
      "manager-review",
      { retries: { limit: 3, delay: "30 seconds", backoff: "exponential" } },
      async () => await requestManagerReview(brief, this.env)
    );

    await step.do("notify", async () => {
      await notifyCustomer(event.payload.ticketId, decision, this.env);
    });

    return { decision };
  }
}

配置里已经绑定 ESCALATION_WORKFLOW。触发:

ts
await this.env.ESCALATION_WORKFLOW.create({ params: { ticketId, reason } });

Workflows 提供每步持久化、重试、可等待人工,适合跨分钟到跨天的流程。

Step 6:AI Gateway

在 Cloudflare Dashboard 创建 Gateway,记录 account id 与 gateway 名,配置:

  • 缓存:相同请求命中缓存;
  • 限流:按用户/模型;
  • 回退:主模型失败切备用;
  • 日志:token、成本、错误;
  • 密钥:供应商 key 存在 Worker secret,或 Gateway 托管。

Worker 里:

bash
npx wrangler secret put OPENAI_API_KEY
npx wrangler secret put INGEST_TOKEN
npx wrangler secret put CF_ACCOUNT_ID
npx wrangler secret put AI_GATEWAY

不要把 key 写进 wrangler.jsonc 或代码。

Step 7:部署

bash
npm run dev          # 本地
npx wrangler deploy  # 线上

本地测试 Durable Object:

bash
curl -X POST "http://localhost:8787/agents/support-agent/user_1" \
  -H "content-type: application/json" \
  -d '{"message":"退款多久到账?","userId":"user_1"}'

Step 8:CI/CD

.github/workflows/deploy.yml:

yaml
name: deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test --if-present
      - uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          command: deploy

把 CLOUDFLARE_API_TOKEN 配成最小权限(Workers Scripts、Vectorize、R2、Workflows)。不要用全局 key。

限制与坑

限制应对
单请求时间/CPU 有限长任务用 Workflows/Queues
Durable Object 单线程按用户/会话分片
向量索引维度固定换模型必须重建
边缘位置用 locationHint,接受一致性权衡
冷启动减少依赖、预热
成本按请求/存储计预算与用量告警

成本估算

text
请求:Worker 请求数 + Durable Object 请求数
存储:DO SQLite + R2 + Vectorize 向量数
模型:AI Gateway 记录的 token

先按「每千次对话」估算,再乘峰值。不要只算模型 token,DO 与向量查询也会计费。

验收清单

  • 同一用户命中同一 DO,状态可恢复
  • /ingest 有鉴权,能分批导入
  • 检索结果带标题、页码、分数
  • 回答带引用,资料不足会说不知道
  • 长任务走 Workflow
  • 模型经 AI Gateway,有成本与错误日志
  • secrets 不入库、不进代码
  • CI 使用最小权限 token
  • 有预算与告警

常见错误

把状态放 Worker 全局变量

实例随时回收。状态必须放 Durable Object 或外部存储。

在请求里跑 5 分钟任务

Worker 会超时。超过 30 秒的流程改异步/Workflow。

向量维度和模型不匹配

写入成功但查询报错,或结果全错。先确认维度。

公网暴露 `/ingest`

任何人都能写你的知识库。必须鉴权 + 限流。

把供应商 key 放进前端

前端只拿短期会话 token,模型 key 永远留在服务端。

小结

  • Cloudflare 的组合:Worker 接入、DO 状态、Vectorize 检索、AI Gateway 模型、Workflows 长任务。
  • 状态与会话按用户分片,长任务异步化。
  • 检索要带来源,写入要鉴权。
  • 成本要按请求、存储、模型三部分估算。

下一篇做安全:8 个可复现的攻击与防御实验。

读完了?进度会保存在本地浏览器。
内容会持续修订。发现错误或想补充,欢迎反馈。 遇到问题?查排查手册 →