AI Agent 运行数小时不崩盘:聊聊正在升温的 Durable Execution

AI Agent 运行数小时不崩盘:聊聊正在升温的 Durable Execution

如果你也关注 Go、后端开发、AI Agent 与工程实践,欢迎点个关注。这里不只分享结论,也会持续记录踩坑过程、架构思路和真实落地经验,希望能与你一起进步。 过去我们构建 AI Agent,通常从一个简单循环开始:

text 复制代码
调用模型 -> 判断是否需要工具 -> 执行工具 -> 把结果交给模型 -> 继续

这个循环用几十行代码就能跑起来,演示效果也很好。

但当 Agent 开始执行真正的生产任务,问题会突然变得复杂:

  • 一次任务运行数小时,中间 Worker 重启;
  • 模型已经调用成功,本地还没保存结果就发生宕机;
  • 邮件已经发出,但调用方因为超时再次执行;
  • Agent 等待人工审批两天,原来的进程早已不存在;
  • 用户关闭页面后重新连接,希望继续看到之前的输出;
  • 发布新版本后,旧任务仍然要按照旧逻辑完成;
  • 同一个任务需要在不同执行路径之间进行回放和评测。

这些问题都指向一个近期快速升温的技术方向:Durable Execution,持久化执行。

Google 在 2026 年发布 Agent Executor,将持久化执行、快照恢复、单写者一致性和连接恢复作为长时 Agent Runtime 的基础能力;Dapr 最新文档将工作流支持的 DurableAgent 作为推荐执行模式;Temporal 也把 AI Agent 视为持久化执行的重要应用场景。Google:Agent Executor、Dapr:Durable Agents、Temporal:Durable Execution

这篇文章不讨论某个具体框架的 API,而是从工程原理出发,解释为什么长时 Agent 需要持久化执行,以及如何用 Go 构建一套可恢复、可重放、不会重复产生副作用的执行模型。


一、先看一个非常真实的故障窗口

假设 Agent 正在处理采购任务:

text 复制代码
1. 查询库存
2. 比较三家供应商
3. 生成采购方案
4. 等待负责人审批
5. 调用供应商接口下单
6. 发送采购结果

第五步中可能出现这样的执行顺序:

text 复制代码
Agent 调用供应商 API
    -> 供应商创建订单成功
    -> Agent Worker 在保存结果前崩溃
    -> 任务被重新投递
    -> Agent 再次调用供应商 API
    -> 创建第二笔订单

如果我们只依赖消息队列,通常只有两个选择:

  • ACK 消息:可能丢失未保存的执行结果;
  • 不 ACK 消息:任务会重跑,外部副作用可能重复发生。

这正是长流程的核心难题:

计算过程可以重放,但已经发生的真实副作用不能随意重放。

普通任务队列保存的是"还有一项任务要执行",持久化执行系统保存的是"这项任务已经执行到哪里,每一步得到了什么结果,接下来应该继续哪一步"。


二、Durable Execution 到底是什么?

持久化执行可以概括为:

将程序执行过程中的关键决策和外部结果写入持久化历史,使任务在进程崩溃、机器重启或网络中断后,可以从已确认的位置继续执行,而不是从头再来。

它通常包含四个核心概念。

1. Workflow

Workflow 描述长期业务流程,例如:

text 复制代码
创建研究任务
-> 搜索资料
-> 分析资料
-> 等待人工确认
-> 生成报告
-> 发布报告

Workflow 负责控制流程,不直接执行不确定的网络和数据库操作。

2. Activity

Activity 是可能失败、超时或产生副作用的操作:

  • 调用大模型;
  • 查询搜索引擎;
  • 发送邮件;
  • 写数据库;
  • 调用支付或订单接口;
  • 执行代码。

Activity 可以独立设置超时、重试和幂等策略。

3. Event History

每个 Workflow 都有一份只追加的执行历史:

text 复制代码
WorkflowStarted
ActivityScheduled(model_call_1)
ActivityCompleted(model_call_1, result_ref)
ActivityScheduled(search_1)
ActivityCompleted(search_1, result_ref)
ApprovalRequested(approval_1)
ApprovalReceived(approval_1, approved=true)
...

