真正让我们反复思考的问题,并不是“怎样保存工作流版本”。
而是:一个请求正在队列里等待,此时有人修改了工作流,那么 Worker 启动后究竟该运行哪一份配置?
这个问题看起来很小,顺着它往下追,却会碰到提示词、模型、备用 provider、输出 schema、缓存、session、后端回调,以及一个工作流所调用的其他工作流。
最后我们得到一个很朴素的结论:AI 工作流也需要部署边界。保存编辑和发布到生产环境,不能是同一个动作。
这篇文章是一次工程案例复盘,而不是学术研究,也不会声称“配置不可变”就能让大模型输出完全确定。我们想记录的是:试图消除哪些故障模式、选择了哪些系统约束,以及这种设计仍然要付出什么成本。
单条可变记录带来的陷阱
最简单的工作流系统只需要一条数据库记录:编辑器写它,生产环境读它。实现起来非常轻松。
但这也意味着,无论界面怎样命名,Save 实际上都是一个部署按钮。
假设一个团队在同一个下午修改了系统指令、更新了 JSON schema,又替换了备用模型。如果生产环境直接读取可变记录,这些字段可能在不同时间到达用户。事后,请求日志也许能告诉你运行了哪个模型,却不一定能还原请求开始时那一整套可执行配置。
当 order_review 调用 fraud_check 时,问题会更明显。即使没人编辑 order_review,只要 fraud_check 发生变化,前者的行为也可能改变。此时移动的已经不是一条记录,而是一整张依赖图。
我们也考虑过一些常见捷径:增加 Draft/Live 两组字段、每次保存都复制工作流,或者依赖审计日志重建旧状态。它们都能解决一部分问题,却很难同时清楚回答:
- 生产环境现在运行哪一份配置?
- 某一个具体请求开始时选中了哪一份配置?
五条约束决定了设计方向
在设计界面之前,我们先写下系统必须守住的行为。
- 编辑 Draft 不能改变 Live。 保存下来的实验仍然只是实验。
- 发布必须明确、可审核。 Live 移动前,要看到行为差异、依赖变化和运行风险提示。
- 已发布版本必须不可变。 如果过去还能被悄悄改写,版本历史就失去了意义。
- 一个请求只能有一套执行上下文。 排队和回调不能让它在半途中换配置。
- 恢复操作不能破坏历史。 让 Live 回到旧版本,与把旧内容带回 Draft,是两件不同的事。
因此,最终模型由可变 Draft、不可变 Revision 和一个 Live 指针组成。
应用仍然使用稳定的工作流名称。生产请求通过名称找到工作流,沿着 Live 指针读取对应快照。编辑 Draft 不会移动指针;发布和回滚才会。
一个版本究竟保存什么
Revision 保存的是工作流的可执行行为,不是编辑器截图,也不是每一个描述性字段的机械副本。
其中包括 provider 与模型链、请求类型、系统指令、客户字段、结构化输出 schema 与示例、缓存和 session 设置、预算控制、流水线步骤,以及被固定的回调依赖。若 schema 和示例没有变化,结构的数据库 ID 或显示名称不会被当作行为变化。
我们保留了两种相关但不同的比较方式,因为“工作流变了吗”其实可能在问两件事:
- 内容指纹(content fingerprint) 判断工作流自身的可执行内容有没有变化。
- 快照 hash(snapshot hash) 还会包含回调目标所固定的准确 Live 版本。
因此,控制台可以分别指出 Draft 未发布的原因:自身内容变了,或者某个依赖发布了新的 Live 版本。

从编辑到生产的完整生命周期
1. 编辑并保存 Draft
Draft 是可变的工作区。团队可以调整提示词、provider 顺序、输出契约、缓存窗口、session 行为、预算或后端流水线,而不改变 Live 流量。
界面里还有一个很小但很重要的规则:表单存在未保存修改时,按钮显示 Save draft。只有先保存、让系统能够拿 Draft 与 Live 比较之后,具备权限的用户才会看到此时真正相关的 Publish 操作。
2. 测试你真正想测试的配置
Playground 可以运行 Auto、Draft、Live 或某个历史版本。存在未发布修改时,Auto 选择 Draft;否则选择 Live。
异步工作流让 Draft 测试变得更微妙。排队任务不能在稍后读取“那一刻的 Draft”。异步 Playground 请求被接收时,系统会保存一个短期执行快照,并把快照 ID 交给任务。只要仍有活跃任务或待处理回调引用它,快照就会保留;引用消失后才进入清理范围。
Live 异步请求携带的则是已发布 Revision ID。两种情况都保证:请求开始时所做的选择,不会在等待执行期间悄悄改变。

3. 查看语义差异
数据库字段 diff 往往噪声很多。真正有用的发布差异应该回答:主模型变了吗?系统指令改了什么?输出契约是否移动?是否增加了流水线步骤?回调目标是否发布了新版本?
因此,发布界面比较的是规范化快照,并同时提供 Workflow 与 JSON 两种视图。Workflow 视图适合观察流水线结构;需要核对准确字段时,仍然可以切到 JSON。

