跳到主内容
创建时间:最后更新:作者:白川

把重复任务变成可复用流程

已有聊天产品,为什么还需要一个工作台? 产品问题陈述与任务价值表。

内容状态:本文的判断依据、源码引用与状态机已逐条对照当前仓库(基线 c42797a9)复核。完整步骤教程、练习代码和真实用户实验尚未交付;文中所有商业价值判断都按产品假设处理,不构成收益保证。

你用 AI 写完一篇文章的第一稿,第二天回来接着改,发现上一轮对话找不到了。

你记不清自己当初有没有交代过「读者是转 AI Agent 的前端工程师」,也想不起昨天那几次生成到底扣了多少额度,于是不太敢再点一次生成。

这不是模型不够聪明。模型给你写完了一段文字,缺的是围着这段文字的一圈东西:这次任务进行到哪一步、产出存在哪、失败之后从哪继续、这些调用花了谁的钱。聊天窗口一次只管一件事——把你这句话变成那段文字,没有地方放这些。

工作台补的就是这一圈。这一章要你交付的,是一份能讲清楚「工作台到底省掉了哪一步」的产品问题陈述和任务价值表。

如果还没确定为什么投入这个方向,可以先阅读 为什么学 Agent 开发。本章从具体任务出发,把学习动机进一步落到产品问题上。

先把两种工作单元分开

「用 AI 写文章」和「跟 AI 聊两句」经常被混成同一件事,因为它们都发生在对话框里。但它们的工作单元并不一样:

一次问答一个任务
什么时候算结束
模型输出完就结束
产出被保存下来才算结束
能不能跨页面
不需要
需要,刷新和切标签后还得在
能不能跨进程
不需要
需要,关掉页面后服务端还在跑
失败之后怎么办
重发一次
从断点继续,不重复调用也不重复计费
最后留下什么
一段对话记录
成果、版本、归属账号和花费

差别全在最后一行上。

一个任务要在「模型输出完」之后继续活着,就得有四样东西撑住,聊天窗口一样都不提供:

  • 状态:现在进行到哪一步;
  • 归属:产出存在哪、属于谁、是第几版;
  • 恢复:中断或失败之后从哪继续;
  • 账本:这次消耗了多少、算在谁头上。

这四样都不需要更聪明的模型,但都需要服务端留一份记录。这条线就是「聊天产品」和「工作台」之间比较硬的分界。

一次创作任务如何影响余额,可以用一个状态机看清楚。下面这张图对应 server/src/features/shared/creative-tasks/creative-task.service.ts 的实现:

预占一次额度,之后的余额跟着任务状态走提交请求同一幂等键只建一个任务排队中预占额度(reserve)等待被调度执行执行中调用模型或供应商归档中写入对象存储保存版本与归属完成按实际用量结算(settle)已取消释放预占(release)失败释放预占(release)重跑归档复用已生成的结果不重复调用模型reserve 预占settle 结算release释放release释放归档失败settle 结算预占一次额度,之后的余额跟着任务状态走

从「重跑归档」出发、再回到「完成」的那条路径值得单独说。

模型已经出结果了,但把结果落到存储这一步失败了。这时任务停在 persisting,output 里保留着 phase: "provider_completed" 和已经生成的原始输出。recoverPersistence 拿这份输出只重跑归档,成功之后才结算——不会重新调用模型,也不会重新扣一次钱。

同样的事如果发生在聊天窗口里,这条路径根本不存在:调用的钱已经花了,结果只留在浏览器内存里,刷新就丢,而账单上已经扣过。

余额那一侧是同一套逻辑。reserve 先冻结预估量,拿到结果后按实际用量 settle,失败或取消就 release。每条命令都带一个幂等键(代码里还预留了 reverse 用于冲正):

typescript
const key = (
  taskId: string,
  attempt: number,
  command: "reserve" | "settle" | "release" | "reverse",
) => `creative:${taskId}:${attempt}:${command}`

键里带上 attempt,是为了把同一任务的第一次尝试和第二次尝试区分开——重试不会互相覆盖,两次尝试也各自留下一条账。任务 ID 本身由「用户 ID + 幂等键」派生(SHA-256 前 16 字节),所以并发重试会落到同一笔账上,而不是记成两笔。

这些缺口是工程必然,不是产品洁癖

有三个现实约束,逼着「一次问答」长成「一个任务」。

一次调用活不过一次页面交互。 生图、生成文章这类请求跑起来就没完,用户会切走标签页、刷新页面,甚至直接关掉浏览器。服务端不把任务记下来,用户回来看到的就是一片空白。

供应商有自己的节奏。 有的接口同步返回结果,有的返回一个任务 ID 让你轮询。源码里为此留了 waiting_provider 状态和 externalTaskId 字段,接住「供应商还没出结果」这段空窗。