恢复任务时,系统读取历史并重新构建 Workflow 状态。

4. Checkpoint / Snapshot

当历史很长时,每次从第一条事件开始恢复会越来越慢。系统可以周期性保存快照:

text 复制代码
Snapshot at sequence 200
+ Events 201...230
= Current State

快照用于加速恢复,事件历史仍然是解释任务如何到达当前状态的重要证据。


三、为什么"保存聊天记录"还不够?

很多 Agent 系统已经把消息写入数据库:

text 复制代码
user: 帮我调研新能源汽车
assistant: 我准备先搜索行业数据
tool: ...
assistant: ...

这只能保存对话内容,不能完整保存执行语义。

系统还需要知道:

  • 这次工具调用是否已经提交;
  • 当前步骤由哪个 Worker 持有;
  • 工具结果是否可以安全复用;
  • 失败属于可重试还是永久失败;
  • 是否正在等待审批或外部事件;
  • 预算还剩多少;
  • 使用的是哪个 Agent、Prompt 和工具版本;
  • 恢复时应该继续执行,还是重新做模型决策。

因此,Conversation Memory 与 Execution State 是两个不同概念:

类型 解决的问题
对话记忆 Agent 知道用户和模型之前说过什么
执行状态 系统知道哪些步骤已经可靠完成
业务状态 订单、支付、工单等领域对象当前是什么状态

三者可能互相关联,但不应该混成一份不可解释的 JSON。


四、持久化执行依赖"确定性重放"

很多工作流引擎恢复状态时,不是直接反序列化整个进程内存,而是重新执行 Workflow 代码,并从历史中读取已经发生过的结果。

例如,第一次执行时:

text 复制代码
代码请求调用 LLM
-> 引擎调度 Activity
-> Activity 返回结果 A
-> 结果 A 写入历史

Worker 重启后:

text 复制代码
Workflow 代码重新运行
-> 再次走到"调用 LLM"
-> 引擎发现历史中已有结果 A
-> 直接返回 A,不再调用 LLM

这就是 Replay。

它带来一条非常重要的规则:

Workflow 代码必须是确定性的;不确定操作必须放进 Activity。

下面的写法不适合直接出现在可重放的 Workflow 中:

go 复制代码
// 错误示意:重放时可能得到不同结果。
now := time.Now()
id := uuid.NewString()
value := rand.Intn(100)
response, _ := http.Get("https://example.com")

这些结果在每次重放时可能不同,会导致代码路径与历史不一致。

正确思路是由执行引擎提供可重放的时间、随机数和版本信息,或者把外部操作放入 Activity:

go 复制代码
type ResearchWorkflow struct {
	Activities Activities
}

func (w *ResearchWorkflow) Run(
	ctx WorkflowContext,
	input ResearchInput,
) (ResearchReport, error) {
	sources, err := ctx.ExecuteActivity(
		"search_sources",
		input.Query,
		ActivityOptions{
			Timeout:    2 * time.Minute,
			MaxAttempts: 3,
		},
	)
	if err != nil {
		return ResearchReport{}, err
	}

	report, err := ctx.ExecuteActivity(
		"generate_report",
		sources,
		ActivityOptions{
			Timeout:    5 * time.Minute,
			MaxAttempts: 2,
		},
	)
	if err != nil {
		return ResearchReport{}, err
	}

	return report.(ResearchReport), nil
}

这里的 WorkflowContext 是框架无关的示意接口。重点不是具体语法,而是:

  • Workflow 只描述控制流;
  • 搜索和模型调用由 Activity 执行;
  • Activity 结果写入历史;
  • 重放时直接复用已完成结果。

五、重放不等于 Exactly Once

持久化执行经常被误解为"所有 Activity 都只会执行一次"。

实际上,在分布式系统中仍然可能出现:

text 复制代码
Activity 调用外部接口成功
-> Worker 在上报结果前崩溃
-> 引擎无法确认是否成功
-> Activity 被重新调度

所以常见保证仍然是 At Least Once。持久化执行负责可靠调度和状态恢复,外部副作用仍然需要业务幂等。

1. 为每个副作用生成稳定业务键

