正在刊行长文 · Essay
2026-08-13所有内容
随机比特 · Random Bits

不用 Agent 框架,反而更容易看清 Agent:一个循环要保存哪些状态

2026-08-13AI Engineering / Systemsrbits.uk
不用 Agent 框架,反而更容易看清 Agent:一个循环要保存哪些状态

先看一个只在演示环境里永远不会发生的故障。

Agent 正在处理一批供应商退款。它已经核对订单,生成计划,获得审批,并调用退款接口。就在接口返回之后、系统记录结果之前,进程重启了。

服务恢复时,只剩一段聊天记录。最后一条消息写着「准备执行退款」,但没人知道外部接口到底有没有成功。重新跑一遍,可能重复退款;直接标成完成,可能漏掉退款;让模型读聊天记录自行判断,它只能把未知状态写得更像一个答案。

团队打开 Agent 框架的 trace,看到几十个漂亮节点,却依然答不出最基本的问题:这次运行现在处于哪一个状态,下一步允许做什么,哪些动作绝不能重放?

这时,暂时拿掉框架反而有帮助。

不是因为框架无用,而是因为抽象隐藏得太成功。只写一个最小循环,团队会被迫看见 Agent 真正是什么:一个由概率决策驱动、会跨越外部系统、需要暂停和恢复的状态机。

两者的边界由此变得清楚:能回答问题的是模型,能从崩溃中回来的是状态。

聊天记录无法恢复真实运行

最小 Agent 循环并不神秘。

加载当前状态,整理本轮观察,让模型提出下一步;运行时代码检查这一步是否合法;如果只是生成结果,就更新终态;如果需要工具,先持久化动作意图,再执行工具并保存回执;随后把回执归约成新状态,进入下一轮。

load state
  → observe
  → propose transition
  → validate
  → persist intent
  → execute
  → persist receipt
  → reduce state
  → repeat or finish

关键区别在于,模型只提出 transition,不能直接宣布状态已经改变。它可以说「下一步调用退款」,但 waiting_approval 能不能进入 executing,要由审批、权限、参数和计划版本共同决定;工具返回一段自然语言,也不能自动让任务进入 succeeded,业务后置条件仍要由代码验证。

一个可持久化的 Agent 循环

聊天记录为什么不够?因为它保存的是对话顺序,不是运行语义。

一条能够恢复的 run state,至少要分成六组。

第一组是任务身份:run_id、原始目标、输入引用、创建者和租户。原始目标应保持不可变,后续计划只是它的版本化解释。

第二组是执行位置:当前状态、计划版本、step_id、attempt 和最后检查点。它回答系统走到哪里,而不是让模型从最后一句话猜。

第三组是上下文工件:证据、文件、摘要和中间结果的引用与版本。大对象不必全部塞进状态,但必须能按引用重建本轮上下文。

第四组是外部动作:pending intent、工具、参数摘要、业务 operation_id、幂等键和真实回执。这组状态决定一个动作能否安全续跑。

第五组是治理:审批请求、授权主体、预算、deadline、取消信号与补偿状态。审批必须绑定具体计划和参数版本,不能变成永久通行证。

第六组是运行版本:模型、提示、Agent 代码、工具 Schema、策略和状态 Schema。一个运行跨过部署后,系统需要知道它应该继续使用旧语义、迁移,还是停止等待人工处理。

Agent Run State 的六层结构

真正考验设计的,是工具调用周围那几毫秒。

安全顺序应该是先生成唯一的 action intent 和 operation_id,完成策略与审批校验,把 pending action 与检查点持久化,然后才调用外部工具。工具收到同一个业务幂等键,执行后返回可查询回执;系统保存回执,再推进状态。

每个崩溃窗口的恢复语义不同。

意图持久化之前崩溃,外部动作尚未发生,可以重新决策。意图已经保存、工具尚未调用,可以继续执行。工具完成但回执尚未保存,是最危险的 unknown 状态:系统必须拿 operation_id 去外部系统查询,而不是直接重试。回执已经保存、状态归约尚未完成,则只需要重新运行纯状态转换,不能再次调用工具。

所谓 checkpoint,不是随便保存一份 JSON,而是为每个失败窗口定义下一步唯一安全动作。

