☰
基于Cloudflare Durable Objects的AI长任务状态管理实战
2026/10/1 4:18:26 网站建设 项目流程

我先说一个真实场景:去年我在做一个 AI 健身计划生成的小产品,用户填完目标、训练天数、器械条件,点一下“生成计划”,前端就开始转圈。最开始我只写了几个 Worker 接口,调用大模型返回 JSON,本以为很简单,结果上线第一天就被打脸——用户生成一个四天分化训练计划,LLM 平均要跑 30 到 40 秒,而 Worker 的请求有超时限制,请求一断,用户看到的不是“生成失败”,而是“一直转圈,最后什么都没发生”。更惨的是,同一个用户因为没拿到结果又点了几次生成,LLM 那边重复计费了好几次。

后来我把整套逻辑迁移到 Cloudflare Durable Objects 上,这个标题里看起来“重”的技术,反而成了整个服务里最省心的一环。这篇不是泛泛聊 AI,也不打算从 Durable Objects 的文档概念讲起,而是把它放进“AI 健身计划生成”这个真实业务里,拆清楚它到底解决了什么问题、怎么设计状态机、怎么处理超时和重复请求,以及我上线后撞到的四个坑。适合正在用 Workers 做 AI 应用、又觉得长任务和状态管理不好搞的开发者参考。

1. 为什么AI健身计划不能只靠一个Worker跑完

1.1 一个普通生成请求的三道坎

一个“生成训练计划”的请求,表面上只是调一次大模型,实际拆开看是这样的:先把用户的年龄、性别、训练目标、可用器械、每周天数、是否有伤病等十几个参数拼成提示词,然后调用 LLM 生成一份包含每周动作安排、组数、次数、休息时间的计划,最后解析 JSON 并落库。

这个过程有三个绕不开的问题。第一,LLM 本身慢,尤其是生成复杂 JSON 结构时,动辄二三十秒,有些推理模型甚至要一分钟;第二,普通 Worker 请求的生命周期是有上限的,你没法让一个 HTTP 请求挂在那里等一分钟;第三,也是最容易被忽略的——生成过程中如果用户关掉页面、断网、或者重复点击,服务端必须知道这件事进行到哪一步了,而普通 Worker 是无状态的,请求结束就什么都不记得了。

我当时的第一反应是“用队列不就行了”,把生成任务丢到队列里,后台慢慢跑。但仔细一想,队列方案还得分服务器,而且一个训练计划的生成结果要回传给用户,还得额外维护一套 job 状态表。整个复杂度一点没少,反而多了一个基础设施要管。

1.2 为什么选中 Durable Objects,而不是别的方案

先给没接触过的朋友一个简单类比:Durable Objects(以下简称 DO)就像你给每个用户请了一个专属管家,每个用户有一个固定的房间(实例),管家有长期记忆(持久化存储),并且同一时间只处理一件事(单线程事件循环)。不管用户什么时候回来问“我的计划生成好了吗”,管家都记得这件事做到哪一步了。

我整理过三种方案的对比,当时写进设计文档里,这里直接放出来:

方案长任务支持状态存储并发控制维护成本适合场景
普通 Worker + KV 存状态弱,超时即断需要自己写状态轮询弱,每个请求独立中短请求,状态简单
外部队列 + 任务表强,但要自建服务需要单独数据库需要额外实现高已有基础设施
Cloudflare Durable Objects强,事件驱动自带持久化 storage天然单实例串行低长任务 + 用户级状态

DO 最打动我的不是某个单一功能,而是“状态、并发、长任务”这三件事它天然都占了。再加上我本来就在 Workers 上,不需要再引入新的后端服务,整个技术栈还是只有一个 Cloudflare 边缘平台。

1.3 先理解 DO 的三个底层机制,再动手

用 DO 之前,我建议你先搞懂三个机制,不然写起来很容易踩坑。

第一个是实例寻址。通过idFromName(userId)可以生成一个确定性的 ID,同一个 userId 永远对应同一个 DO 实例。这就实现了“每个用户一个管家”,不同用户之间的状态天然隔离,不会出现 A 的训练计划写到 B 名下这种低级事故。

第二个是单线程事件循环。DO 的每个实例在同一时刻只处理一个事件,这意味着你在代码里不需要考虑“两个请求同时改同一个状态”的竞态问题。看似限制,其实是巨大的便利——做状态机的时候,读到状态、判断逻辑、写入新状态,整个流程是原子的。

