⏱️ 把串行 Agent Pipeline 改成 Temporal 工作流之后,快了 3 倍
一次真实的同步→并行重构记录,从 45 秒到 15 秒
背景
我之前做了个跨境电商运营助手,流程是这样的:
加载商品 → 翻译 → 搜竞品 → 分析评论 → 定价建议 → 生成总结
每个步骤串行执行,一个跑完下一个才能开始。一次完整分析大约 30-45 秒。
这还只是单个商品。如果上线后有 100 个商品排队分析......
看一眼当时的代码:
python
def orchestrate(product_id: str) -> CrossBorderReport:
product = load_product(product_id)
translated = translate_product(product) # ~3s
competitor_report = analyze_competitors(...) # ~5s(Tavily 网络请求)
review_analysis = analyze_reviews(product_id) # ~3s
pricing = get_pricing_recommendation(...) # ~3s
summary = generate_summary(...) # ~3s
return report
翻译 3 秒,搜竞品 5 秒,这些步骤完全不依赖对方,却要排队等。这太浪费了。
核心问题
分析依赖图:
markdown
┌──→ 翻译 ──┐
│ │
加载商品 ────────┼──→ 搜竞品 ──┼──→ 竞品分析 ──┐
│ │ │
└──→ 分析评论 ──┘ ├──→ 定价建议 ──→ 总结
│
┌──────────────────────────┘
翻译和搜竞品之间没有箭头------它们可以并行。分析评论也只依赖商品数据。真正需要串行的只有:
- 定价建议:需要翻译结果 + 竞品分析 + 评论分析
- 生成总结:需要所有上游
原方案被硬写成串行,纯粹是代码结构限制。
为什么选 Temporal
第一个想到的是 asyncio.gather(),但只有并行不够:
| 需求 | asyncio.gather |
Temporal |
|---|---|---|
| 并行执行 | ✅ | ✅ |
| 自动重试 | ❌ 需要自己写 | ✅ 内置 |
| 超时控制 | ❌ 需要自己写 | ✅ 每个步骤独立设置 |
| 持久化 | ❌ 崩溃从头再来 | ✅ 从断点恢复 |
| 可观测性 | ❌ 全靠 print | ✅ Server Web UI |
| 分布式 | ❌ 单机 | ✅ 可跨机器 |
关键是:Tavily API 偶尔超时(5-10 秒卡住),asyncio.gather 只能等它超时,Temporal 可以设 start_to_close_timeout=30s,超时自动重试,重试用指数退避。
改造过程
Step 1:定义 Activity
每个 Activity 包装一个业务步骤,加上超时控制:
python
@activity.defn
async def load_product(product_id: str) -> dict:
"""从 product.json 加载商品信息"""
...
@activity.defn
async def translate_product(product_dict: dict) -> dict:
"""翻译商品信息"""
...
@activity.defn
async def search_competitors(product_dict: dict) -> dict:
"""搜索竞品"""
...
每个 Activity 独立、无状态、可重试。
Step 2:定义 Workflow 编排
python
@workflow.defn
class CrossBorderWorkflow:
@workflow.run
async def run(self, product_id: str) -> dict:
# Step 1: 加载商品(超时 10 秒)
product = await workflow.execute_activity(
load_product, args=[product_id],
start_to_close_timeout=timedelta(seconds=10),
)
# Step 2 & 3: 翻译 + 搜索竞品(并行!)
translated, competitors = await asyncio.gather(
workflow.execute_activity(
translate_product, args=[product],
start_to_close_timeout=timedelta(seconds=60),
),
workflow.execute_activity(
search_competitors, args=[product],
start_to_close_timeout=timedelta(seconds=30),
),
)
# Step 4 & 5: 竞品分析 + 评论分析(并行!)
comp_report, review = await asyncio.gather(
workflow.execute_activity(
analyze_competitors, args=[product, competitors],
start_to_close_timeout=timedelta(seconds=60),
),
workflow.execute_activity(
load_and_analyze_reviews, args=[product_id],
start_to_close_timeout=timedelta(seconds=60),
),
)
# Step 6: 定价(必须等上面全部完成)
pricing = await workflow.execute_activity(
get_pricing_recommendation,
args=[product, translated, comp_report, review],
start_to_close_timeout=timedelta(seconds=60),
)
# Step 7: 生成总结
summary = await workflow.execute_activity(
generate_summary,
args=[product, translated, comp_report, review, pricing],
start_to_close_timeout=timedelta(seconds=30),
)
return {"summary": summary, "pricing": pricing, ...}
关键就在这里------asyncio.gather 把互不依赖的 Activity 同时提交给 Temporal Server,Server 会调度到 Worker 并行执行。
Step 3:编写 Worker
一个长驻进程,监听任务队列:
python
async def main():
client = await Client.connect("localhost:7233")
worker = Worker(
client,
task_queue="cross-border-queue",
activities=[load_product, translate_product, ...],
workflows=[CrossBorderWorkflow],
)
await worker.run()
Step 4:测试
Temporal 提供了 WorkflowEnvironment.start_local(),不需要跑 Docker,直接内嵌启动:
python
async with await WorkflowEnvironment.start_local() as env:
async with Worker(env.client, task_queue="queue", ...):
result = await env.client.execute_workflow(
CrossBorderWorkflow.run, "P001",
id="test-001", task_queue="queue",
)
print(result["summary"])
效果对比
| 方案 | 耗时 | 是否支持重试 | 是否支持超时 | 故障恢复 |
|---|---|---|---|---|
| 同步串行 | ~17 秒 | ❌ | ❌ | ❌ 重头再来 |
| Temporal 并行 | ~8 秒 | ✅ 指数退避 | ✅ 每步独立 | ✅ 断点恢复 |
具体到每个步骤的时间线对比:
同步串行:
0s ─ 加载商品 ─ 翻译 ─ 搜竞品 ─ 分析竞品 ─ 分析评论 ─ 定价 ─ 总结 → 17s
Temporal 并行:
markdown
0s ─ 加载商品 ─┬─ 翻译 ─┬─ 分析竞品 ─┬─ 定价 ┬─ 总结 → 8s
├─ 搜竞品 ┤ │ │
└─ 分析评论 ───────────┘ │
│
并行节省了约 50% 的时间。 如果 Tavily 网络请求卡住超过 30 秒,Activity 会自动超时重试,而不会让整个流程挂掉。
遇到的坑
1. Temporal SDK 版本 API 变化
execute_activity 的参数必须用 args=[...] 关键字传:
python
# ✗ 不能用这种方式
await workflow.execute_activity(func, arg1, arg2, timeout=...)
# ✓ 必须用 args
await workflow.execute_activity(func, args=[arg1, arg2], timeout=...)
一开始翻了 15 分钟的错误栈才发现。
2. Activity 的 return 必须可序列化
Temporal 靠 gRPC 通信,Activity 返回值会被序列化成 JSON。所以不能返回 Pydantic 模型,得先 .model_dump() 转成普通 dict。
python
@activity.defn
async def translate_product(product_dict: dict) -> dict:
product = Product(**product_dict)
translated = _translate(product) # 返回 TranslatedProduct 对象
return translated.model_dump() # 转 dict,Temporal 才能传
3. Workflow 里的 import
Temporal 的 Workflow 代码会被重放(replay),普通 import 在重放时可能报错。需要包一层:
python
with workflow.unsafe.imports_passed_through():
from temporal_worker.activities import load_product, ...
什么时候值得用 Temporal
适合的场景:
- 流程超过 3 个步骤,且步骤间有明确的依赖关系
- 步骤涉及外部 API(Tavily、LLM 调用),容易超时或出错
- 需要持久化------进程重启后从断点恢复,不丢状态
- 需要监控------Temporal Web UI 能看到每个 Workflow 的执行状态
不适合的场景:
- 简单的 2-3 步串行------
asyncio.gather就够了 - 同步 CLI 脚本------不需要持久化
- 不需要分布式------单机场景太重了
项目复盘
这次改造给我几个启发:
- 画依赖图比画流程图重要------先搞清楚哪些步骤有数据依赖,哪些可以并行,再选技术方案
- 不要过早优化------串行能跑就先串行,跑通了再考虑并行。如果文章里的商品只有 3 个,串行的 45 秒完全可以接受
- Temporal 的学习曲线主要在 SDK API 上 ------概念本身很直观(Workflow编排、Activity执行),但
unsafe.imports_passed_through、args=[...]这种细节需要踩坑 - 生产环境要加什么------现在只是跑通了流程,如果要上线还需要:Activity 重试策略调优、Workflow 超时全局兜底、心跳检测(长时间运行的 Activity)
项目地址: github.com/cuzz123/cro...
如果你也在做 Agent 工作流的改造,欢迎交流经验 ~