loading

Loading

探索与发现

首页 Agent 工程

Agent工具失败恢复:状态快照粒度怎么选

分类:Agent 工程
字数: (2996)
阅读: (1)
0
摘要:从计划粒度与幂等窗口角度拆解 Agent 工具失败恢复中的状态快照选择和工程代价。

Agent 跑一个多步骤工作流,第 7 步工具调用超时,本地进程崩溃。重启之后是从第 1 步重来,还是从最近一个能对得上的状态继续?这个问题的答案决定了恢复精度,也决定了你修复率与额外开销之间的平衡。这篇文章讨论工程上最实际的一个点:你到底应该把快照切到多细。

快照粒度决定恢复代价

Agent 执行中的快照粒度没有一个统一的正确值。它把恢复精度、存储与写入开销绑在一起。可工程判断的界限大概分三档。

  • 每次工具调用后记录状态能给出最细的恢复点。只要工具调用或审计确认过后就行。直接后果是回放精度高,但写入次数和序列化工作量也最重。适合每一次跳转都有显著副作用、而且延迟外部起作用的调用,比如落库和调用外部 API。
  • 按计划阶段记录状态(块级)把写入量压低。对副作用小、本身就幂等的工具调用,这种粒度尚可容忍从上一个模糊点恢复,甚至能容忍在同一阶段失败一次重新跑同一工具。这对成本低、幂等的调用是常用选择。
  • 按决策点快照最粗糙的可用粒度指的是只在 Agent 根据模型输出做出分支时记录。它的成本最低,并且能保证决定后续路径的那次模型输出不会在恢复时被改掉。代价是决策之间仍会重执行确定性的、或内部循环的成果,对异步活动、人工审核附近不友好。

怎么选,其实就是在比一个代价:重跑一步的 token 或 API 成本,与往日志写一条序列化状态的存储和延迟成本。单步的模型花费远超存储开销时,无悬念地用每次工具调用级别的顺序记录。

完整落盘的一种典型设计:基于日志的顺序恢复

能做到确定性恢复的通用做法是将每个步骤先写出意图、再记录每个动作的结束。在 Agent 架构中一般等同预写日志:加上中间结果、工具调用后的状态、被裁掉的模型输出原文,全部记录在一个只追加、且能在读取时依据检索依据找回来的位置。「把做了的事记下来、先把记录写死,才能做到跳过重复执行」,是数据领域用了几十年的做法。

比如某次运行已经执行了要发给 Slack 或生成某条业务记录的操作,如果前面记录里有什么已经完成。恢复过程读到这段状态就应该标记「不要重发」。这类跳过是状态级别的,不依赖调用了什么模型来推动好行为。对于无法撤回的外部交互,记录并 skip 已写是最硬的兜底边界——仍然不应把消息真的再发一版。

另一方面,模型输出的不确定性在这里也必须对待。同一原文在重启之后重新交给模型,解码结果可能完全不一样。因而恢复不能重新询问运行层下面「模型自己推理」,而应按先前的记录拿回原输出分支。

无法跳过的情况:副作用窗口与幂等键

不过,有日志不等于足够。现实中可能仍然出现写于外部世界的操作已发生,但是,这一行为用仍然落在模型侧工具被发起结果的回写落点时,这里的任何「发生」「延迟写入顺序」。缺口在实际场景里对应的是:工具已经提交或写端已经在日志中发生以后才能跟随,那原预期「一开始就写一个所有流水步骤的可追溯状态。任意系统或输出冲突便不可避免会出现同样的安全提交新范围。要防止这些问题看这部分的主要抵御能力就是用计划层面「这是另个可以匹配的提交痕迹」:这条命令取自同一个工作流 ID 与步骤号派生出的唯一值。某个独立服务如果先前见过一模一样请求发出但因确认位置方式相似不会被重执行第二次。例如使外部链接这条链路的形态就被保留在外部的自然识别元素中。

