1. 为什么AI Agent的“出错处理”比让它跑起来更重要
很多人搭AI Agent,第一反应都是怎么把流程跑通:接上大模型、挂几个工具、写个循环让它自己调。跑通那一刻确实爽,但真正上线之后你会发现,跑通只是及格线,出错处理才是决定这套东西能不能用的分水岭。我见过太多demo阶段惊艳的Agent,一进真实环境就翻车——工具返回了脏数据它照单全收,中间步骤失败了它继续往下走,最后产出一堆看似合理实则离谱的结果,你还得从头查是哪一步歪的。
AI Agent和普通程序最大的区别在于:普通程序的执行路径是你写死的,出错会抛异常;而Agent的路径是模型自己规划的,它“觉得”自己没出错,这才是最要命的地方。所以一套能上生产的Agent,必须内建四道防线:校验(Validation)、暂停(Pause)、回滚(Rollback)、人工接管(Human-in-the-loop)。这四个词看着像运维术语,但落到Agent身上,每一个都有它独特的实现逻辑。
这篇文章我打算把这四道防线拆开讲透。适合谁看?如果你已经能搭出一个能跑的Agent,但被它的“不可控”折磨过,或者正准备把Agent从玩具推向真实业务,那这篇就是给你写的。我会讲清楚每一步为什么这么设计、具体怎么落地、参数怎么定,以及我自己踩过的那些坑。全程按从业者交流的口吻来,不整虚的。
先给个整体心智模型,方便你后面理解:把Agent的一次任务执行想象成一次数据库事务。事务有ACID,Agent的可靠性也类似——校验是保证“输入合法”,暂停是“锁住现场别乱动”,回滚是“撤销到一致状态”,人工接管是“DBA介入兜底”。想通这个类比,后面所有设计都顺了。
2. 校验:在Agent动手之前先把脏东西挡在门外
2.1 校验到底在防什么:三类最常见的“脏输入”
校验这个词太泛了,落到Agent场景,你得先明确它防的是什么。我把它归成三类,每一类的处理策略完全不同。
第一类是工具返回值的结构校验。Agent调用一个API或函数,返回的JSON字段缺了、类型错了、嵌套层级变了,模型拿到这种数据会“脑补”出一个合理结果,然后继续往下走。这种错误最隐蔽,因为模型不会报错,它会编。
第二类是语义校验。结构没问题,但内容不合理。比如查询订单返回了金额为负、日期是未来、用户ID对不上。这类错误结构校验抓不到,得靠业务规则。
第三类是前置条件校验。Agent要执行某个动作(比如发邮件、扣款、删文件),执行前必须确认前置状态满足。这一步不做,后面回滚的代价会指数级上升。
提示:校验的黄金法则是“越早拦截,代价越小”。在Agent链路里,入口处一次校验的成本,可能只有事后回滚的百分之一。
2.2 结构校验:用Schema把工具的“嘴”管住
结构校验最省事的做法是给每个工具定义严格的输出Schema。现在主流的大模型工具调用框架基本都支持JSON Schema,你要做的是把required、type、enum这些约束写死,别偷懒。
举个我实际用的例子,一个查询库存的工具,返回Schema我会这么定:
{ "type": "object", "required": ["sku_id", "quantity", "status"], "properties": { "sku_id": {"type": "string", "pattern": "^SKU-[0-9]{8}$"}, "quantity": {"type": "integer", "minimum": 0}, "status": {"type": "string", "enum": ["in_stock", "out_of_stock", "backorder"]} } }注意几个细节:pattern约束了SKU格式,minimum挡住了负数,enum把状态锁死。这三样加起来,能挡掉我遇到过的八成结构类脏数据。校验失败怎么办?不要让模型自己修,直接抛一个明确的错误信息回给Agent,让它重新调用或者走降级路径。让模型“猜”怎么修,是灾难的开始。
2.3 语义校验:业务规则才是真正的守门人
结构对了不代表能用。语义校验得你自己写规则,这块没有银弹。我的经验是把规则分成硬规则和软规则。
硬规则是绝对不能违反的,违反了直接中断。比如金额不能为负、时间戳不能超过当前时间、用户ID必须在白名单里。软规则是“可疑但可能合理”的,比如金额突然比历史均值高10倍,这种不中断,但打标记,触发后面的暂停或人工复核。
实现上我习惯用一个独立的校验层,和工具调用解耦。工具只管返回数据,校验层负责判断。这样规则改了不用动工具代码。校验层返回三种结果:PASS、WARN、BLOCK。WARN会写进执行日志并可能触发暂停,BLOCK直接终止当前分支。
2.4 校验规则怎么维护才不烂尾
校验规则最容易烂尾的地方是:写的时候很全,业务一变就没人更新,最后规则和现实脱节,要么误杀要么漏放。我的做法是给每条规则加元数据:谁负责、什么时候加的、对应哪个业务场景、最近一次触发是什么时候。
用表格管理起来长这样:
| 规则ID | 类型 | 表达式 | 动作 | 负责人 | 最近触发 |
|---|---|---|---|---|---|
| R001 | 硬 | amount >= 0 | BLOCK | 张三 | 2024-05-12 |
| R002 | 软 | amount < avg*10 | WARN | 张三 | 2024-06-01 |
| R003 | 硬 | ts <= now() | BLOCK | 李四 | 从未 |
“最近触发”这一列特别有用。一条规则半年没触发过,要么是业务变了,要么是它根本没生效,两种情况都值得你去查。我靠这个习惯揪出过好几条写错了正则、实际上永远返回PASS的废规则。
3. 暂停:让Agent在关键时刻“停得住”
3.1 暂停不是卡死:区分三种暂停语义
很多人一听“暂停”就想到进程挂起,其实Agent的暂停有三种完全不同的语义,混在一起设计必然出问题。
第一种是协作式暂停:Agent主动在某个检查点停下来,等待外部信号再继续。这是最健康的暂停,因为现场是干净的,状态是明确的。
第二种是强制暂停:外部监控发现异常,命令Agent停下。这时候Agent可能正卡在某个工具调用中间,状态是模糊的,需要额外处理。
第三种是条件暂停:满足某个条件(比如命中软规则、需要人工确认)自动触发。这是业务里最常用的。
我设计暂停机制时,核心原则是:能协作式就别强制,能提前定义检查点就别事后补救。
3.2 检查点设计:暂停的“锚点”在哪
检查点就是Agent执行链路里允许暂停的位置。不是随便哪都能停的,你得在关键节点埋锚点。我的经验是至少在这几个位置埋:
- 每个工具调用之前(准备动手,先停一下确认)
- 每个工具调用之后(拿到结果,停一下校验)
- 跨阶段切换时(比如从“查询”进入“写入”)
- 命中软规则时
检查点要保存完整现场:当前步骤、已完成的步骤、中间变量、模型上下文。这样恢复的时候才能无缝接上。我见过有人只存了个步骤号,恢复时上下文丢了,Agent直接失忆重来,前面白干。
3.3 暂停状态的持久化:别把状态放内存里
这是血泪教训。早期我把Agent的暂停状态放在进程内存里,结果服务一重启,所有暂停的任务全丢了,用户那边显示“处理中”,实际早就没了。暂停状态必须持久化,落到数据库或至少是可靠的文件存储。
我用的结构大概是这样:
class AgentCheckpoint: task_id: str step_index: int status: str # running / paused / failed context: dict # 模型上下文、中间变量 pending_action: dict # 待执行的动作 created_at: datetime resume_token: str # 恢复凭证resume_token这个字段很关键。恢复操作必须带凭证,防止乱恢复。凭证可以是一次性的,恢复后失效,避免重复执行。
3.4 暂停超时与自动降级
暂停不能无限期挂着。用户不响应、人工审核排队太长,任务就僵在那了。所以每个暂停都要设超时,超时后走降级策略。
降级策略我一般分三档:超时5分钟,发提醒;超时30分钟,自动走保守路径(比如放弃当前分支,返回部分结果);超时2小时,标记为失败并归档,释放资源。
注意:超时时间别拍脑袋定。要看你业务的容忍度。支付类可能5分钟就急,内容生成类挂一天也无所谓。这个参数必须和业务方对齐,不能技术自己定。
4. 回滚:把Agent搞乱的东西收拾干净
4.1 回滚的难点:Agent的副作用不像数据库那么好撤
数据库回滚有事务日志,Agent回滚难就难在它的副作用是分散的、外部的、不可逆的。发出去的邮件撤不回,调用的第三方API可能已经改了状态,写进下游系统的数据可能已经被消费了。
所以Agent的回滚设计,第一原则是:能不产生副作用就不产生,能延迟产生就延迟产生。把副作用集中到执行链路的最后阶段,前面全是只读操作。这样一旦出错,回滚成本极低——因为压根还没写。
4.2 补偿事务:为每个写操作准备一个“撤销动作”
对于必须产生的副作用,用**补偿事务(Compensating Transaction)**模式。每个写操作都配一个对应的撤销操作。发邮件配一个“发更正邮件”,扣款配一个“退款”,创建记录配一个“软删除”。
补偿动作要在设计阶段就定义好,不能等出错了现想。我习惯在工具定义里直接带上补偿方法:
class Tool: def execute(self, params): ... def compensate(self, result): ... def is_idempotent(self): ...is_idempotent这个标记很重要。幂等的操作重试安全,非幂等的重试会出大事。回滚时优先用幂等操作,非幂等的必须配合去重键。
4.3 回滚的粒度:全量回滚还是部分回滚
不是所有错误都要全量回滚。有时候前面几步是对的,只有最后一步错了,全量回滚反而浪费。我的策略是按检查点回滚:回到最近一个干净的检查点,而不是回到起点。
判断“干净检查点”的标准是:该点之后没有产生不可逆副作用,且状态一致。如果中间有不可逆操作,那就只能全量回滚加人工介入。
这里有个坑:回滚本身也可能失败。补偿动作调用第三方API,第三方挂了怎么办?所以回滚要有重试和告警。回滚失败的任务必须进人工队列,不能静默吞掉。
4.4 回滚日志:事后复盘的唯一依据
每次回滚都要记详细日志:回滚了什么、为什么回滚、补偿动作执行结果、有没有残留。这份日志是事后复盘的唯一依据。
我用的日志结构:
| 字段 | 说明 |
|---|---|
| task_id | 任务标识 |
| rollback_from | 从哪个检查点回滚 |
| reason | 触发原因 |
| actions | 执行的补偿动作列表 |
| result | 成功/部分成功/失败 |
| residue | 残留的不可逆副作用 |
residue这一列最容易被忽略,但最重要。它明确告诉你“这次回滚没收拾干净的地方在哪”,人工介入时直接看这一列就行。
5. 人工接管:最后的兜底,也是最该设计好的一环
5.1 什么时候该交给人:接管触发条件
人工接管不是失败,是设计的一部分。关键是什么时候交。交早了浪费人力,交晚了损失扩大。我的触发条件清单:
- 命中硬规则且无法自动修复
- 回滚失败或回滚后有残留
- 暂停超时且降级策略也无法处理
- 连续N次重试失败(N一般设3)
- 涉及高风险操作(大额、不可逆、对外)
这些条件要可配置,不同业务线阈值不同。别写死在代码里。
5.2 接管界面:让人能快速看懂Agent干了什么
人工接管最大的痛点是信息不对称:人接手时不知道Agent前面干了啥。所以接管界面必须把执行链路、中间状态、出错点、已产生的副作用一次性呈现清楚。
我设计的接管面板包含四块:执行时间线(每步做了什么)、当前状态快照、错误详情、可执行的操作按钮(继续、回滚、终止、修改后继续)。操作按钮要带二次确认,尤其是回滚和终止。
5.3 接管后的状态回写:别让Agent“失忆”
人工处理完之后,结果要回写到Agent的状态里,让它知道“这一步已经被人处理过了”。否则Agent恢复后可能重复执行。回写内容包括:处理人、处理时间、处理动作、最终结果。
这块我踩过坑:早期没做回写,人工处理完Agent又跑了一遍,重复发了通知。后来加了human_override标记,Agent看到这个标记就跳过对应步骤。
5.4 人工接管的成本控制:别让它变成常态
人工接管是兜底,不是常规流程。如果接管率超过某个阈值(我一般设5%),说明前面的校验、暂停、回滚设计有问题,得回头优化,而不是加人手。
我会定期统计接管率、接管原因分布、平均处理时长。接管原因集中在某一类,就去优化那一类的自动处理能力。这是个持续迭代的过程,不是一次设计就完事。
6. 四道防线怎么串起来:一个完整的执行框架
6.1 执行主循环:校验-执行-暂停-回滚的编排
把四道防线串起来,主循环大概长这样:
def run_agent(task): checkpoint = load_or_create(task) while not checkpoint.done: action = plan_next(checkpoint) if not validate(action, checkpoint): handle_validation_failure(checkpoint) continue if should_pause(checkpoint): save_checkpoint(checkpoint, status="paused") return # 等待恢复 result = execute(action) if not validate_result(result): rollback_to_last_clean(checkpoint) continue checkpoint.advance(result) return checkpoint.output核心是每一步都过校验,每个检查点都可暂停,出错能回滚。这个骨架不复杂,难的是每个环节的细节。
6.2 状态机视角:把Agent执行建模成状态迁移
更严谨的做法是把整个执行建模成状态机。状态包括:INIT、RUNNING、PAUSED、ROLLING_BACK、HUMAN_TAKEOVER、DONE、FAILED。迁移条件明确,每个状态的行为明确。
状态机的好处是不会出现“既暂停又回滚”这种模糊状态。任何时刻Agent只处于一个明确状态,排查问题时一目了然。我用状态机重构过一次,之前那些“任务卡住不知道在哪”的问题基本消失了。
6.3 可观测性:没有日志和指标,四道防线都是摆设
最后强调一点:四道防线要生效,前提是你能看见它们在工作。校验命中率、暂停次数、回滚成功率、接管率,这些指标必须采集和展示。
我用的最小指标集:
| 指标 | 含义 | 告警阈值 |
|---|---|---|
| validation_block_rate | 校验拦截率 | >20% 需排查 |
| pause_timeout_rate | 暂停超时率 | >10% 需优化 |
| rollback_failure_rate | 回滚失败率 | >1% 立即告警 |
| human_takeover_rate | 接管率 | >5% 需优化 |
没有这些数字,你根本不知道防线是在挡子弹还是在漏风。
7. 我踩过的坑和几条实在的经验
7.1 校验别过度:误杀比漏放更伤
早期我把校验写得太严,结果大量正常请求被拦,用户体验极差。后来学乖了:硬规则只留真正致命的,其余全降级成软规则加告警。先观察一段时间,确认某类问题确实高发,再升级成硬规则。宁可漏放几个,也别误杀一片。
7.2 暂停状态一定要能“看见”
暂停的任务如果不可见,就等于丢了。我现在的做法是每个暂停任务都在管理后台有明确展示,带倒计时和负责人。谁的任务谁负责,超时自动升级。这个机制上线后,暂停任务的平均处理时长从几小时降到十几分钟。
7.3 回滚要演练,不能等真出事才第一次跑
补偿逻辑最容易写错,因为平时不触发。我强制要求每个补偿动作上线前必须演练:手动构造错误,跑一遍回滚,确认干净。演练过的回滚和没演练过的,成功率差一个数量级。
7.4 人工接管界面要“傻瓜化”
接管的人往往不是原开发者,可能是运营或客服。界面必须傻瓜化:一眼看懂发生了什么,一键执行推荐操作。我见过太多接管界面做得像调试工具,结果没人会用,接管形同虚设。
7.5 四道防线是迭代出来的,不是设计出来的
别指望一次设计就完美。我的做法是先上最基础的校验和暂停,跑一段时间看数据,哪里漏了补哪里。回滚和接管在业务真正需要时再加。过度设计比设计不足更常见,很多团队一开始就搞复杂的状态机,结果维护成本高到没人敢改。
最后分享一个我一直在用的小技巧:给每个Agent任务生成一个人类可读的执行摘要,出错时直接看摘要就能定位问题,不用翻日志。这个摘要用模型生成,成本极低,但排查效率提升巨大。踩过几次坑之后,我现在所有Agent项目第一件事就是把这个摘要机制搭起来。