想象一个很具体的产品请求:用户输入一首歌和一句画面描述,系统先生成脚本,再把它拆成八个镜头; 八个镜头分发到不同的视频模型或 GPU worker,失败的镜头要重试,完成后还要统一调色、配字幕、合成、上传,最后通知用户。 整条链路可能跑二十分钟,也可能因为额度、排队和人工确认拖到几小时。

一、先看一个普通后端会怎样丢掉这支 MV

最直接的实现,是业务 API 收到 HTTP 请求后启动一个 goroutine:先调 LLM,再提交八个镜头,循环查询结果,最后合成。 只要请求和进程都活着,这段代码当然能工作。麻烦发生在它跑到一半的时候。

  • 浏览器刷新后,生成任务也许还在跑,但用户已经不知道该去哪里找它。
  • pod 重启后,当前镜头、重试次数和等待状态都随进程内存消失。
  • 视频服务已经接单、回执却丢失时,后端只看见超时;再次提交可能多付一份钱。

这三个故障表面上分别像前端、进程和网络问题,底层却是同一个缺口:任务进度只存在于这次请求和这个进程里。 多写几个 if err != nil 只能处理函数看得见的错误,不能让一个已经消失的进程继续拥有状态。

二、先把 Temporal 说成人话:任务活着,不要求进程一直活着

一句话定义:Temporal 是一套把长任务的进度记录在进程之外,并让新的执行进程能够接着做的工作流平台。 它不负责生成视频,也不是另一种 Agent 框架;它负责记住“这是哪一个任务、已经完成什么、正在等什么、下一步该做什么”。

先只认识四块积木:

  • 任务卡(Workflow Execution):某一次具体任务的持久身份。这里就是“为用户 42 生成这支 MV”,而不是一段抽象代码。
  • 事实本(Event History):这张任务卡的事实记录。开始了什么、调度了什么、哪一步完成了,都按顺序写进去。
  • 流程规则(Workflow):决定下一步,例如“八个镜头都完成后再合成”。它读历史并作决定,不直接调用外部世界。
  • 真实动作(Activity):调用 LLM、视频服务、对象存储或通知接口,并把结果交回任务卡。

把四块积木拼到同一件事上,可以先写出下面这张教学用任务卡。它不是 Temporal SDK 的请求格式,而是帮助我们在后文始终追踪同一个对象:

任务身份:mv-20260710-42
原始输入:song://summer.wav + “霓虹公路,八个镜头”
流程规则:脚本 -> 镜头并发生成 -> 验收 -> 合成 -> 通知
已经确认:脚本存于 object://mv-42/treatment.json
正在等待:scene-03 的 provider job vid-8831
最终产物:尚未产生
用户可见状态:6 / 8 个镜头完成

原始音频、脚本和视频本体属于对象存储;任务身份和推进事实属于 Temporal;“6 / 8”是业务从 Workflow 状态投影给前端的视图。 这三个位置不能混成一份“大状态”:大文件不适合塞进历史,前端状态也不能反过来成为流程是否完成的权威。

Workflow 和 Activity 代码都由业务部署的 Worker 进程运行。Worker 不是第五份持久状态,而是随时可以被替换的执行者。 可以把它记成一句更朴素的话:Temporal Service 保存任务卡和事实本;Worker 只是临时来干活的人。 durable 不是“一个 goroutine 活得很久”,而是“goroutine 可以消失,任务身份和已完成事实仍然存在”。 这正是 Workflow Execution 官方概念 所描述的持久执行。

任务所有权转移图,从短命 HTTP 连接和进程内存转移到 Workflow ID、Event History 与持久状态
连接只负责发起或观察;Temporal 中的 Workflow Execution 才是长任务的持久身份。

2.1 跟着同一支 MV 从开始跑到结束

  1. 创建任务:业务 API 用稳定的业务 ID 创建 Workflow Execution,然后就可以把 HTTP 响应还给前端。
  2. 记录起点:Temporal 先写下“这支 MV 已开始”,再把第一小段决策工作交给 Worker。
  3. 决定下一步:Worker 运行 Workflow;Workflow 看到还没有脚本,于是安排一个“生成脚本”的 Activity。
  4. 执行真实动作:另一个 Worker 运行 Activity,调用 LLM 或视频 provider;结果返回后被写入历史。
  5. 等待与接力:镜头生成期间不必占住 Worker。回调、计时器或下一次轮询到来时,任意可用 Worker 都能读取历史并接着判断。
  6. 完成与查询:合成结果写入对象存储,通知 Activity 发出消息;浏览器即使重开,也能用同一个业务 ID 查询这次执行。