4. 验证并发布
发布是一个事务,而不是几次“尽力而为”的写入。系统会在锁内重新读取工作流,检查回调图是否存在环,并确认每个目标仍处于 active 状态且拥有可用的 Live 版本。这样可以缩小“预览发布”与“真正提交”之间被并发修改的空隙。
发布审核还会显示架构图里容易被忽略的现实问题:
- 回调目标已经发布了更新的 Live 版本;
- 已存在的响应缓存可能继续被命中,直到过期;
- Session memory 可能跨越一次发布继续存在;
- 某个依赖 pin 可能有意停留在较旧版本。
验证通过后,系统会创建——或复用——具有相同快照 hash 的不可变 Revision,记录发布事件与可选备注,再移动 Live 指针。发布完全相同的内容不会凭空制造一条“新历史”。

为什么依赖也必须固定版本
假设 Live 的 order_review R7 调用 fraud_check R3。第二天,fraud_check 发布了 R4。
order_review 是否应该自动开始使用 R4?我们的答案是否定的。R7 继续固定 fraud_check R3。即使目标工作流已经向前移动,这张已发布依赖图仍然可以被准确解释。
此时,父工作流 Draft 会提示依赖的 Live 版本已经移动。再次发布父工作流,才代表明确选择采用新的依赖。相比“永远使用 latest”,这确实多了一步,却消除了一个很令人挫败的故障来源:明明没人碰这个工作流,它的行为却变了。
还有一个实际细节:停用子工作流后,新发布不能再固定它,但已经引用它的历史父版本仍然可用。否则,删除当前对象会反过来破坏旧版本的含义。
异步任务和回调必须继承同一个 pin
长时间运行的任务最容易暴露版本设计里的漏洞:
如果每个阶段都按名称重新查找工作流,一次发布就可能把同一个逻辑请求切成多个版本:最初的模型调用使用 R4,后面的回调步骤却使用 R5。
为避免这种情况,Live 任务携带 Revision ID,Draft 任务携带执行快照 ID,等待中的回调 channel 继续传递同一份上下文。这里的承诺很克制,但很重要:同一个请求不会在执行中途悄悄换掉工作流配置。
这仍然不能保证模型输出可重复。Provider 基础设施、底层模型更新、采样和外部工具都可能变化。能够被固定的是我们这一侧的契约:请求开始时选中的可执行配置。
Rollback 与 Restore 被刻意分开
两者都从旧版本开始,因此很容易混淆。
Rollback 改变 Live。 它把指针移到已有 Revision,让新的生产请求使用那份已知配置。它不会删除较新的发布,也不会顺便发布当前 Draft。
Restore 改变 Draft。 它把历史快照复制回可编辑工作区,Live 不动。团队可以修改恢复后的内容、测试、比较,再有意识地发布。
结构化输出让 Restore 比“复制一个外键”复杂。如果原结构仍属于同一项目,而且 schema 与示例没有变化,就可以直接复用;如果结构已被修改或删除,Restore 会创建一个具有唯一名称的新结构。这样,历史契约可以重新编辑,而不会覆盖当前其他对象。
这个细节并不吸引眼球,却决定了半年之后人们是否还敢相信“恢复”按钮。
这套设计的代价
发布边界消除了歧义,但不会消除复杂度。
- 产品需要解释 Draft、Live、历史 Revision 和短期执行快照。
- 快照清理必须理解排队任务和待处理回调,而不能只看时间戳。
- 依赖 pin 用主动发布换掉了自动采用 latest 的便利。
- 缓存和 session 可能跨越一次发布,因此界面必须诚实提醒,而不能暗示所有状态会瞬间切换。
- 权限也必须区分:Publish 与 Rollback 影响生产,Restore Draft 则属于编辑操作。
这些成本都是真实的。个人原型使用单条可变记录可能完全合理。当多人编辑工作流、请求异步执行、工作流相互调用,或者有人需要准确解释昨天的生产行为时,发布模型才开始显出价值。
一份可用于任何 AI 工作流平台的审计清单
实现方式各不相同,但下面的问题具有普遍性:
- 保存配置是否会立即影响生产?
- Live 是否是指向不可变内容的持久指针?
- 嵌套工作流会固定版本,还是在执行时解析 latest?
- 发布发生在排队之后、执行之前时,任务会怎样处理?
- 回调是否继承请求最初的执行上下文?
- Diff 能否区分本地编辑与依赖移动?
- 发布前后,缓存和 session 的语义是否足够诚实?
- 运维人员能否回滚 Live,同时保留完整历史?
- 编辑者能否恢复旧内容,又不改变 Live?
最初那个问题——队列里的请求究竟该运行什么——后来成了很有用的设计测试。如果一个系统无法精确回答它,那么发布边界大概率还不完整。
对我们来说,答案不再是“Worker 醒来时工作流碰巧长什么样”,而是请求开始时选中的 Revision,或 Draft 执行快照。它带来的不是“生产永远不会出意外”这种虚假的安全感,而是一种更安静的踏实:至少,配置不必再成为事故里的谜团。
更具体的实现说明可以查看工作流发布文档。我们也很想知道其他团队如何处理依赖发布和长时间运行的 AI 任务——看似简单的版本模型,往往会在这两个地方暴露它真正的假设。