第三个是持久化 storage。state.storage是 DO 自带的键值存储,写入后即使实例被冻结、重启,数据也还在。对于训练计划这种低频突变、需要稳定保存的数据,比 KV 更合适,因为不需要额外的缓存一致性逻辑。

这三件事理解透了,后面的代码就顺理成章。

2. 架构拆解:状态协调交给Durable Objects,AI生成交给LLM

2.1 从请求到生成的完整数据流

整个服务我拆成了三个文件:入口 Worker、DO 类、AI 调用模块。用户点的“生成计划”,实际走的是这样一条链路:

  1. 客户端 POST/api/plans,带用户信息和一个幂等键。
  2. 入口 Worker 根据 userId 拿到对应的 DO 实例,把请求转交给 DO 的fetch方法。
  3. DO 先检查是否已经存在生成中的计划,如果有就直接返回“已存在”,防止用户连点。
  4. DO 写入状态PLAN_GENERATING,设置一个立即触发的 alarm,然后立刻向客户端返回202 Accepted和轮询地址。
  5. alarm 触发后,DO 内部调用 LLM API,把返回的 JSON 校验、清洗、落库。
  6. 客户端轮询/api/plans/{userId},DO 根据状态返回PENDING、GENERATING、READY或FAILED,拿到READY后再取计划内容。

这里最关键的设计是第 4 步:请求先返回,AI 生成放在 alarm 里异步执行。如果你在 DO 的fetch里直接 await LLM,问题会和普通 Worker 一模一样——HTTP 连接等着大模型慢慢吐字。把真正的生成塞进 alarm,相当于把“接收请求”和“干活”彻底解耦,这也是 DO 能扛长任务的核心玩法。

2.2 核心代码:入口 Worker

入口 Worker 的代码很简单,主要就是拿到 DO 实例并转发请求:

// src/index.ts import { WorkoutPlanDO } from "./workout-do"; export interface Env { WORKOUT_PLAN_DO: DurableObjectNamespace; OPENAI_API_KEY: string; } export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url); const userId = url.searchParams.get("userId") || "default-user"; // 每个用户一个 DO 实例,按 userId 确定性寻址 const id = env.WORKOUT_PLAN_DO.idFromName(userId); const stub = env.WORKOUT_PLAN_DO.get(id); if (url.pathname === "/api/plans" && request.method === "POST") { // 创建/触发生成计划,DO 内部会做幂等处理 return stub.fetch(request); } if (url.pathname === "/api/plans" && request.method === "GET") { // 查询当前用户计划状态,任意 Worker 都可以查询,不一定走 DO return stub.fetch(request); } return new Response("Not Found", { status: 404 }); }, };

注意,入口 Worker 不需要自己写任何生成逻辑,它只做路由和转发。所有状态读写都收拢在 DO 内部。

2.3 核心代码:DO 类

DO 类是核心,状态机和 alarm 都写在里面,我简化后的版本长这样:

// src/workout-do.ts import { generateWorkoutPlan } from "./ai"; export class WorkoutPlanDO { private state: DurableObjectState; private env: Env; constructor(state: DurableObjectState, env: Env) { this.state = state; this.env = env; } async fetch(request: Request): Promise<Response> { const method = request.method; if (method === "POST") { return this.handleCreate(request); } if (method === "GET") { return this.handleQuery(); } return new Response("Method Not Allowed", { status: 405 }); } async handleCreate(request: Request): Promise<Response> { const body = await request.json(); const idempotencyKey = body.idempotencyKey || `plan-${Date.now()}`; // 幂等检查:同一幂等键下已经有状态,直接返回当前状态,不重复触发 const existing = await this.state.storage.get<any>("plan"); const status = await this.state.storage.get<string>("status"); if (existing && status === "READY") { return new Response(JSON.stringify(existing), { headers: { "content-type": "application/json" }, }); } // 写入初始状态并保存请求体,供 alarm 阶段使用 await this.state.storage.put("status", "PLAN_GENERATING"); await this.state.storage.put("request", JSON.stringify(body)); await this.state.storage.put("idempotencyKey", idempotencyKey); // 设置 0 毫秒后的 alarm,异步触发真正的 AI 生成 await this.state.storage.setAlarm(Date.now()); // 立即返回 202 + 轮询地址,避免请求超时 return new Response( JSON.stringify({ status: "PLAN_GENERATING", pollingUrl: `/api/plans?userId=${body.userId}`, }), { status: 202, headers: { "content-type": "application/json" }, } ); } async alarm(): Promise<void> { const request = JSON.parse(await this.state.storage.get<any>("request")); try { const plan = await generateWorkoutPlan(request, this.env); await this.state.storage.put("plan", plan); await this.state.storage.put("status", "READY"); } catch (err) { await this.state.storage.put("error", String(err)); await this.state.storage.put("status", "FAILED"); } } async handleQuery(): Promise<Response> { const status = await this.state.storage.get<string>("status"); const plan = await this.state.storage.get<any>("plan"); const error = await this.state.storage.get<string>("error"); if (status === "READY") { return Response.json({ status, plan }, { status: 200 }); } if (status === "FAILED") { return Response.json({ status, error }, { status: 500 }); } return Response.json({ status }, { status: 200 }); } }