这六步就是全文主干。后面的 Task Queue、replay、Signal、heartbeat 和幂等键,都是在 Worker 会消失、外部调用会超时之后, 为这条主干补上的保护机制。

阅读契约。 这篇文章始终跟随同一支 AI MV。读完后,应当能用自己的话解释 Temporal 是什么、四块积木分别做什么、 一次任务怎样按顺序推进、Worker 与事实本各能改变什么、代码升级为什么也要照顾 replay, 以及哪一种外部副作用仍然必须由业务做幂等。

证据边界。 Temporal 的平台语义以官方文档和固定源码快照为准;服务内部链路来自 temporalio/temporal。 对 tRPC-Agent-Go 的判断来自 公开源码快照。 外部视频 provider 怎样去重、取消和回调不属于 Temporal 可见内部,本文只讨论它能约束的集成边界。

2.2 三个容易混淆的“前端”和“worker”

主干清楚以后,再消除名字冲突。浏览器前端是用户界面;Temporal 源码里的 Frontend Service 是接收 gRPC API 的集群服务; Temporal Worker 是运行 Workflow 和 Activity 代码的进程。它又不同于 Agent 系统里被称为 worker 的子 Agent。

本文含义它拥有的东西
Client / UI浏览器、App 或业务 API。创建任务、保存业务 ID、展示状态,不拥有执行内存。
Temporal ServiceFrontend、History、Matching 等服务与持久层。执行身份、事件历史、任务队列与计时器。
Temporal Worker轮询 Task Queue、运行用户代码的进程。一次短暂的 Workflow Task 或 Activity attempt。
Agent worker父 Agent 委派出去的角色或后台 run。某段 Agent 语义,不天然拥有 durable execution。

三、一次创建请求怎样变成可恢复的工作流

现在把刚才的任务卡落到实现。创建请求不会把整张教学卡原样发给 Temporal;它先回答三个普通问题: 这是哪个业务任务、它遵循哪套流程规则、哪组 Worker 可以处理它。 SDK 分别用 Workflow ID、Workflow Type、Task Queue 和输入表达这些答案,再调用 StartWorkflowExecution。 Temporal 仓库的 README 也明确指出:这个仓库是 Temporal Server, Workflow、Activity 和 Worker 应通过语言 SDK 编写。它不是 import 后便自动接管 goroutine 的普通库。

Temporal 创建与执行路线图,Client 经过 Frontend、History 和 Persistence 创建事件历史,再由 Matching Task Queue 分发给 Worker
权威状态在 History/Persistence;Task Queue 只负责把下一小段工作交给可替换的 Worker。

3.1 Frontend 接受的是创建执行,不是占住连接

服务入口 WorkflowHandler.StartWorkflowExecution 会准备请求、解析 namespace,然后把创建请求交给 History Service。源码注释直接说明它会创建 WorkflowExecutionStarted 事件并调度第一个 Workflow Task。 所以 Start 成功返回后,浏览器继续连着并不是执行成立的条件。

3.2 History 先落记录,再让 Worker 推进

History Service 为每个 Workflow Execution 维护 mutable state 和追加式 Event History。 仓库的架构说明把核心设计写得很直白:每次执行都有 append-only history,完整状态可通过 replay 重建; Workflow 代码必须确定且无副作用,Activity 则需要幂等或明确不重试。 参见 docs/architecture/README.md

下面把同一支 MV 多走几步。字段和不影响这段解释的中间事件经过简化,但参与者、Command 与已接受事件的顺序是关键:

