把重复任务变成可复用流程
已有聊天产品,为什么还需要一个工作台? 产品问题陈述与任务价值表。
内容状态:本文的判断依据、源码引用与状态机已逐条对照当前仓库(基线
c42797a9)复核。完整步骤教程、练习代码和真实用户实验尚未交付;文中所有商业价值判断都按产品假设处理,不构成收益保证。
你用 AI 写完一篇文章的第一稿,第二天回来接着改,发现上一轮对话找不到了。
你记不清自己当初有没有交代过「读者是转 AI Agent 的前端工程师」,也想不起昨天那几次生成到底扣了多少额度,于是不太敢再点一次生成。
这不是模型不够聪明。模型给你写完了一段文字,缺的是围着这段文字的一圈东西:这次任务进行到哪一步、产出存在哪、失败之后从哪继续、这些调用花了谁的钱。聊天窗口一次只管一件事——把你这句话变成那段文字,没有地方放这些。
工作台补的就是这一圈。这一章要你交付的,是一份能讲清楚「工作台到底省掉了哪一步」的产品问题陈述和任务价值表。
如果还没确定为什么投入这个方向,可以先阅读 为什么学 Agent 开发。本章从具体任务出发,把学习动机进一步落到产品问题上。
先把两种工作单元分开
「用 AI 写文章」和「跟 AI 聊两句」经常被混成同一件事,因为它们都发生在对话框里。但它们的工作单元并不一样:
差别全在最后一行上。
一个任务要在「模型输出完」之后继续活着,就得有四样东西撑住,聊天窗口一样都不提供:
- 状态:现在进行到哪一步;
- 归属:产出存在哪、属于谁、是第几版;
- 恢复:中断或失败之后从哪继续;
- 账本:这次消耗了多少、算在谁头上。
这四样都不需要更聪明的模型,但都需要服务端留一份记录。这条线就是「聊天产品」和「工作台」之间比较硬的分界。
一次创作任务如何影响余额,可以用一个状态机看清楚。下面这张图对应 server/src/features/shared/creative-tasks/creative-task.service.ts 的实现:
从「重跑归档」出发、再回到「完成」的那条路径值得单独说。
模型已经出结果了,但把结果落到存储这一步失败了。这时任务停在 persisting,output 里保留着 phase: "provider_completed" 和已经生成的原始输出。recoverPersistence 拿这份输出只重跑归档,成功之后才结算——不会重新调用模型,也不会重新扣一次钱。
同样的事如果发生在聊天窗口里,这条路径根本不存在:调用的钱已经花了,结果只留在浏览器内存里,刷新就丢,而账单上已经扣过。
余额那一侧是同一套逻辑。reserve 先冻结预估量,拿到结果后按实际用量 settle,失败或取消就 release。每条命令都带一个幂等键(代码里还预留了 reverse 用于冲正):
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、链路本地地址等),避免拿用户给的链接把服务端引到内网。这类检查属于信任成本的一部分,不是全部。
因此,这门课把账号隔离、取消请求、任务恢复和计费放进主线,而不是上线之后再追加的附录。读者最终要能解释一次请求从输入到结果再到余额的全过程。
实践产物与验收
这一节要交付两样东西:产品问题陈述和任务价值表。
产品问题陈述
用一句话把问题写清楚,再拆成几个字段。填法如下(示例用的是本文开头的写文章任务,请换成你自己的):
最后一行是关键。前几行是问题陈述,最后一行是验收标准——它必须能用「打开工作台,看到 X」这种可观察的方式说清楚,否则说明问题还没定义完。
任务价值表
至少填三行,覆盖你实际会做的不同类型的任务。每一行都要能回答:省掉了哪一步,怎么知道省掉了。
填的时候有两条硬要求:
- 说不出少了哪一步,就不要把它写进产品范围。 这是本文最重要的判断标准。「更稳」「更快」不算,要写成「从 5 次页面切换变成 1 次」这种能被观察到的东西。
- 每一行都要配一条反证。 写一行「用户仍然要复制大量上下文」「作品常常找不到」,试用之后它如果还在,说明这一行的方案没成立。
验证记录要包含运行环境、使用的配置版本、命令或操作、期望与实际结果;有外部服务时注明 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 里的落地方式。
源码路径用于定位当前工程,不是可直接复制的新项目代码。正式教程会按从零实现顺序解释接线,必要时单独标记历史兼容。本文未检查这些业务在生产环境的完整可用性。
官方资料与核验范围
- OWASP Prompt Injection Prevention:用于信任成本那部分的威胁建模;安全章节会区分当前实现和教学补齐。
- PostgreSQL 文档:账本涉及事务与约束,部署版本在正式教程中另行固定。
本文引用的源码行为已在基线 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 与部署行为以正式写作时固定的版本和实际验证为准。