失败点和成功点不是同一个地方。 调用模型可能失败,写存储也可能失败,还可能两件都做完了、中间某个非关键步骤挂了。用一个状态位记不住这种局面,所以状态被拆成了 queued、running、waiting_provider、persisting、succeeded、failed、cancelled 几个,各自对应不同的余额命令。

这也是为什么工作台通常先长成任务调度和记录系统,再长出界面——界面是这套记录的投影。

成果要有归属和版本

任务状态解决了「进行到哪」,产物本身还得回答两个问题:存在哪,属于谁。

当前工程是本地优先加云端按账号绑定。

本地那一份存在浏览器的 IndexedDB 里。 文章工作室的草稿、版本和作品都走 packages/app-kit/src/storage/aiwork-article-studio.ts。每次保存会往 versions 对象仓库里写一条快照:新的 UUID、articleId 指回母稿、savedAt 记下时间,版本列表按 savedAt 倒序排,恢复时还能用 articleId 挡住「拿别的文章的版本过来恢复」。本地图片资产同理,图片本身以 Blob 形式留在本地,读取时再 createObjectURL 出来。

云端那一份绑定到 ownerId。 草稿上挂一个 cloudSync 字段,里面记着 ownerId、sourceKey、articleId 和同步时的 version。同步时先比对 ownerId:对不上就新建一条绑定,换一个 sourceKey、不挂已有的 articleId,不会把别人的文章覆盖掉。

本地记录也按账号分区。 图片工作室的每条本地记录都带 ownerId,查询时按它过滤。这里有一处宽松处理值得记下:ownerId 未设置的记录在查询时被当作可见,多账号场景下应该收紧。

个人自带的模型配置同样按账号分区。 用户自己填的模型存在 localStorage 里,键名是 offer-ai-models:v1:${userId},换用户或退出登录时清空。注意这里存的是明文的 apiKey——用户把自己的 Key 交给浏览器,就等于交给了这台设备上的任何一段脚本。这是信任成本的一部分,而不是已经解决的问题。

你可能不需要工作台

上面这套东西有成本:服务端状态机、账本、存储、权限,都得有人维护。所以第一个问题不是「工作台好不好用」,而是「这件事该不该做成工作台功能」。

如果你的使用方式符合下面任何一条,现成的聊天产品大概率就够:

  • 一天问不到几次,每次都从头开始,不需要接着上次;
  • 生成的文字拿出去就用,不需要再打开修改;
  • 不介意在几个标签页之间来回复制粘贴;
  • 用量小到可以忽略,或者干脆自己填 Key、不经过平台。

判断可以按三个问题从上到下走一遍:

该不该做成工作台功能,问完三个问题就有答案会重复发生吗?只偶尔问一次的问答成果要留着改吗?生成一次就不再打开要从中途接着做吗?失败、重启或换设备之后做成工作台功能服务端任务状态版本归属与计费账本用现成聊天产品把背景重新贴一次一次生成加存储结果能下载下来即可单任务接口加账本记录用量与失败原因否是否是否是该不该做成工作台功能,问完三个问题就有答案

三个问题问完,答案落在「做成工作台功能」还是「用现成聊天产品」,其实回答的不是「要不要一个好用的界面」,而是「要不要一个服务端的任务记录」。前两个问题都答「否」,说明这件事仍然是一次问答;第三个问题答「是」,就意味着必须有状态、版本和账本,界面只是它们的投影。

需要说清楚的是:这三个问题问的是任务的结构,不是任务的规模。同样是写一篇文章,偶尔写一次和每周都要写,答案可能完全不同——差别不在模型,在重复的程度。

第一版只承诺一个闭环

先让用户登录、选择模型、发送请求、查看结果、恢复历史,并理解额度变化。这个闭环通了,再接入图片、文章或面试这样的具体场景。它比同时展示十个无法稳定交付的入口更容易验证价值。

当前工程里,聊天工作台的分工是这样的:/ 表示新会话,/chat?id=xxx 表示打开某个会话,URL 本身就是工作台的状态;侧边栏高亮项由路径反推,刷新后不会跳回默认值。浏览器侧的聊天请求统一走 /api/workspace/chat 代理,草稿、项目和技能这些数据由 createChatClient 与 IndexedDB 存储分别负责。

额度那一侧的反馈要能在界面上被看见。useWorkspaceCreditAccess 读的是 billing.data.balance.available,余额不足时给出「当前可用积分不足,购买权益到账后即可继续对话」的提示,并且只有确认到账的权益才会解除提示,待确认的订单不算。

选首个场景时,记录用户今天怎样完成任务、需要切换几次页面、哪里最容易丢失输入。试用后观察任务完成率、修改次数和返回使用的原因。只有这些数据变好,才有证据继续投入。课程中的商业价值属于需要验证的产品假设,不构成收益保证。

三种要一直付的成本

