设计可靠的智能体工作流
让长任务经得起现实折腾:幂等性、检查点、可恢复性,以及显式的失败状态。
假设进程随时会死
长时工作流一定会被打断:网络抖动、限流、容器重启、用户合上笔记本。从一开始就为中断而设计,因为给一段线性脚本后补可恢复能力等于重写。
每个步骤都要问的核心问题:如果此刻崩溃,重试时会发生什么?
幂等让重试变安全
一个操作跑两次等于跑一次,就是幂等。正是这一条性质让重试从危险变成安全。
| 不幂等 | 改造成幂等 |
|---|---|
create_report(id) | create_report(id, dedupe_key=task_id) |
append_row(...) | upsert_row(row_id, ...) |
send_email(to, body) | send_email(idempotency_key=hash(to+body+date)) |
increment_counter() | set_counter(computed_value) |
幂等键要从任务本身确定性推导,不能用随机数——随机就失去意义了。
检查点:把状态写到上下文之外
每完成一个有意义的步骤就把进展写入持久化存储,而不是留在模型记忆里:
run-2026-10-16-a1/
├── manifest.json # 输入、任务 ID、开始时间
├── steps/
│ ├── 01-extract.json # 状态、产物指针、耗时
│ ├── 02-transform.json
│ └── 03-review.json
└── artifacts/
恢复时读 manifest,找到第一个未完成的步骤,从那里继续。已完成的步骤应通过检查产物是否存在来跳过,而不是相信状态字段——进程在写一半时挂掉,状态字段会说谎。
失败状态必须显式
「失败」不是诊断结论。要区分,因为每种都有不同处置:
| 状态 | 含义 | 应对 |
|---|---|---|
retryable 可重试 | 瞬时问题(超时、429) | 退避重试 |
validation_failed 校验失败 | 输出格式错误 | 带上错误信息重试 |
needs_input 需补充信息 | 关键信息缺失 | 询问人工,不要猜 |
blocked 被阻塞 | 外部依赖不可用 | 挂起任务并通知 |
fatal 致命 | 不可恢复 | 干净停止,保留产物供排查 |
把一切失败都当 retryable 的工作流会烧光预算并最终损坏状态;把一切当 fatal 的工作流则过早放弃。
绝不让模型静默吞掉失败。某一步没成功,工作流必须知道,而不是只留下一段模糊的印象。
预算跟踪
长任务往往是钱先花完,而不是想法先穷尽。按次运行跟踪:
- 已消耗 token / 上限
- 工具调用次数 / 上限
- 已耗挂钟时间
- 每步的重试次数
在开始每个步骤前检查预算,而不是事后发现超支。触顶时以 interrupted 状态停止并留下可恢复的检查点,绝不停在一半已应用的状态上。
恢复检查清单
- 读 manifest → 还原任务身份与输入
- 通过产物是否存在发现已完成步骤
- 重新校验上一轮遗留的步骤产物(不要默认完好)
- 从第一个未完成步骤继续
- 之后每步开始前复查预算
第 3 条最常被跳过。崩溃前几毫秒写入的检查点可能是截断的;校验它只需一次读取,却能省下数小时的困惑。