这段代码里有个容易被忽略但很重要的细节:alarm里如果调用 LLM 抛错,一定要把FAILED状态写回去,并且把错误信息存下来。不然用户打死也不知道自己的计划为什么永远停在GENERATING。我早期没写 catch,debug 的时候全靠猜,浪费了一整个下午。

2.4 数据存储的分层:什么数据放哪里

DO 自带 storage 并不是唯一的数据存储,我最终把数据分成三层:

  • DO storage:存训练计划的状态机数据和最终计划 JSON,因为要跟着生成流程走,天然适合放在 DO 里;
  • D1 数据库:存用户档案、历史生成记录、每个计划的使用反馈,这些需要按条件查询,比如“找出所有这个月生成过计划的人”,DO 的 KV 存储不适合做查询;
  • KV:存静态资源和 AI 生成的提示词模板,读多写少,边缘缓存友好。

这样分不是为了炫技,而是每种存储的能力边界不一样。你要是图省事全塞 DO storage,后期会发现“按日期筛选计划”这种查询根本写不出来。反过来,把状态机这种需要强一致性的数据放 D1,又会在并发更新时遇到各种冲突。

3. 提示词与结构化输出:让生成结果可校验、可落库

3.1 提示词的第一原则:强制输出 JSON

很多人写 AI 健身计划提示词,让模型“返回一份周计划”,结果模型给你一段 Markdown 表格,甚至夹杂“祝您训练愉快”这种废话。我的做法是在系统提示词里直接声明:只输出 JSON,不要输出任何解释性文字。并且在 JSON 里嵌入一个可被程序直接识别的schema_version字段,为以后迭代留余地。

我目前线上用的提示词核心段落长这样,你可以直接抄:

你是一位拥有 CSCS 认证的健身教练。请根据以下用户档案和训练条件,生成一份一周训练计划。 用户档案: - 性别、年龄、身高体重(略) - 训练目标:增肌 - 每周可训练天数:4 - 可用器械:杠铃、哑铃、可调节凳 - 伤病限制:无 输出要求: 1. 只输出一个 JSON 对象,不要包含任何文字说明。 2. JSON 结构必须符合以下 Schema: { "schema_version": "1.0", "goal": "muscle_gain", "days_per_week": 4, "weeks": [ { "week": 1, "sessions": [ { "day": "Monday", "focus": "Push", "exercises": [ { "name": "Barbell Bench Press", "sets": 4, "reps": "8-10", "rest_seconds": 90, "notes": "控制离心,胸大肌充分拉伸" } ] } ] } ] } 3. 动作要具体,不要写「肩部练习」这种模糊描述,必须写出具体动作名。 4. 组数和次数要符合渐进超负荷原则,相邻两周要有合理调整。

给模型一个 Schema 模板,而不是一段描述,是结构化输出成功率最高的办法。实测下来,按这个方式,LLM 返回可解析 JSON 的成功率能到 95% 以上,剩下 5% 交给代码兜底。

3.2 拿回来先校验,再落库

LLM 返回的 JSON 不能直接信。我遇到过模型把"reps": "8-10"写成"reps": 8-10(没引号导致 JSON 解析失败),也遇到过"sets": 0这种业务上完全没意义的数值。

所以我加了两个层面的校验。第一层用 JSON Schema 校验器(我用的是 Ajv)验证结构完整性,字段类型对不对,必填字段有没有缺失。第二层是业务规则校验:所有动作必须出现在我的动作库里,组数必须在 1 到 6 之间,每次训练时间估算不能超过 90 分钟,一周内同一肌群的训练频率不能超过安全上限。业务校验不过会触发一次重试,重试时把失败原因拼进提示词,让模型修正。

