持久 Agent:当任务活得比会话长
聊天式 agent 的生命周期等于一次会话:用户发起,模型循环,产出结果,上下文丢弃。持久 Agent(persistent agent / durable agent)指的是另一类系统:它承接的工作生命周期长于单次模型调用、长于单个上下文窗口、长于单个进程,甚至长于发起它的那台设备。
一个夜间跑依赖升级的 agent、一个跨三天迁移代码库的 agent、一个每周整理 issue 的 agent,都属于这一类。它们的共同点是:任务的时间尺度和会话的时间尺度脱钩了。这篇文章给「持久」下一个可操作的定义,并整理支撑它的三根支柱。
本文与已有文章的边界
| 已有文章 | 主要讨论 | 与本文的关系 |
|---|---|---|
| ReAct:推理与行动交织的 Agent 循环原型 | 单条轨迹内的推理-行动循环 | ReAct 是内环,本文讨论内环之外怎么让任务活下去 |
| Agent 全景指南 | Agent 范式演化与高可用 | 提供范式背景,本文聚焦持久性这一个维度 |
| Goal 模式深度研究 | 完成谓词、循环停止条件 | 完成谓词决定持久任务什么时候可以死 |
| Harness Engineering:长程 Agent 的工程化底座 | 工具、验证、环境的工程化 | Harness 是持久 agent 的运行环境,本文讲持久性本身的机制 |
四道墙
一个 agent 任务想活得久,要翻过四道墙。每道墙对应一种「死法」。
| 墙 | 死法 | 典型场景 |
|---|---|---|
| 上下文窗口 | 上下文耗尽,任务做到一半失忆 | 大型重构做到第 40 个文件时 token 见顶 |
| 会话 / 进程 | 进程退出,内存状态全丢 | 终端关闭、session 超时、程序崩溃 |
| 设备 | 宿主机不在线,任务停摆 | 笔记本合盖,本地 cron 不再触发 |
| 人类注意力 | 无人触发,任务不存在 | 没人记得每周一早上该发起那个扫描 |
上下文压缩只能缓解第一道墙,对后三道无能为力。压缩再好,进程一退出照样归零。所以持久性不是一个上下文管理问题,而是一个系统设计问题:任务的状态、执行进度和触发时机都必须存在于 agent 进程之外。
与聊天 Agent 的对照
| 维度 | 聊天 Agent | 持久 Agent |
|---|---|---|
| 生命周期 | 一次会话 | 跨会话、跨天、跨设备 |
| 状态位置 | 上下文窗口内 | 文件、git、数据库、事件日志 |
| 触发方式 | 人实时发 prompt | 定时器、事件、调度器 |
| 失败后果 | 重新问一次 | 需要从断点恢复,不能重做副作用 |
| 完成判定 | 模型自述 + 人当场确认 | 显式完成谓词 + 证据 |
| 人的位置 | 全程在场 | 只在升级点出现 |
右边一列的每一项都要专门的工程机制来支撑。把它们归拢,是三根支柱。
支柱一:可恢复状态
持久 agent 的第一原则:不把任务状态寄存在模型记忆或聊天上下文里。状态必须落在进程外的载体上,让任何一个新 session 能够读进度、接着干。
Anthropic 在 long-running agents 实践里给出了一个朴素可抄的方案。第一轮由 initializer agent 建立三样东西:init.sh(环境怎么拉起来)、进度文件(做到哪了、什么已验证)、初始 git 提交。之后每一轮 coding agent 的开场动作都是读 git log 和进度文件,收场动作都是提交代码并更新进度。状态载体就是文件系统和 git 历史,没有任何专用基础设施。
对 coding agent 来说这个方案格外自然,因为代码库本身就是状态:diff 记录改了什么,测试结果记录验证到哪,分支记录有几条并行的尝试。OpenAI 的 harness engineering 实践把这个思路推广成一条原则:仓库是 system of record,对 agent 不可见的信息等于不存在。
flowchart LR
S1["Session 1"] --> ES[("外部状态<br/>progress 文件 · git · 任务清单")]
ES --> S2["Session 2"]
S2 --> ES
ES --> S3["Session 3"]
S1 -. "上下文丢弃" .-> X1["✗"]
S2 -. "上下文丢弃" .-> X2["✗"]
每个 session 的上下文都会死,外部状态不死,任务就不死。
支柱二:可恢复执行
状态解决「新 session 知道做到哪了」,但还有一个更细的问题:崩溃发生在两次状态落盘之间怎么办?这就是 durable execution 要解决的事。
Temporal 是这个领域的代表。它的机制是 event sourcing 加确定性重放:workflow 的每一步(启动、activity 完成、定时器触发)都作为事件写进历史,进程崩溃后,新 worker 把事件历史从头重放一遍,代码走到崩溃前的位置,然后继续。对开发者来说,代码看起来像一直在跑,实际上是「随时可以死、死了能精确复活」。代价是 workflow 代码必须确定性:所有副作用(网络调用、随机数、时间)都要包进 activity,由框架记录结果。
LangGraph 走的是 checkpoint 路线:图的每个 super-step 结束时,checkpointer 把整个图状态存进数据库,同一个 thread 可以在任意 checkpoint 恢复、回放、甚至分叉出新路径。它还提供 interrupt 原语,让图可以停在某个节点等人类输入,几天后从原地继续——human-in-the-loop 成了持久执行的一等公民。
Diagrid 的工程师对 checkpoint 路线提出过一个值得记住的批评:checkpoint 不等于 durable execution。checkpoint 只保证「回到上一个存档点」,两个存档点之间做过的副作用既没有被记录,也不会被去重。如果节点内部调用了外部 API,崩溃恢复后这个调用会重来一次。event sourcing 记录的是每一步的结果,checkpoint 记录的是每一段的快照,粒度差了一个量级。选型时真正该问的问题是:崩溃点前最后一个副作用,恢复后会不会执行第二次。
支柱三:触发与调度
前两根支柱让任务「死不了」,第三根支柱让任务「不需要人也能开始」。
触发方式大体三类:时间触发(cron、定时器)、事件触发(CI 失败、PR 到达、issue 入队)、计划触发(一个长期目标被拆成的批次任务依次入队)。工程上的关键区别是触发器活在哪里:活在本地机器上的 cron 翻不过设备那道墙,活在服务器上的调度器才能做到发起设备离线后任务照常运行。
调度器还承担第二个职责:把「重复」正规化。一个每晚运行的任务,每次运行都应该拿到同样结构的任务说明、同样的权限边界、同样的验收标准,而不是靠人每天手打一段大同小异的 prompt。触发的自动化倒逼任务规格的显式化。
恢复语义:at-least-once 的世界
三根支柱立起来之后,还剩一个所有分布式系统都躲不掉的问题:恢复语义。
崩溃恢复本质上是重试,重试意味着任何一步都可能执行不止一次。对只读操作无所谓,对副作用是致命的:邮件会发两封,分支会建两个,打款会打两笔。持久 agent 的副作用必须满足三者之一:天然幂等(覆盖写文件)、显式去重(幂等键、claim lock)、或者被推到人工门后面(合并、部署、对外发布必须人批准)。
这也是持久 agent 和聊天 agent 在安全设计上的根本差异。聊天 agent 的危险操作有人当场看着;持久 agent 在凌晨三点无人在场时恢复执行,它的每一个副作用都要提前回答「重复执行一次会怎样」。
内环与外环
把本文和 ReAct 放在一起,可以得到一个干净的分层:ReAct 定义了内环——一条轨迹里 thought、action、observation 怎么转;持久性机制定义了外环——轨迹和轨迹之间,任务怎么活下去。
内环的失败模式是幻觉、重复循环、观察面太差;外环的失败模式是失忆、重复副作用、无人触发。两层的工程手段几乎不重叠:内环靠更好的工具、观察面和推理格式,外环靠外部状态、事件日志、调度器和幂等设计。讨论 agent 可靠性时把这两层混在一起,是很多争论各说各话的原因。
最小落地清单
不引入任何专用框架,一个持久 agent 任务也应该具备:
- 一份进程外的任务规格:目标、边界、验收标准,写在文件里而不是聊天里。
- 一个进度载体:progress 文件或 git 历史,每轮开始先读、结束必写。
- 一个进程外的触发器:cron、CI hook 或服务器调度,不依赖人记得。
- 副作用清单:逐项标注幂等 / 去重 / 人工门。
- 完成谓词:满足什么证据任务就终止,避免永生任务,参见 Goal 模式深度研究。
需要更强保证时再引入 Temporal 或 LangGraph 这类框架。框架解决的是执行粒度的持久性,上面五件事框架替代不了。
