先描述一个你大概遇到过的场景。
晚上九点,你让编程智能体跑一个大活:把三个仓库的依赖升到同一版本,改掉所有受影响的调用,跑通测试,最后提一个合并请求。它在第八十分钟开始动手改代码,第三条命令执行到一半,进程没了。
可能是容器被回收,可能是服务器超时,可能是你手滑关了终端。重跑一次,前面那一小时的词元白花;从头再来,它未必走同一条路。
十月下旬,Vercel 实验室开源了一个叫 durable-harness 的包,专门处理这件事。Apache 2.0 许可,npm 上当前版本 0.0.1,明确标着实验性质——零点零版,接口随时可能变。
它改的其实只有一件事
不是让智能体更聪明,也不是让它跑得更快。它改的是「进程死了以后怎么办」。
过去的做法是把对话历史存下来,崩了之后把历史重新喂给模型,让它接着干。这办法的问题很明显:历史记录的是说过什么,不是真正执行过什么。模型以为自己做过的事,和机器上实际发生的事,是两回事。
durable-harness 换了个结构:一次会话就是一次工作流运行,这次运行的事件日志是唯一的事实来源。 对话内容、正在进行的那一轮、每一次模型调用、每一次工具调用,全部按顺序落进日志。任何一个环节崩了,换一台机器,都能从这个日志里恢复,把那一轮做完。
这里有个细节值得一提:因为超时、崩溃、唤醒或者任务被交接到另一台机器而分成好几次调用的一轮,在系统里仍然算同一轮,检查点跟着会话走。这对长任务是必要的——大多数崩溃不是发生在两次工具调用之间,而是发生在一次调用中间。
会话变成一次运行,代价和好处都很具体
好处是恢复的粒度细。它不是「从头再来」,也不是「从上一轮重来」,而是从最后一个没做完的步骤重来。已经完成的工具调用结果都存着,恢复时直接复用,不重复花钱。
代价是你要接受一套新的抽象。「会话」不再是应用内存里的一个对象,而是存储里的一条运行记录;「对话」不再只属于某个前端,而是日志的一段。想清楚这一点,后面几个设计就顺了。
实际写代码时,它给的是几个入口包,对应不同的智能体框架。Pi 走 durable-harness/pi-durable(官方推荐,用的是 Pi 自带的持久化内核),OpenCode 走 durable-harness/opencode,fx 走 durable-harness/fx。Claude Code 和 Codex 各有一个包,但规矩更严——它们只能在会话自己的 Vercel 沙箱里跑,绝不允许跑在共享的函数实例上。
这条限制不是洁癖。命令行工具会读写文件、执行 shell,放在共享实例上,等于让一个用户的任务碰另一个用户的机器。
三种存储,从本地到自托管
持久化必然要问一句「存在哪」。它给了三档。
本地开发用 Workflow SDK 的本地实现,零配置,装完就能跑,不需要数据库。部署到 Vercel 用托管存储和队列,会话可以在 Agent Runs 里看。要自托管,用 Postgres 版或其他实现了同一套接口的后端。
这个设计对国内的团队有现实意义:能不能自托管,往往决定这套东西能不能进内网。 只支持自家云的持久化方案,在很多场景里第一步就被排除了。
最值钱的一条:不该重跑的工具,不会跑第二次
这是整个包设计里最容易被略过、又最该抄走的部分。
每个工具在接入时要标一个属性:重放安全,还是绝不重放。读文件、查网页、跑只读查询,重放无所谓;发邮件、下单、转账、发布文章,重放一次就是事故。
它的处理方式是:不安全重跑的工具,如果崩溃时正在执行、状态不明,不会被执行第二次,而是以一个「已中断」的状态返回给模型。模型拿到这个状态,可以选择换个办法、或者回头问人。
同一逻辑还延伸到文件改动:虚拟工作区或沙箱里的每次文件变更,和这次调用的结果一起提交,不会重复应用。沙箱环境还用了会话自己的隔离令牌,被接管的那台机器不会再继续执行。
这套思路站内此前在别的产品里见过——Perplexity 把智能体沙箱开源时,重点也是「边界做在系统层」;微软那份智能体体验手册里反复强调的「闸门要证明代码真的跑起来」,讲的是同一件事的另一面:别相信「大概跑过了」,要么有日志,要么有证据。
它填不上的三个坑
第一,正在执行的外部动作,它救不了。
崩溃那一刻,那个工具调用可能已经把请求发出去了,对方服务器也收了。日志只知道「发出了」,不知道「对方办成了没有」。这类动作必须在接收端做幂等——用唯一键、去重表或者查询接口确认真实状态。durable-harness 的作者自己也这么写:持久化能阻止已完成的步骤重跑,但阻止不了一个已经出网的在途调用。
第二,对话历史不等于持久化。 只要你的系统还靠「把聊天记录再喂一遍」来恢复,就仍然会重复执行工具。日志和对话是两套东西,别混。
第三,它在零点零版。 官方写明只发补丁版本,接口随时可能变;追踪功能要 Node 24;OpenCode 和 effect 的版本如果不是它锁定的那个,那一轮会直接失败并打印两边的版本号。想上生产,先把版本钉死。
什么情况下你根本不需要它
这句话值得单独说,因为多数人不需要。
判断标准只有一条:这一轮会不会活得比进程长。
几秒钟就结束的请求响应式智能体,需要的不是工作流引擎,是时间预算、迭代次数上限,和幂等的副作用。把这类任务套上持久化,只会多一层调试成本。
真正需要的是这几类:跑几十分钟到几小时的编码任务;要等持续集成跑完、或者等人审批的流程;崩一次就要重花几十美元词元的批处理;以及跨天执行、中途要停下来的活。
还有一个信号:当你开始手动记录「它跑到哪了」,就该考虑换方案了。手工记录的进度表,本质就是一个不合格的事件日志。
顺带一提,这个月不止一家在补这件事
据二手报道整理(细节未逐一核对):Mastra 的 AgentController 在十月初转正式版,主打持久化线程与断点恢复;Anthropic 发过一篇讲长跑智能体 harness 的长文;Google 给每个智能体分了隔离的轻量虚拟机。
方向是一致的:智能体从「问答」走向「干活」,评价标准就从「答案好不好」变成「断了能不能接上」。 站内写过 Devin 把记忆写进 Git 仓库、Anthropic 自研版本控制系统,都是同一个问题的不同解法——一个管「记得什么」,一个管「改了什么」,这个包管「做到哪了」。
最小上手路径
真要试,建议按这个顺序,别一上来就接生产。
第一步,先只做本地。 装包加对应的适配包,用本地存储跑通一个最简单的任务,确认会话能恢复。这一步不需要任何云服务。
第二步,把工具分类。 把你手上的工具列出来,逐个标「可重放」或「不可重放」。这一步花的时间最多,价值也最大——标完之后你会发现,真正不能重跑的其实只有少数几个。
第三步,处理那几个不可重放的。 给它们加幂等键,或者在接收端加唯一约束。这一步不做,前面的日志只是让你更清楚地看到自己重复扣了钱。
第四步,加鉴权。 每个智能体的对话路由在生产环境必须有授权检查,公开路由要显式打开,别默认放开。
第五步,再考虑换存储。 自托管走 Postgres,托管走云。切换的成本不高,因为接口是同一套。
老达点评
一、这个包卖的不是功能,是一种记账习惯。 做之前先写下来,做完再确认,别相信「应该跑过了」。这条习惯跟智能体没关系,写普通后端也该这样。它只是把这件事做成了默认行为。
二、「不安全重跑的工具只执行一次,然后返回中断」是这个包最好的设计。 大部分方案在这里选择重试,理由是「用户希望任务继续」。但在付款、发信、发布这类动作上,正确的做法是停下来问人,而不是赌一次。
三、它把「需不需要」写清楚了,这点很少见。 官方文档直接说:几秒钟结束的智能体不需要持久化执行。一个工具愿意告诉你什么时候别用它,通常说明作者自己踩过坑。
四、对独立开发者,现在最好的用法是抄设计,不是接依赖。 零点零版的接口会变,接进去就要跟着改。但「会话即运行、日志即事实、工具分可重放与否」这三条,今天就能用在你自己那套脚本里——哪怕只是给每个步骤加一行日志,加一个幂等键。
想把智能体这条线从头理一遍,可以看 AI 智能体与自动化专题;如果更关心编程工具本身怎么选、怎么配,AI 编程工具专题里的横向对比更直接。