text 复制代码
tool_call_id = workflow_id + step_id + operation

例如:

text 复制代码
purchase_123 + step_5 + CREATE_ORDER

重试时继续使用同一个键。

2. 服务端使用唯一约束兜底

sql 复制代码
CREATE UNIQUE INDEX uk_supplier_order_request
ON supplier_orders (tenant_id, request_id);

重复请求返回第一次创建的订单,而不是再次执行。

3. 区分结果未知与业务失败

外部调用超时,不应立即记为失败:

text 复制代码
SUCCEEDED:确认成功
FAILED:确认失败
UNKNOWN:调用结果无法确认

进入 UNKNOWN 后,应使用原请求号查询远端、等待回调或者执行对账,而不是直接生成新请求号重试。

4. Activity 自身也要保存尝试记录

go 复制代码
type ActivityAttempt struct {
	WorkflowID   string
	ActivityID   string
	Attempt      int
	IdempotencyKey string
	Status       string
	StartedAt    time.Time
	FinishedAt   *time.Time
	ResultRef    string
	ErrorCode    string
}

这样可以区分:

  • Workflow 逻辑重放;
  • Activity 因失败重试;
  • Worker 崩溃后重新领取;
  • 用户显式要求重新执行。

六、一个最小可理解的 Go 执行模型

生产环境通常会选择成熟工作流引擎,但理解最小模型有助于做正确的架构决策。

1. 事件结构

go 复制代码
type EventType string

const (
	EventWorkflowStarted   EventType = "WORKFLOW_STARTED"
	EventActivityScheduled EventType = "ACTIVITY_SCHEDULED"
	EventActivityCompleted EventType = "ACTIVITY_COMPLETED"
	EventActivityFailed    EventType = "ACTIVITY_FAILED"
	EventApprovalRequested EventType = "APPROVAL_REQUESTED"
	EventApprovalReceived  EventType = "APPROVAL_RECEIVED"
	EventWorkflowCompleted EventType = "WORKFLOW_COMPLETED"
)

type WorkflowEvent struct {
	WorkflowID string
	Sequence   int64
	Type       EventType
	StepID     string
	Payload    json.RawMessage
	CreatedAt  time.Time
}

数据库对 (workflow_id, sequence) 建立唯一约束,事件只允许追加,不允许原地修改。

2. 当前状态由历史归约得到

go 复制代码
type WorkflowState struct {
	Status             string
	CompletedActivities map[string]json.RawMessage
	PendingApproval    string
	LastSequence       int64
}

func Reduce(events []WorkflowEvent) WorkflowState {
	state := WorkflowState{
		Status:              "NEW",
		CompletedActivities: make(map[string]json.RawMessage),
	}

	for _, event := range events {
		state.LastSequence = event.Sequence

		switch event.Type {
		case EventWorkflowStarted:
			state.Status = "RUNNING"
		case EventActivityCompleted:
			state.CompletedActivities[event.StepID] = event.Payload
		case EventApprovalRequested:
			state.Status = "WAITING_APPROVAL"
			state.PendingApproval = event.StepID
		case EventApprovalReceived:
			state.Status = "RUNNING"
			state.PendingApproval = ""
		case EventWorkflowCompleted:
			state.Status = "SUCCEEDED"
		}
	}
	return state
}

这段代码展示了事件溯源的核心:当前状态不是唯一事实,事件历史才是;当前状态可以由历史重新计算,也可以用 Snapshot 加速。

3. 追加事件时处理并发

多个 Worker 可能同时尝试推进同一 Workflow,因此必须使用乐观并发控制:

sql 复制代码
INSERT INTO workflow_events (
  workflow_id,
  sequence,
  event_type,
  step_id,
  payload,
  created_at
)
VALUES (
  :workflow_id,
  :expected_sequence + 1,
  :event_type,
  :step_id,
  :payload,
  NOW()
);

唯一键冲突表示其他 Worker 已经推进了状态,当前 Worker 必须重新加载历史,不能覆盖新状态。

生产系统还会使用租约、分区单写者或 Actor 模型降低竞争。Google Agent Executor 也将单写者架构作为分布式会话一致性的关键设计。Google:Agent Executor