Client -> Temporal Service
  StartWorkflowExecution(workflow_id=mv-20260710-42, input_ref=object://mv-42/input.json)
Temporal Service -> Event History
  WorkflowExecutionStarted

Workflow Worker -> Temporal Service
  Command: ScheduleActivity(write-treatment)
Temporal Service -> Event History
  ActivityTaskScheduled
Activity Worker -> Temporal Service
  ActivityTaskCompleted(result_ref=object://mv-42/treatment.json)

Workflow Worker -> Temporal Service
  Command: ScheduleActivity(submit-scene-03)
Activity Worker -> Temporal Service
  ActivityTaskCompleted(provider_job_id=vid-8831)
Webhook API -> Temporal Service
  WorkflowExecutionSignaled(scene-03-ready, artifact_ref=object://mv-42/scene-03.mp4)

Worker 从 Task Queue 取走 Workflow Task,重放 Workflow 函数,直到它遇到需要等待的 Activity、Timer、Signal 或 Child Workflow, 再把“接下来应该做什么”的 Commands 交回服务端。Matching 的 PollWorkflowTaskQueue 展示的正是 worker 轮询和取任务的服务边界。

这里最容易漏掉的区别是:Worker 提议 Command,Temporal Service 接受状态转换并追加事实。 Worker 崩溃时,尚未被服务端接受的提议可以消失;已经进入 Event History 的事实则成为下一位 Worker 的输入。

参与者它能读到什么它能改变什么它不能冒充什么
Client / 业务 API业务输入、Workflow ID、查询结果。请求创建、Signal、Update 或取消。不能直接改写 Event History。
Workflow Worker历史投影出的 Workflow 状态。返回 Timer、Activity、Child Workflow 等 Commands。不能把本地内存当权威状态。
Activity Worker一次 Activity 的输入和 heartbeat details。执行副作用,返回 result、failure 或 heartbeat。不能宣称整个 Workflow 已完成。
Temporal Service已持久化的执行状态、队列与计时器。接受事件、调度 Task、驱动恢复。不替业务生成视频或判断内容是否合格。

四、Workflow 管控制流,Activity 承担真实世界

为什么不能把“生成镜头、上传文件、发通知”全写在 Workflow 函数里?因为恢复依赖 replay。 同一段 Workflow 代码可能运行多次,它必须在相同历史下产生相同 Commands;读取当前时间、随机数、直接调用视频 API,都会让重放路径漂移。 Temporal SDK 因此把非确定性和副作用推出 Workflow,放进 Activity。

Workflow 与 Activity 边界图,Workflow 只保存确定性控制流,Activity 执行 LLM、视频 Provider、GPU、对象存储和通知等副作用
Workflow 决定“何时、并发多少、失败后怎么办”;Activity 执行“真正去调用谁”。

4.1 一支 MV 的边界应该怎样切

步骤建议边界为什么
拆脚本、决定镜头列表LLM Activity 返回结构化镜头表。LLM 非确定,结果记录后 replay 不再重复调用。
提交每个镜头每个镜头一个 Activity 或 Child Workflow。独立重试、限流、取消和观察。
等待外部生成Signal/webhook、异步完成或轮询 Activity。等待期间不占用 worker 线程。
合成与上传长 Activity,产物写对象存储。大视频不进入 Event History,只保存 URI 与元数据。
通知用户独立幂等 Activity。合成重试不应重复发送通知。

Temporal 官方同样把 media transcoding、LLM call、large download 列为 Activity 的代表场景,并建议把大功能拆成多个 Activity, 以获得更小的失败恢复范围、更清楚的 timeout 和更容易实现的幂等性。 参见 Temporal Activities

4.2 等待不是让 goroutine 睡着

视频 provider 返回 job ID 后,Workflow 可以等待 Signal、Timer 或 Activity Future。等待状态已经在服务端可重建, 没有 worker 时它也不需要占住一个业务线程。外部 webhook 到达后,业务 API 把结果作为 Signal 写入 Workflow; 用户要求换风格或取消时,也可以通过 Signal、Update 或 cancellation request 改变后续路径。 Query、Signal、Update 的读写语义可见 Workflow Message Passing

五、恢复发生在“最后一个已记录事实”之后

Event History 不保存 worker 的堆栈,而是保存已经被服务端接受的状态转换。 新 worker 取得 Workflow Task 后,从函数开头 replay;SDK 把历史中已有的 Activity 结果、Timer 和 Signal 重新喂给代码, 并检查它产生的 Commands 是否与历史一致。走到第一个尚未记录的动作时,才真正继续执行。

Temporal replay 恢复图,Worker A 崩溃后 Worker B 读取 Event History,跳过已完成步骤并从第一个未记录动作继续
replay 会重新运行与旧历史兼容的 Workflow 代码,但不会重新执行历史中已经完成并记录结果的 Activity。

5.1 浏览器断开、Workflow Worker 崩溃、Activity Worker 崩溃并不相同

故障Temporal 看见什么恢复方式
浏览器/SSE 断开Workflow 没有变化。前端之后用业务 ID 重新查询或订阅。
Workflow Worker 崩溃Workflow Task 未完成或超时。另一 worker replay history,再生成 Commands。
Activity Worker 崩溃Activity 没有完成回执,最终触发 timeout。按 Retry Policy 重新调度 Activity attempt。
Temporal Service 短暂不可用已提交历史仍在持久层。服务恢复后继续派发;实际保障取决于集群与存储部署。

对长 Activity,heartbeat 同时承担存活检测、取消传递和应用层进度 checkpoint。 例如合成 8000 帧时,可以把最近完成的分片写入 heartbeat details;worker 失联后,下一次 attempt 读取这份进度继续。 官方文档说明 Start-To-Close timeout 用于发现 worker crash,heartbeat payload 可交给下一次 attempt: Detecting Activity Failures。 服务端对应入口也把 heartbeat 的两个用途写在注释里:报告 liveness 与 progress,见 RecordActivityTaskHeartbeat

5.2 进程可以换,旧执行依赖的 Workflow 语义不能突然换

“任意 Worker 都能接手”还有一个前提:它运行的 Workflow 代码必须能解释已有历史。 假设旧历史记录的是“镜头完成后直接合成”,新版本却在同一位置无条件插入“先运行内容审核 Activity”; 旧执行 replay 时,新代码产生的 Command 就可能和历史不一致。进程替换解决的是机器故障,不自动解决不兼容的代码升级。

旧执行的历史:scene-ready -> compose
不安全的直接替换:scene-ready -> moderate -> compose

安全发布要回答:
1. 旧执行继续由兼容代码处理,还是通过版本路由固定在旧 Worker?
2. 新分支是否通过 SDK 的 patch / version 机制显式加入?
3. 发布前能否用真实 Event History 做 replay test?
4. 新版本失败时,旧执行和新执行分别怎样回滚?

Temporal 当前的 Worker Deployments 指南 把 Worker Versioning 作为安全发布新 Workflow 代码的推荐默认方式,并把 patching 作为无法使用版本化部署时的兼容手段。 对持续几十分钟或数小时的 AI 任务,这不是运维附注,而是 durable execution 能否在多次发布后仍可重放的一部分。

六、最危险的缝:外部服务已经成功,回执却丢了

到这里很容易产生一个误读:“用了 Temporal,每一步就 exactly once。”事实不是这样。 Workflow 控制逻辑能通过 history 获得 effectively-once 的执行效果;Activity 与外部系统之间仍然存在经典的不确定窗口。

  1. submit_scene(scene-03) 已让视频 provider 创建付费任务。
  2. provider 返回 job ID 之前,worker 或网络断开。
  3. Temporal 只知道 Activity 没有完成,于是按策略重试。
  4. 第二次请求若没有同一幂等身份,provider 可能再创建一个任务。
Activity 幂等窗口图,外部 Provider 已接受任务但响应丢失,Temporal 重试时通过稳定 idempotency key 找回同一个 provider job
Temporal 能可靠地决定“要不要再尝试”,但外部副作用是否重复仍取决于 provider contract 与业务幂等键。

6.1 幂等键必须和逻辑动作绑定

一个实用做法是用 workflowId + sceneId + operation 生成稳定幂等键,并让 provider 或自己的提交表以它去重。 provider job ID 返回后立即作为 Activity result 持久化;后续等待和查询围绕 job ID 进行。 如果 provider 没有幂等 API,就需要在自己的数据库里建立 submission ledger,并接受“调用成功但本地未记录”无法被纯客户端彻底消除的边界。

{
  "workflow_id": "mv-20260710-42",
  "scene_id": "scene-03",
  "operation": "submit-video",
  "idempotency_key": "mv-20260710-42:scene-03:submit-video"
}

6.2 取消也是协作协议,不是远程 kill

Workflow 收到取消请求后,可以取消还未开始的步骤,并向正在运行的 Activity 传递 cancellation。 但 Activity 需要 heartbeat 或主动检查 context 才能及时收到;外部 provider 还必须提供 cancel API,业务 Activity 才能真正终止远端 GPU 任务。 因此“Temporal 中显示 Canceled”与“所有外部算力立刻停止”不能画等号。

6.3 Provider 说完成、Workflow 结束、产品可交付是三个状态

另一个常见误读是把外部 provider 的 succeeded 直接当成产品完成。 回调只能证明 provider 声称任务结束;业务还要确认对象可读、格式与时长正确、镜头清单完整、取消没有抢先发生,并把通过验收的产物引用写回 Workflow。 Workflow 完成以后,产品层才应把这支 MV 标成可下载或可发布,并决定何时通知用户。

provider produced
-> Activity verifies artifact and cost
-> Temporal records artifact_ref
-> Workflow satisfies all completion conditions
-> product marks the MV deliverable
-> notification is sent idempotently

任何一道验收门失败
-> 不宣称可交付
-> 重试、补偿或进入人工处理

这样就把“外部产出了文件”“流程有了完成证据”“产品允许交付”分开了。 Temporal 能保存并恢复这条验收链,但视频质量标准、预算上限和最终发布权限仍由业务定义。

七、tRPC-Agent-Go 在 worker 内搭流程,Temporal 管 worker 外的生命

对 Agent 框架而言,Temporal 不是另一套 GraphAgent。两者回答的问题不同:tRPC-Agent-Go 负责模型、工具、子 Agent、事件流和图节点怎样执行; Temporal 负责一次长任务的身份、状态、重试、计时、消息和恢复怎样越过进程边界。

tRPC-Agent-Go 与 Temporal 分层图,Temporal 保存 durable workflow、queue 和 recovery,tRPC-Agent-Go 在可替换 worker 内执行 Agent graph、model 和 tools
更自然的组合是让 Temporal 做 durable control plane,让 tRPC-Agent-Go 继续做 Agent runtime。

7.1 tRPC-Agent-Go 已经有哪些接近的积木

先界定固定源码快照里的真实状态:agent/taskrun 定义了可替换的 Controller 接口, 仓库随附并被示例采用的是 inprocess.Service;多节点 durable controller 是留给产品实现的扩展面, 不是“文件已经存在所以主路径已经分布式”。Graph checkpoint 与 detached cancel 已实现,但各自只覆盖下面表中的局部边界。

能力快照中的状态当前能解决什么还没有自动解决什么
WithDetachedCancel已实现的 RunOption。父请求 cancel 后,当前进程里的 run 可以继续。进程重启和多节点恢复。
Dynamic Workflow已实现,文档明确是第一版前台一次性执行。临时 Python 代码编排多个 Agent。不保存可跨进程恢复的执行状态。
Graph checkpoint已实现的 checkpoint / resume 机制。保存 state、frontier,支持显式 resume 和 time travel。谁发现 owner 死亡、谁抢占并恢复、外部 Activity 语义。
taskrun.Controller接口已实现;随附 controller 是 in-process。run ID、status、wait、cancel、child session 的控制面。分布式存储、队列、lease 与跨节点接管由产品实现。

这些边界都能从源码直接验证。WithDetachedCancel 只改变 parent context 的取消传播;Dynamic Workflow 文档称第一版为 “前台、一次性执行”; Graph 文档把 checkpoint 描述为 显式恢复与 time travel 的基础; taskrun 文档要求多节点实现基于 外部存储、队列、lease 与跨节点取消; 内置 FileStore 加载到未结束 run 时, normalizeLoadedRuns 会把它标记为被前一次 runtime restart 中断,而不是继续执行。

7.2 两种接法的取舍

粗粒度接法是把一次完整 runner.Run 作为 Activity。改造小,但 worker 在 Agent 运行中途崩溃时,整个 invocation 可能重试, LLM 与工具副作用需要更强幂等。细粒度接法则把 LLM call、tool call、视频提交和人工等待分别变成 Activity 或 Child Workflow; 恢复点和可观测性更好,但会把 Agent loop 改造成 durable state machine,接入成本明显更高。

把同一个 scene-03 放进两种方案,重复边界才会清楚。下面是集成合约,不是当前 tRPC-Agent-Go 已内置的 Temporal adapter:

接法代表性持久单位Temporal 记录什么Worker 中途崩溃后
粗粒度整个 Agent run:run ID、输入引用、幂等作用域。一次 Activity 的 scheduled / completed,以及最终产物引用。最后结果未落历史时,整个 runner.Run attempt 可能重做。
细粒度一个逻辑步骤:scene ID、tool/model 操作、稳定副作用键。每个步骤的 result reference、等待点和完成事实。只从第一个未记录步骤继续,但 Agent loop 必须显式建模成可重放状态机。

因此更稳妥的产品边界不是让 Agent 框架重造 Temporal,而是保留一个可替换的 durable controller 接口: 简单部署使用进程内 taskrun;需要跨节点恢复时接 Temporal、云状态机或业务自己的调度平台。

八、业界没有唯一答案,先按恢复压力选层级

Temporal 是成熟的 durable execution 平台,源自 Uber Cadence;但“长任务”并不自动意味着必须使用 Temporal。 如果流程只有一个可幂等的异步视频 API,数据库任务表加消息队列可能已经足够;如果主要压力是 Kubernetes GPU DAG,Argo Workflows 更贴近算力调度; 如果团队完全运行在 AWS,Step Functions 能减少自建控制面的运维。

方案更适合的压力主要代价
Temporal / Cadence代码式长流程、Signal/HITL、复杂恢复、跨服务编排。确定性约束、worker 版本治理、集群或 Cloud 成本。
Restate / DBOS / Inngest希望以更轻的服务或 Postgres/step 模型获得 durable execution。生态、语言与部署模型各不相同,需要按边界验证。
AWS Step Functions / Azure Durable Functions深度绑定云服务,优先托管运维。云绑定、状态机与费用模型。
Argo / Airflow / Prefect / DagsterGPU、数据、媒体、ML 批处理 DAG 与调度。交互式消息和应用级长事务不是它们共同的强项。
Queue + DB流程短、状态少、团队能自己维护重试与补偿。取消、超时、幂等、可观测和孤儿恢复都要自己补。

可直接对照的官方资料包括 RestateDBOSInngestAWS Step FunctionsAzure Durable Functions。 它们共同承认的问题是状态、checkpoint、retry 和 recovery;差别在编程模型、部署面与谁来托管执行引擎。

九、把这支 AI MV 压缩成几条可迁移规则

如果状态属于应放在哪里保护的 invariant
浏览器展示业务 API、可重连事件流、查询接口。断线不改变执行状态。
流程控制Workflow state 与 Event History。进程死亡后可以重建下一步。
外部副作用Activity + 幂等键 + provider job ledger。重试不会静默制造重复成本。
长步骤进度Heartbeat checkpoint 或外部进度表。失败恢复不必从零开始。
视频与音频产物对象存储,只在 history 保存引用。工作流历史保持小而可重放。
Workflow 代码版本Worker Deployment / patch 路由与 replay tests。发布后旧执行仍能解释自己的历史。
产品可交付状态业务验收记录与发布权限。provider 成功不被误当成用户结果已经可用。
Agent 节点内部状态tRPC-Agent-Go session/graph/checkpoint。Agent runtime 与 durable control plane 各守边界。

所以,“在 worker 里面搭流程”和“让流程活过 worker”不是同一件事。 前者决定一轮活着的时候怎样调用模型、工具和子 Agent;后者决定这轮在承载它的进程消失后,谁还能证明它已经做过什么、下一步该做什么。 Temporal 的价值正是在这里:它把流程从一段短命控制流,变成一个由持久历史驱动、可被新 worker 接手的执行身份。

但真正可靠的 AI 视频系统仍然是组合题:durable orchestrator 管业务生命,GPU/Kubernetes 平台管算力, 对象存储管大产物,Agent runtime 管推理,provider contract 和幂等账本管外部副作用。 只有这些所有者被分清,“前端挂了,后端还在跑”才会从一句模糊需求变成一套能验证的工程语义。

参考资料