// src/schema.ts import Ajv from "ajv"; const planSchema = { type: "object", required: ["schema_version", "goal", "days_per_week", "weeks"], properties: { schema_version: { type: "string" }, goal: { type: "string" }, days_per_week: { type: "number" }, weeks: { type: "array", minItems: 4, maxItems: 12, items: { type: "object", required: ["week", "sessions"], properties: { week: { type: "number" }, sessions: { type: "array", minItems: 1 }, }, }, }, }, }; export function validatePlan(plan: unknown): { ok: boolean; errors?: string[] } { const ajv = new Ajv(); const validate = ajv.compile(planSchema); const valid = validate(plan); return valid ? { ok: true } : { ok: false, errors: validate.errors?.map((e) => e.message || "") }; }

3.3 生成后的后处理:动作映射与渐进负荷

校验通过不等于可以直接用,我还会做一轮后处理。首先是动作映射:大模型有时会编出一个根本不存在的动作名,或者同一个动作在不同器械下的变体名称不一致,需要映射到我动作库里对应的标准名和肌群标签。其次是强度折算:根据用户档案里的训练水平,把固定组次范围换算成对应的 RPE(训练自觉强度),让计划对新手和进阶用户都可用。

最后一步是渐进超负荷检查。AI 生成的周计划经常出现训练量完全一样的情况,后处理会把每周总组数、总次数算出来,对比上一周,如果下周没有增加 2% 到 5% 左右的训练量,就自动调整最后一组的建议次数或组间休息时间,保证“增肌计划”在原理上站得住脚。这一步做完,用户才会觉得这不是一份“模板套娃”计划。

4. Durable Objects的三件脏活:状态机、幂等与限流

4.1 状态机设计:用户和管家都看得懂的流转

训练计划从创建到可用,我定义了五个状态:

状态含义用户看到的提示
PLAN_PENDING已收到请求,还没开始生成排队中
PLAN_GENERATINGAI 正在生成生成中,约需 30 秒
PLAN_READY生成完成,计划可查看查看计划
PLAN_FAILED生成失败,可重试生成失败,点击重试
PLAN_EXPIRED超时未完成请重新生成

这个状态机放在 DO 里,用state.storage保存当前状态,流转规则只有四种:创建时从PENDING到GENERATING;alarm 成功后从GENERATING到READY;失败时从GENERATING到FAILED;用户重试时从FAILED回到GENERATING。

为什么要有PENDING这个状态?因为我要区分“请求刚进来”和“alarm 正在生成”。有些极端情况下 alarm 可能延迟几秒,这段时间内用户来轮询,如果直接返回GENERATING,语义上也没错,但从可观测性角度,你没法判断是“即将生成”还是“已经卡住”。多一个状态,后面接监控和告警都方便。

4.2 幂等键:解决用户连点的“重复扣费”

这是整个服务里最现实的坑。用户点一次生成,前端看到没反应,又点一次,如果后端不做幂等,LLM 就会被调用两次,账单上就多一笔钱。

我的做法见前面代码:客户端在 POST 时带上一个idempotencyKey(通常是 UUID),DO 处理创建请求前先检查 storage 中是否已有同一个 key 对应的状态。如果有,直接返回当前状态,不再触发新的 alarm。

还有个细节值得提醒:幂等键的作用域要收紧到“用户级 DO 实例”内。也就是说,同一个用户的不同计划请求要生成不同 key,而同一个计划的重复点击要复用同一个 key。我的方案是前端在用户第一次点击时生成 UUID,后续重试都带着这个 UUID 走,后端据此判断是“重试”还是“新计划”。

4.3 限流:利用 DO 的天然串行,而不是靠额外的限流器

AI 生成服务最怕的不是流量大,而是多个请求同时在短时间内打到 LLM 上游,触发上游的 Rate Limit。我原来在普通 Worker 里做过一个“请求发起前检查是否在冷却期”的开关,结果分布式环境下根本不可靠——多个 Worker 实例同时检查,发现都没人访问,然后一起发出去,照样打爆。

换到 DO 以后,这个问题被架构层面解决了:同一个用户的所有请求都进同一个 DO 实例,而 DO 实例是单线程事件循环,同一时刻只处理一个事件。这意味着一万个用户同时点生成,LLM 上游看到的请求依旧来自不同 DO 实例,而每个用户之间的请求不会自相残杀。