模型与基础设施成本。 失败重试、媒体保存和后台任务都在花钱。所以额度不能只在成功后扣一次——预占、结算、释放三段式,才知道一次失败的请求到底该不该扣钱。取消也是一次释放:源码里 cancel 只允许在 draft、queued、running、waiting_provider 四个状态下执行,并且同样会 release 已预占的额度。

维护成本。 模型协议、认证行为和依赖版本都会变。当前工程里聊天、文章生成、简历和视频分镜都经由 packages/agent-runtime 这层 Pi 封装调用模型,而共享的提供商适配(DeepSeek、智谱、Anthropic 兼容接口)以及面试、生图仍直接使用 AI SDK——两种运行时并存;课程不会宣称已经完成统一迁移,这类边界要在选型时逐项确认。

信任成本。 用户交给工作台的数据、个人 Key 和作品,需要明确的访问边界和删除路径。对象存储那一侧已经在处理外部地址带来的风险:server/src/features/shared/creative-tasks/creative-asset-storage.ts 在拉取外部资源时会解析主机名并拦截内网地址(127.0.0.1、10.x、链路本地地址等),避免拿用户给的链接把服务端引到内网。这类检查属于信任成本的一部分,不是全部。

因此,这门课把账号隔离、取消请求、任务恢复和计费放进主线,而不是上线之后再追加的附录。读者最终要能解释一次请求从输入到结果再到余额的全过程。

实践产物与验收

这一节要交付两样东西:产品问题陈述和任务价值表。

产品问题陈述

用一句话把问题写清楚,再拆成几个字段。填法如下(示例用的是本文开头的写文章任务,请换成你自己的):

字段填写示例
谁
每周要写 1–2 篇产品文章的独立开发者
在什么场景下
有一篇进行到第三稿的文章,第二天要接着改
想完成什么
改完正文、配好插图,存成能继续改的版本
现在卡在哪
上一次对话找不到了,背景要重贴,旧稿要翻聊天记录
成功的样子
打开就看到上次的稿子和改到第几版,额度变化一眼可见

最后一行是关键。前几行是问题陈述,最后一行是验收标准——它必须能用「打开工作台,看到 X」这种可观察的方式说清楚,否则说明问题还没定义完。

任务价值表

至少填三行,覆盖你实际会做的不同类型的任务。每一行都要能回答:省掉了哪一步,怎么知道省掉了。

任务现在怎么做卡在哪一步工作台接管后怎么验证有改善
写产品文章
聊天窗口生成,手动另存草稿
上下文要重贴,旧稿靠翻记录
草稿与版本留在本地,继续编辑能接着改
一次生成到定稿的页面切换次数下降
迭代一张配图
同一提示词反复发,等结果
生成完找不到上一版,只能重做
结果按作品保存,可对比挑选
同一张图从第一版到定稿的平均生成次数
待填

填的时候有两条硬要求:

  1. 说不出少了哪一步,就不要把它写进产品范围。 这是本文最重要的判断标准。「更稳」「更快」不算,要写成「从 5 次页面切换变成 1 次」这种能被观察到的东西。
  2. 每一行都要配一条反证。 写一行「用户仍然要复制大量上下文」「作品常常找不到」,试用之后它如果还在,说明这一行的方案没成立。

验证记录要包含运行环境、使用的配置版本、命令或操作、期望与实际结果;有外部服务时注明 fixture、沙箱还是真实调用。

源码阅读入口

  • server/src/features/shared/creative-tasks/creative-task.service.ts:任务状态机。create 负责幂等与预占,execute 负责调用、归档与结算,recoverPersistence 只重跑归档,cancel 负责取消与释放。
  • server/src/features/shared/creative-tasks/creative-billing.ts:四条余额命令,以及幂等键的拼法。
  • server/src/features/shared/creative-tasks/creative-asset-storage.ts:对象存储写入,以及拉取外部地址时的内网拦截。
  • frontend/main/src/features/chat-workspace/components/workspace-client.tsx:URL 与工作台状态的映射、个人模型按 userId 分区、浏览器侧 API 与存储的分工。
  • frontend/main/src/features/article-studio/hooks/use-article-workspace.ts:文章草稿的本地状态机(loading/empty/failed/ready)、生成操作的幂等键、生成结果如何合并回草稿。
  • packages/app-kit/src/storage/aiwork-article-studio.ts:草稿、版本快照与云端绑定在 IndexedDB 里的落地方式。

源码路径用于定位当前工程,不是可直接复制的新项目代码。正式教程会按从零实现顺序解释接线,必要时单独标记历史兼容。本文未检查这些业务在生产环境的完整可用性。

官方资料与核验范围

本文引用的源码行为已在基线 c42797a9 上复核:server/src/features/shared/creative-tasks、frontend/main/src/features/chat-workspace、frontend/main/src/features/article-studio 和 packages/app-kit/src/storage 相对上一次调研基线没有变化。

调研日期为 2026-10-25。在线资料可能更新;实现 API 与部署行为以正式写作时固定的版本和实际验证为准。