写操作四个崩溃窗口

这也解释了为什么「从 checkpoint 重放」需要格外小心。

诊断回放只读取事件,回答当时发生了什么。重新执行则会再次调用模型、API 和工具,得到不同结果甚至制造第二次副作用。框架界面里同样叫 replay 的按钮,语义可能完全不同。只要 checkpoint 之后存在付款、发信、删除或发布,恢复策略就必须显式区分读取历史、重新归约和真正重执行。

上下文压缩也不能替代状态。

摘要适合减少 token,却不应成为唯一事实源。它可能省略失败、参数和版本。持久层保存原始事件或工件引用,摘要只是可替换的 context artifact;恢复时根据当前预算重新装配,而不是把一段旧摘要当完整现场。

长时间暂停会带来另一个问题:时间在 Agent 不运行时仍然前进。

审批可能隔了一天才返回,期间订单被别人修改,用户权限被撤销,工具 Schema 已升级。恢复不能只检查「审批通过」,还要重新验证资源版本、权限、计划摘要和 deadline。批准的是昨天那组动作,不是今天所有看起来相似的调用。

部署同样需要版本边界。新的提示词或 reducer 不应在没有迁移的情况下接管旧 checkpoint。状态记录 schema_version;恢复器要么用兼容代码继续,要么执行显式迁移,要么把任务放进 needs_human。序列化失败后从头重跑,是最危险也最常见的「容错」。

不用框架做第一版的价值,就在于这些语义没有地方可以躲。

一个 SQLite 表、一个 append-only event log 和几十行 reducer,已经足够验证核心状态机。状态转换写成显式函数;模型输出先解析为 typed proposal;副作用通过 activity adapter;每次转移保存前置版本和新版本。等这些语义跑通,再选择框架提供 checkpoint、队列、HITL、trace 或分布式执行,团队才知道自己是在复用能力,而不是购买一种模糊的安全感。

评估框架时也就有了具体问题:checkpoint 是每步保存还是最终保存,pending write 如何处理,中断能否跨进程恢复,重放会不会再次执行工具,状态 Schema 如何升级,运行中的旧任务能否绑定旧代码,取消是否向子任务传播。框架文档回答不了的部分,不应被默认当成已经解决。

评测也不该只让正常流程跑通。

建立三组对照:A 组只在内存里循环;B 组保存聊天记录,重启后从最后一条消息继续;C 组保存显式状态、pending intent、receipt、版本和 reconciliation 规则。

挑二十个带写操作或人工审批的任务,在每个边界强制 kill 进程:模型响应后、意图保存前后、工具成功后回执前后、审批等待期间、上下文压缩后和版本部署时。再加入取消、超时和外部状态变化。

比较恢复正确率、丢失进度、重复副作用、unknown 状态解决率、人工恢复次数、MTTR,以及持久化增加的延迟和存储成本。C 组不一定最快,但它应该能让每一种恢复结果被预测、复现和解释。

三种恢复方案的故障实验

上线门槛可以直接从状态机检查:所有非终态都有明确允许转移;每个写工具有 operation_id、幂等或 reconciliation;审批绑定计划版本;checkpoint 能重建上下文;恢复前重新校验权限和外部版本;旧状态有迁移策略;取消与补偿可观测;任何运行都能回答当前状态、最后已确认副作用和下一步安全动作。

回到开头那笔状态不明的退款。

有了显式状态,系统会看到 pending_action 已经持久化,却没有本地 receipt。它不会请模型猜,也不会再次创建退款,而是用同一个 operation_id 查询支付系统。若外部已成功,补记回执并继续;若明确未执行,才在原幂等范围内重试;若仍然未知,进入人工核对,而不是伪装成成功或失败。

框架可以让 Agent 写得更快。

但在决定使用哪个框架之前,团队至少应该亲手写清一次:哪些状态必须活过进程,哪些动作可以重放,哪些结果只能查询,哪些版本不能混用。

当这些问题有了答案,框架才是加速器。

否则,它只是把一个没有恢复语义的循环,画成了一张看起来很完整的图。

作者|随机比特

随机比特公众号二维码
公众号 · 随机比特
从 AI 工具热闹里拆工程真相

写边界、控制面、上下文、成本与安全。