当然,如果你有“一个用户同时生成多个计划”的需求,限流粒度就变成了用户维度:在 DO 内部用一个lastRequestAt时间戳判断,两次请求间隔小于 5 秒直接拒绝。因为都在同一个 DO 实例里,这个判断没有竞态问题,写起来非常简单。

4.4 异常补偿:LLM 挂了也不能让用户干等

AI 生成一定会失败,网络抖动、上游超时、上下文超长、JSON 解析失败,任何一环都可能挂。我的 alarm 里做了两层补偿。

第一层是重试:同一个任务最多重试 2 次,每次重试前先修改提示词,把上次失败的原因写进去。比如“上次返回的 JSON 缺少 required 字段 sessions,请确保每个 week 对象都有 sessions 数组”。第二次重试如果还失败,状态置为FAILED,不继续耗。

第二层是降级:如果连续两次失败,我会用一份本地存储的“通用模板计划”兜底,标记为TEMPLATE,用户能看到完整计划,同时界面提示“AI 生成暂时不可用,已为您提供专业模板”。这个兜底不赚钱,但保住了用户的留存率。用户不会因为你生成失败就流失,但会因为生成失败后“什么也得不到”而流失,这两件事差别很大。

5. 上线后撞上的四个坑:超时、重复扣费、JSON校验和状态丢失

5.1 第一个坑:返回了 202,但用户永远拿不到结果

我最初的设计没有用 alarm,而是想当然地在fetch里直接 await LLM,然后返回结果。结果就像开头说的,用户疯狂转圈。改成202 + alarm之后又踩了一个新坑:alarm 虽然触发了,但 DO 实例里没有对应的状态,查出来永远是空的。排查后才发现,我当时在 alarm 里没有先读request存储,而是直接用了this.request——但 DO 的 alarm 回调里根本没有 request 上下文。

修复方案就是代码里展示的那样:创建请求时,把用户参数完整存到 storage 里,alarm 触发时再去读。DO 的 fetch 和 alarm 之间不能共享内存,只能靠 storage 传数据,这个认知如果不建立起来,后面还会踩更多类似的坑。

5.2 第二个坑:重复请求导致 LLM 重复计费

这个坑我在第 4 章的幂等设计里已经详细说了,但上线时还是出过一次事。原因是前端在用户点击后,先调用了一次 POST,又因为一个 beforeunload 事件触发了一次 POST,两个请求几乎同时到达 DO——第一个走到了“写状态、设置 alarm”,第二个进来时发现状态已经是PLAN_GENERATING了就直接返回了,看起来是安全的。但因为我早期代码里用Date.now()当幂等键,两个请求生成了不同的幂等键,导致第二个请求绕过了幂等检查。

最后我把幂等键生成完全挪到客户端,用 crypto.randomUUID(),并且在后端把“创建新计划”和“幂等重试”两个逻辑分开,才彻底解决。经验是:幂等这件事必须从客户端到服务端一起设计,单独靠一端做,总有缝隙漏过去。

5.3 第三个坑:LLM 返回合法 JSON,但业务上完全不可用

Ajv 校验只能保证结构合法,不能保证内容合理。我遇到过一次 LLM 返回的 JSON 结构完全正确,但里面的动作名是“Arnold Press”,我的动作库里没有,直接存进去之后,用户在前端看到动作列表是空的。

修复方案是增加一个“业务语义校验”层,把动作名映射到动作库 ID,映射失败就返回重试信号。而且重试提示词里要写得足够具体:“你使用了不在动作库中的动作 Arnold Press,请从以下动作列表中选择:...”实测发现,把动作列表塞进提示词后,模型很少再乱编动作名了。不是因为它记性好,而是因为它不需要猜了。

5.4 第四个坑:训练计划突然“消失”了

这个坑发生在我开始改动 DO 的 storage 结构之后。我原来把整个计划 JSON 存在plan这个 key 下,后来想改成按周拆分的plan_1、plan_2,就在代码里加了读取plan_1的逻辑。结果老用户的所有计划全部读不出来了,因为他们的数据还是存在plankey 下。

这不是 DO 的问题,而是我在设计存储结构时没有做迁移兼容。后来我在读取逻辑里写了一个 fallback:先读新 key,读不到就读旧 key,读到旧 key 后异步做一次数据迁移。这里也提醒各位:DO 的 storage 结构一旦上线就会积累真实数据,改结构前一定要想好兼容策略,不然线上事故就是这样发生的。

5.5 排查链路:用 wrangler tail 定位 DO 内部状态