七、Human-in-the-loop 为什么特别适合持久化工作流?

人工审批可能几秒完成,也可能等待几天。如果使用普通 HTTP 请求:

  • 连接无法保持几天;
  • Worker 不能一直占用;
  • 服务发布后原进程会消失;
  • 审批消息可能重复到达;
  • 用户可能撤回或超时。

持久化工作流可以在等待时完全释放计算资源:

sequenceDiagram participant A as Agent participant W as Workflow Engine participant H as Human participant T as Tool A->>W: 请求执行高风险操作 W->>W: 写入 ApprovalRequested W-->>A: 挂起,不占用 Worker W->>H: 发送审批通知 H->>W: Approved W->>W: 写入 ApprovalReceived W->>A: 从 Checkpoint 恢复 A->>T: 执行工具 T-->>A: 返回结果

审批事件需要:

  • 稳定的 approval_id;
  • 明确的审批人和权限;
  • 到期时间;
  • 同意、拒绝和撤回状态;
  • 重复回调幂等;
  • 完整审计记录。

Workflow 收到事件后从等待点继续,不需要重新让模型回顾全部历史并猜测自己应该做什么。


八、断线恢复:不要把连接生命周期等同于任务生命周期

长时 Agent 通常通过 SSE 或 WebSocket 输出进度。客户端断线是常态,不应该触发任务重跑。

每个输出事件都应具有递增序号:

text 复制代码
run_id: run_123
sequence: 42
event: tool_completed
data: ...

客户端保存最后收到的序号,重连时携带:

http 复制代码
GET /v1/runs/run_123/events
Last-Event-ID: 42

服务端先补发 43...current,再继续发送实时事件。

Google Agent Executor 将这种能力称为 Connection Recovery:客户端按最后看到的序号重新连接并补齐缺失响应。Google:Agent Executor

这里需要区分三种状态:

text 复制代码
Workflow State:任务执行到了哪里
Stream State:客户端收到了哪里
Conversation State:模型上下文包含什么

客户端重连只改变 Stream State,不能创建新 Workflow。


九、版本升级是持久化执行最容易忽略的坑

一个 Workflow 可能运行几天,而应用每天都在发布。

假设旧代码是:

text 复制代码
搜索 -> 写报告

新代码变成:

text 复制代码
搜索 -> 事实核查 -> 写报告

旧 Workflow 恢复时,如果直接执行新代码,历史中没有"事实核查"事件,重放就可能发生不一致。

常见处理方式有三种。

1. Workflow 版本标记

创建任务时记录:

text 复制代码
workflow_type
workflow_version
agent_version
prompt_version
tool_schema_version

旧任务继续使用兼容实现,新任务使用新版本。

2. 版本化分支

工作流引擎可以在历史中记录版本决策:

go 复制代码
version := ctx.GetVersion("add_fact_check", 1, 2)
if version >= 2 {
	if err := runFactCheck(ctx); err != nil {
		return err
	}
}

重放旧任务时读取历史版本 1,新任务记录并使用版本 2。

3. Continue-as-new

历史过长或逻辑需要大幅升级时,可以创建一个继承必要状态的新 Workflow,让旧历史归档。

无论采用哪种方式,都不应该假设所有运行中的任务会在下一次发布前结束。


十、工作流、任务队列和 Actor 应该怎么选?

它们不是互斥技术,而是解决不同问题。

技术 更适合解决
任务队列 分发相对独立、可以短时间完成的任务
持久化工作流 多步骤、长时间、需要等待和恢复的业务流程
Actor 有稳定身份和封装状态的并发实体
状态机 限制业务对象的合法状态迁移
事件溯源 保存完整变化历史并重建状态

一个 Agent 平台可能同时使用:

text 复制代码
Workflow 管理 Run 生命周期
-> Queue 分发 Activity
-> Actor 管理 Session 单写状态
-> 状态机约束业务阶段
-> Event History 支持恢复与审计

什么情况下普通队列已经足够?

  • 任务几秒内完成;
  • 无须等待外部事件;
  • 失败后可以安全从头执行;
  • 没有不可重复的副作用;
  • 不需要保存中间决策轨迹。

