03 - Temporal 与确定性重放
前置:01 篇 4.1 节的两种恢复模型。
本篇回答:为什么这类框架禁止在编排代码里用随机数和系统时间、这个约束的边界到底在哪、以及 Agent 代码该怎么组织才不会 被它拖住发布节奏。
术语
- Workflow(工作流):编排逻辑,必须确定性
- Activity(活动):实际执行副作用的函数,允许任意不确定操作
- Event History(事件历史):服务端记录的该工作流发生过的全部事件
- Command(命令):工作流代码执行时向服务端发出的意图,例如"启动一个 Activity"
一、它存的是事件,不是状态
02 篇里 LangGraph 存状态快照。Temporal 存事件历史。
"从第一行重跑"是理解这套机制的关键,它是后面所有约束的来源。
服务端在重放时逐条比对:
When the Workflow's code replays, the Commands that are emitted are compared with the existing Event History. If a corresponding Event already exists within the Event History that matches that command, then the Execution progresses.
二、确定性约束
代码要重跑并走出完全相同的路径,任何导致路径分叉的操作都被禁止。
2.1 禁止清单
| 操作 | 分叉原因 | 正确做法 |
|---|---|---|
random() | 两次取值不同,可能进入不同分支 | 移入 Activity,或用 SDK 的确定性随机 |
time.now() | 同上 | 用 SDK 提供的工作流时钟 |
| 直接发 HTTP / 查数据库 | 重放时会真的再次打到外部系统 | 移入 Activity |
| 直接调用大模型 | 同 prompt 两次输出可能不同 | 移入 Activity |
| 起线程、依赖系统调度顺序 | 执行顺序不可复现 | 用 SDK 的并发原语 |
官方文档明确点名了 LLM 调用:
Workflow code must be deterministic to support replay. To handle non-deterministic operations like API calls, LLM/AI invocations, database queries, and other external interactions, put them in Activities.
2.2 代码组织的实际形态
# ❌ 错误:模型调用直接写在工作流函数里
# 重放时会真的再打一次模型,且返回值可能与历史不同 → 路径分叉
@workflow.defn
class ResearchWorkflow:
@workflow.run
async def run(self, topic: str) -> str:
resp = openai.chat.completions.create(...) # 不确定操作,禁止
if "需要深入" in resp.choices[0].message.content:
...
# ✅ 正确:不确定操作封进 Activity,工作流只做编排
@activity.defn
async def call_model(prompt: str) -> str:
"""Activity 内部允许任意不确定操作。
它的返回值会被写进 Event History,重放时直接取历史值,不会重复调用。"""
resp = openai.chat.completions.create(
model="gpt-4o", messages=[{"role": "user", "content": prompt}]
)
return resp.choices[0].message.content
@workflow.defn
class ResearchWorkflow:
@workflow.run
async def run(self, topic: str) -> str:
# execute_activity 会产生一个 Command,被记入 Event History。
# 重放时比对到同一条 Command,直接返回历史结果。
summary = await workflow.execute_activity(
call_model,
f"总结 {topic}",
start_to_close_timeout=timedelta(minutes=2),
# 重试策略由框架执行,不需要自己写 try/except 循环
retry_policy=RetryPolicy(maximum_attempts=3),
)
# 这里的分支判断基于 Activity 的返回值。
# 因为返回值来自历史,重放时必然走同一分支 —— 确定性得以保持。
if "需要深入" in summary:
...
2.3 非确定性错误的两个来源
第一类是写错了,改掉即可。第二类才是生产上真正会咬人的 —— 它把发布节奏和工作流实例的生命周期绑在了一起。对一个跑三天的长任务来说,这意味着三天内不能随意改工作流代码。
三、约束的实际边界
"不能改代码"的说法过于笼统。实际受限的只有会产生 Command 的操作。官方列出的是三类:
Starting or cancelling a Timer, Scheduling or cancelling Activity Executions, Starting or cancelling Child Workflow executions
即定时器、Activity、子工作流。
3.1 可以自由修改的
@workflow.defn
class ResearchWorkflow:
@workflow.run
async def run(self, topic: str) -> str:
summary = await workflow.execute_activity(call_model, ...)
# 下面这些都不产生 Command,重放不受影响,可以随时改:
title = summary.strip().split("\n")[0] # 纯计算
title = f"【调研】{title}" # 改字符串拼接方式
workflow.logger.info("已生成标题: %s", title) # 加日志
result = self._format(summary, title) # 重构辅助函数
return result
3.2 不能修改的
# 以下四类改动会破坏与旧历史的兼容:
# 1) 调换两个 Activity 的先后顺序
# 2) 新增一个 Activity
# 3) 删除一个 Activity
# 4) 增删定时器或子工作流
# 需要改时,用 SDK 的版本化 API 让新旧历史各走 各的分支。
这条推论把"不能改代码"收窄成了"不能改这三类操作的序列",可操作性完全不同。
3.3 对 Agent 场景的实践建议
Agent 里改动最频繁的恰恰是 prompt 和工具编排。缓解方式:
| 做法 | 效果 |
|---|---|
| prompt、工具调用、模型调用全部放进 Activity | 本来就是必须的;顺带让这些高频改动不触碰 Command 序列 |
| 工作流函数只保留最稳定的流程骨架 | 工作流改得越少,版本兼容负担越轻 |
| prompt 作为参数传入 Activity,而非硬编码 | 改 prompt 变成改配置,完全不动代码 |
四、它换来了什么
02 篇 5.1 节列出的 LangGraph 缺口,在这里都有对应答案:
| 能力 | 机制 |
|---|---|
| 自动恢复 | 服务端知道工作流未结束,会调度另一个 worker 接手,无需自写巡检 |
| 挂起不占资源 | 等待三天期间没有进程运行,定时器由服务端维护 |
| 自动重试 | Activity 失败按策略重试,次数、退避、超时均为配置项 |
| 跨服务编排 | 工作流可调用其他服务的 Activity,不限单进程 |
| 执行记录完整 | Event History 本身就是精确到每次调用的执行记录 |
4.1 最后一条对 Agent 的额外价值
Agent 可观测性里需要自行埋点才能看清的执行过程,在这里是重放机制的副产品 —— 它本来就必须记录每次 Activity 调用。
但重放会污染 trace:同一个工作流可能被重放多次,若在工作流函数里直接埋点,同一步会出现 N 次 span。处理方式见 05 篇。
五、接入成本
| 成本项 | 说明 |
|---|---|
| 运行一个集群 | Temporal Server 是独立的分布式系统,需要自己的 存储(Cassandra / MySQL / PostgreSQL)。有托管版可免运维,需付费 |
| 代码结构调整 | Agent 代码要拆成 Workflow + Activity 两层,不是加装饰器就能完成 |
| 团队认知成本 | 最贵的一项。不理解重放机制的人会写出隐蔽的非确定性缺陷,且只在崩溃恢复时暴露 —— 恰是最不希望出问题的时刻 |
5.1 选型判断
不适合:团队无分布式工作流经验,且诉求仅为"Agent 挂了能续上"。这种情况下 Temporal 属于过度工程,应先看 04 篇。
适合:跨多个服务、运行数天、涉及资金或不可撤销操作的业务流程(订单履约、审批流、资金结算)。这类场景下约束换来的可靠性是划算的,这也是它在金融与电商领域被广泛采用的原因。
下一篇 → 04 - DBOS 与 Restate
← 回到 专题索引 · Agent Infra 板块总览