读 / 写分也要放在工程约束里。对纯查询、资源获取操作按重试无所谓。一个强限制是调用外部写动作,必须支持幂等或换一种规避方式,否则跳过只能在日志侧成立外面不认识也没有意义,就回到了重复调用行为中。要彻底免争议的设计在于让持久化被调用(活动)封装并由运行时接管日志与重试策略中。属于编排本身的逻辑通过 ID 去搜寻并步进确定路径,不直接被允许链接外部过程侧命令。重启之后必须依据输入实际触发外部写入。

要不要引入「一步一个状态」这类代价

这个用不必要在某些运行阶段引入。判断也不难:这条流水跨过 3–5 步以上、步骤具有昂贵或不幂等的副作用、一次执行用时超几分钟、中途仍碰审批或人工介入、失败本身可能带较高经济后果时,它在流水设计里应该当成缺陷自选一次固定。反过来简短文的小聊天不用每步快照,加退避与截断范围的异常就够。不要一口气不碰这种额外记录能更平滑,一个不假思索就把备灾做到自动化不重活小示例工具反而不能体会用户输入部分里的接口。

生产上几个值得抓住的点来自持久执行生态:快照在每次节点完成后将状态固化在具有事务/可靠写入能的系统中,生产环境官方常给 Postgres 这种相对确定的物质层;恢复时回放先前存储值而非静态调用其它模型、而非重新访问对外接口;承载副作用的上下文隔离出去并配合重试。再配合线程 ID 或工作流 ID 取目标状态,设计足够一致。我自己的尝试没有做太多花哨跳转行为 — 它是从几个足够多次复现工具接口失败恢复是清干净到有可测出数据。前网络段内容已证明增量收益能在中断时不再走全部动作,也不靠依赖生产故障循环来证明。

就算引擎最终处理了跨状态断线,保护「操作本身」的责任始终留给你的流水设计者和源服务方:凡是对真实环境发生变化的调用都需要自带唯一含义,否则快照只承担免重放或重放的开始,不自动让已经发生的写入变成一次。

从失败点再回到工序里

快照的粒度其实在回答:在我这次生产在关键语义是跳过什么?应该优先把精确控制压在有昂贵花费的模型调用或对外写出旁边,也就是作在「一次调用算一件事」的条件边界把状态聚成单位。对比真实企业调用应用结果更容易把它转换成索引请求数一致、持久可复用运行。那些曾投入的时间往往来自于工具没有幂等造成一次或一个通过重新跑回来却在账上收取—两次结果与两条记录。若不能连工具错误尝试本身也回滚,不做幂等键就该停下改用前备份式补偿。这也对应实际团队的报告:按这些模式引入后在故障中看到重复副作用的数量下降与成本回收。前提是:不需要在业务可赔而不作调整的边界对变化发起没有记录的逻辑。

# 伪代码:按步提交意图、记录结果,恢复时复用
record = wal.load(workflow_id)
state = restore_or_init(record)
for step in plan[state.next_step:]:
    if state.has_done(step.id):
        tool_output = state.result_of(step.id)   # 确定性重建路径
        continue
    wal.write(Intent(step.id, step.name, args))  # 动作之前写意图
    tool_output = invoke(step, data=step, key=idem_key(workflow_id, step.id))
    wal.write(Outcome(step.id, tool_output))      # 步骤完成后落结果
    state = advance(state, tool_output)

最后的工程取向只有一句:先认定哪些步骤真正不能重复、哪些操作不可撤回,再把快照细到可以做出这个区分。工作流里的快照粒度不是越细越好看,是在写明代价后选给自己团队能长期承受的那一档。

转载请注明出处: 皮哈皮阿

本文的链接地址: https://blog.phpat.com/agent-engineering/677.html

手机扫码阅读

微信或手机浏览器扫一扫,随时随地随心阅读与分享

您可能对以下文章感兴趣
1 / 1
预览图