什么情况下应该考虑持久化执行?

  • 任务运行分钟、小时或天;
  • 包含多个模型与工具调用;
  • 需要人工审批;
  • 需要暂停、恢复或取消;
  • 失败后不能从头重做;
  • 工具调用成本高或有真实副作用;
  • 需要完整审计与轨迹评测;
  • 多 Agent 之间存在依赖和交接。

十一、使用成熟引擎,还是自己实现?

一个最小事件循环并不难写,难的是后面的边界:

  • 历史一致性;
  • 定时器和长时间等待;
  • Worker 租约与故障接管;
  • Activity 重试和心跳;
  • 取消传播;
  • Workflow 版本兼容;
  • 历史压缩;
  • 多租户隔离;
  • 跨区域容灾;
  • 可视化和故障运维。

因此,生产项目通常更适合评估成熟方案:

  • Temporal:以 Workflow 和 Activity 为核心,提供多语言 SDK 和持久化执行;
  • Dapr Workflows / Durable Agents:与 Dapr 的状态、Pub/Sub、服务调用和可观测能力结合;
  • Google Agent Executor:面向分布式 Agent 的开放 Runtime 标准,强调恢复、隔离和会话一致性;
  • 云厂商托管工作流:适合已经深度使用相应云生态的团队。

选择时重点比较:

  1. 是否支持当前主要语言,尤其是 Go;
  2. Workflow 的确定性与版本升级模型;
  3. Activity 的超时、重试、心跳和取消;
  4. 外部事件与人工审批支持;
  5. 历史和 Payload 的大小限制;
  6. 多租户、安全与数据驻留;
  7. 自托管和托管成本;
  8. 可观测性与运维工具;
  9. 与现有 MQ、数据库和 Kubernetes 的整合成本;
  10. 是否会把 Agent 框架与执行引擎强耦合。

不要只看"能不能跑通 Demo",而要测试 Kill Worker、断网、重复事件、长时间等待和版本升级。


十二、我们的落地顺序

如果现有 Agent 仍运行在一个同步 HTTP Handler 中,可以按以下顺序演进。

第一阶段:任务异步化

  • 创建稳定的 run_id;
  • HTTP 只负责提交任务;
  • 客户端通过查询或事件流获取进度;
  • Run 使用显式状态机。

第二阶段:步骤持久化

  • 拆分模型调用和工具 Activity;
  • 保存 Step、Attempt 和结果引用;
  • 写操作增加幂等键;
  • 记录 UNKNOWN 状态并提供对账。

第三阶段:故障恢复

  • 引入 Workflow History;
  • 增加 Checkpoint;
  • Worker 使用租约和心跳;
  • 支持从已完成步骤恢复;
  • 增加取消传播。

第四阶段:等待与协作

  • 外部事件唤醒;
  • Human-in-the-loop;
  • 多 Agent 子工作流;
  • 连接断开后的事件补发。

第五阶段:生产治理

  • Workflow 和 Agent 版本管理;
  • Replay 测试;
  • 历史归档;
  • 跨租户隔离;
  • 成本、质量和恢复能力指标;
  • 故障注入与灾难恢复演练。

每个阶段都能独立产生价值,不必为了使用某个热门框架一次性重构整个平台。


结语

AI Agent 越来越像一种长期运行、会等待、会分支、会调用外部系统的动态程序。传统的"请求进来、执行完成、返回结果"模型很难承载它。

这也是 Durable Execution 重新受到关注的原因。

它不是让机器永不故障,而是让故障不再意味着任务丢失或从头开始:

进程可以消失,Worker 可以更换,连接可以断开,但任务的执行历史必须留下,已经完成的步骤不能凭空丢失,真实副作用不能因为重放而重复发生。

对于生产 Agent,模型决定它能做多聪明的事,持久化执行决定它能否把这些事可靠地做完。

未来我们可能会把 Agent 看作一种新的分布式工作负载,而 Durable Execution,就是这类工作负载正在补上的执行底座。

相关推荐
金銀銅鐵14 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路15 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x15 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn17 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈18 小时前
AI 读你的提示词,其实很没耐心
后端
用户83562907805119 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我1234519 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒19 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记19 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk5987819 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端