工作流不存 Token:流式 AI 如何既实时又可靠
巡检分析、报告生成这类任务,既要边生成边展示,又要失败可重试、结果可审计。若把每个流式 token 都写进工作流历史,体积与重放成本都会爆炸。更干净的切法是:工作流管可靠结果,SSE 管实时观感。
一、两种需求冲突
| 需求 | 满足方式 | 冲突点 |
|---|---|---|
| 实时展示 | SSE 逐 token 推送 | token 不该进工作流历史 |
| 可靠重试 | 工作流记录状态 | 状态不该包含海量 token |
| 结果审计 | 落库 + 对象存储 | 与 SSE 无直接关系 |
二、边界划分
flowchart TB
U[用户发起分析] --> API[业务 API]
API --> WF[工作流: 建任务/调模型/落库]
API --> SSE[SSE: 订阅进度与增量文本]
WF --> DB[(结果表/对象存储)]
WF -.进度事件.-> Bus[消息/缓存]
Bus --> SSE
SSE --> U
U -->|断线重连| SSE
SSE -->|Last-Event-ID| Bus
读图: 工作流只负责可靠落库;SSE 从总线读增量。两条链路在「进度事件 + 最终结果」汇合,而不是共用同一份 token 历史。
原则:
- 工作流历史:输入引用、模型版本、最终产物 ID、错误码
- 不进历史:逐 token 文本、临时调试日志
- SSE:从缓存/消息总线读增量;结束帧以「落库成功」为准
三、为什么不把 Token 存进工作流
flowchart TB
A[把 token 存进历史] --> B[历史膨胀 GB 级]
B --> C[replay 变慢]
B --> D[存储成本飙升]
A --> E[重试时重复 token 难去重]
A --> F[敏感内容扩大存储面]
F --> G[合规风险]
正确做法:把「完整答案」作为 Activity 输出一次写入 DB;SSE 在 done 前允许丢包,刷新后以 DB 为准。
四、工作流内部结构
sequenceDiagram
participant W as Workflow
participant A as AI Activity
participant M as Model
participant DB as 结果库
W->>A: executeAnalysis(inputRef)
A->>M: 调用模型(幂等键)
M-->>A: 逐 token 返回
A->>A: 拼接完整结果
A->>DB: 写入最终结果
A-->>W: 返回 resultId
W->>W: 标记 done
W->>W: 发进度事件到总线
五、幂等与防重复
flowchart TB
A[重复点击分析] --> B{WorkflowId 已存在?}
B -->|是| C[返回已有运行中实例]
B -->|否| D[创建新工作流]
D --> E[WorkflowId = analysis:bizId:inputHash]
E --> F[Activity 带幂等键调模型]
F --> G[避免超时重试双计费]
- WorkflowId:
analysis:{bizId}:{inputHash} - 同一业务重复点击,返回已有运行中实例
- Activity 调模型时带幂等键,避免超时重试双计费
六、前端体验流程
stateDiagram-v2
[*] --> 发起
发起 --> 等待runId
等待runId --> 流式接收
流式接收 --> 拼接delta
拼接delta --> 收到done
收到done --> 拉最终结果
拉最终结果 --> 校准显示
流式接收 --> 断线
断线 --> 重连: 带Last-Event-ID
重连 --> 流式接收
校准显示 --> [*]
- 先建任务拿
runId - 立刻开 SSE
- 本地拼接 delta;收到
done再拉一次最终结果校准 - 断线重连从
Last-Event-ID或服务端游标续传
七、done 前后以谁为准
流式过程中允许「观感不完整」;一旦落库成功,以 DB 为真相源。下面这张决策图把边界钉死:
flowchart TB
A[前端收到帧] --> B{事件类型?}
B -->|delta| C[本地拼接展示<br/>可丢 可重连补]
B -->|done| D[拉最终结果 API]
D --> E[用 DB 内容覆盖本地缓冲]
B -->|error| F[展示失败态<br/>引导重试工作流]
实践上:delta 阶段不必强一致;done 之后必须校准一次。否则用户刷新页面会看到「半截流」和「已入库全文」两套真相。
八、Activity 伪代码
1 | async function executeAnalysis(input: AnalysisInput): Promise<string> { |
九、小结
「流式」满足人,「工作流」满足系统。两者交汇点应是 进度事件 + 最终落库,而不是把 token 流塞进工作流引擎。这是巡检 AI、报告生成类功能长期可运维的关键分界。