DO 的调试比普通 Worker 麻烦一点,因为你有两个层面要查:入口 Worker 的行为和 DO 实例内部的行为。我最常用的排查链路是三条命令配合:

# 查看入口 Worker 的请求日志 wrangler tail --format pretty # 在 DO 代码里主动打日志,定位状态流转 console.log("[DO] alarm triggered, current status:", await this.state.storage.get("status")); # 本地模拟 DO alarm 触发 wrangler dev --test-scheduled

提醒一句:wrangler tail默认能看到console.log输出,但 DO 实例的日志会带有durableObject字段,从那里可以确认是哪个 userId 的实例在报错。我排查问题时的习惯是,在 alarm 的入口和出口各打一条日志,把状态打出来,基本能在两分钟内定位是 LLM 问题、存储问题还是状态机问题。

6. 部署、鉴权与成本:一个真实小项目的落地清单

6.1 wrangler 配置与发布

DO 的配置集中在wrangler.toml,第一次配置时要注意两点:一个是durable_objects.bindings,一个是migrations。迁移配置尤其容易漏,没有它,你会一直收到“Durable Object class not found”之类的错误。

name = "workout-forge" main = "src/index.ts" compatibility_date = "2024-11-01" [[durable_objects.bindings]] name = "WORKOUT_PLAN_DO" class_name = "WorkoutPlanDO" [[migrations]] tag = "v1" new_sqlite_classes = ["WorkoutPlanDO"]

发布流程就三步:wrangler login登录,wrangler deploy发布,wrangler tail看日志。这一步没什么弯弯绕,但一定要把 migration 写在最前面,不然以后想给 DO 加新的方法,会发现实例调度不生效。

6.2 鉴权:别让你的 AI 计费接口裸奔

这个服务只要上线,就会有人扫你的端点。我的 AI 生成接口一开始用的是 userId 参数直接寻址,没有鉴权,结果被刷了三天,账单多了几十美元。后来统一改成:前端通过一个独立的登录服务换取短期 token,所有 /api/plans 请求都必须带Authorization: Bearer <token>,Worker 入口在转给 DO 之前先做 JWT 校验。

如果你不想引入认证服务,至少也要做 IP 维度的限流,或者在请求里加上客户端生成的匿名 ID 并配合签名校验。裸奔的 AI 接口不是技术问题,是钱的问题。

6.3 成本估算:DO 不贵,LLM 才是大头

很多人一听 Durable Objects 就觉得是“企业级大厂才用得起的架构”,实际上对于一个 5000 用户规模的小产品,DO 的成本完全可以量化。

项目估算方式月成本估算
DO 请求数假设每人每月生成 4 次计划 + 轮询 10 次,约 7 万次请求免费额度内或小几十元
DO 存储每个计划约 20KB,5000 人 × 历史 10 条计划 ≈ 1GB几元到十几元
DO 耗电量alarm 每次执行约 5 秒,每月约 1 万秒执行时长可控,个位数美元
LLM API每次生成消耗约 6 万 token,按中等模型算单次 0.1 美元左右,每月数千元

算完你就明白,这类产品的大头永远是模型调用费,DO 的基础设施成本几乎可以忽略不计。所以我在设计上反复强调幂等和防刷,省下的都是真金白银。

6.4 后续扩展:缓存、定时调整与共享模板

当用户量上来之后,有三个明显的优化方向。第一个是热门参数组合的缓存:很多人都是“增肌、每周 4 天、杠铃哑铃”,同一份计划模板可以复用,不需要每次都调 LLM,用 KV 缓存命中即可。第二个是周期性调整:DO 的 alarm 可以用来做计划到期提醒和每周训练完成后的 AI 微调,用户不需要每次手动重新生成。第三个是多用户共享:一个教练用户生成一份计划,批量分发给他的学员,每个学员还是各自独立的 DO 实例,但计划内容指向同一个模板,能大幅减少 LLM 调用。

这几个扩展我都试过一部分,最直观的收益是 LLM 调用次数直接降了 40%。

从开始踩超时的坑,到最终把整套状态逻辑收进 DO,我最大的体会是:所谓“用 Durable Objects 做 AI 应用”,并不是因为 DO 是时髦的技术名词,而是它恰好补上了 AI 长任务里最难受的那块拼图——异步协调和持久状态。如果你也在 Workers 上做 AI 产品,第二天的计划千万别让用户傻等,把生成逻辑丢进 alarm,把状态机放进 DO,你会回来感谢这个设计。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询