1. 从一次线上事故说起:为什么 Agent 的可靠性问题被严重低估了
去年冬天,我负责的一个多 Agent 协作系统在凌晨两点崩了。不是那种干脆利落的崩溃,而是"半死不活"——任务队列里堆着三百多个待处理请求,每个 Agent 都显示"运行中",但没有任何一个真正在推进工作。日志里翻来覆去就是那几行:agent execution terminated due to error、the agent execution provider did not respond in time。重启服务之后,问题暂时消失,但三天后又卷土重来。
那次事故让我彻底意识到一件事:我们一直在用"请求-响应"的思维去构建 Agent 系统,但 Agent 的本质根本不是一次 API 调用,而是一个长期运行的分布式状态机。
这个认知转变非常关键。当你把 Agent 当成一个函数——输入问题、输出答案——你会觉得它跟普通的微服务没什么区别,加个重试、加个超时、加个熔断就完事了。但当你把 Agent 当成一个可能运行几分钟、几小时甚至几天的状态机,它要维护记忆、要调用工具、要跟其他 Agent 协作、要在失败后恢复现场,问题的性质就完全变了。
这篇文章我想聊的就是这个:Agentic AI 基础设施的可靠性到底难在哪里,以及我们该怎么系统性地解决它。不管你是正在搭建第一个 AI Agent 的开发者,还是已经在维护多 Agent 协作平台的工程师,这些从实战里踩出来的经验应该都能帮到你。我会从架构设计、状态管理、故障恢复、可观测性几个维度展开,尽量把每个"为什么"讲透,而不是只丢一堆结论。
2. 重新理解 Agent:它不是函数,是状态机
2.1 函数式思维带来的三个致命误判
大部分 Agent 框架的入门文档都在教你写这样的代码:给 Agent 一个任务,它思考、调用工具、再思考,最后返回结果。这个心智模型太像普通的 API 调用了,于是我们自然而然地套用了微服务的那套可靠性方案。但这里藏着三个致命误判。
第一个误判是"执行时间是秒级的"。普通 API 调用通常几十到几百毫秒,超时设个 5 秒绰绰有余。但一个复杂的 Agent 任务,比如"帮我调研这个行业的竞争格局并生成报告",可能要调用十几次工具、做几十轮推理,执行时间轻松突破几分钟。你按秒级超时去设计,Agent 会在正常工作时被误杀。
第二个误判是"执行是无状态的"。函数调用完了就完了,下次调用从零开始。但 Agent 有记忆——短期记忆是当前对话上下文,长期记忆是向量库里的历史经验,永久记忆可能是写进数据库的用户偏好。这些状态在 Agent 运行过程中不断变化,一旦进程崩溃,状态就丢了一半。
第三个误判是"失败可以简单重试"。普通接口失败了重试一次没副作用。但 Agent 重试可能意味着:重复调用了一个有副作用的工具(比如已经发了邮件、已经扣了款)、重复消耗了大量 token、或者在一个已经部分完成的任务上从头再来。重试的代价完全不是一个量级。
2.2 状态机视角下的 Agent 生命周期
换个视角,把 Agent 看成一个状态机,它的生命周期大致是这样的:初始化 → 规划 → 执行(循环:思考-调用工具-观察结果)→ 收敛 → 完成/失败。每个状态之间的转换都可能出问题,而且很多问题是"卡在中间"——既没成功也没失败,就那么挂着。
我习惯把 Agent 的状态分成三类来管理:
| 状态类型 | 具体内容 | 存储位置 | 丢失后果 |
|---|---|---|---|
| 瞬时状态 | 当前推理步骤、临时变量 | 内存 | 可重建,影响小 |
| 会话状态 | 对话历史、工具调用记录 | Redis/内存 | 需恢复,影响中 |
| 持久状态 | 长期记忆、任务进度、副作用记录 | 数据库 | 不可丢,影响大 |
这个分类直接决定了你的持久化策略。瞬时状态丢了无所谓,会话状态要能快速恢复,持久状态必须保证不丢。很多团队出问题,就是把该持久化的东西放在了内存里,进程一重启全没了。
2.3 为什么"分布式"是绕不开的
单机 Agent 已经够复杂了,为什么还要分布式?因为现实需求逼着你这么做。一个 Agent 要处理的任务可能超出单机算力,需要多个 Agent 并行协作;Agent 要调用的工具可能分布在不同的服务里;为了高可用,你不能让所有 Agent 跑在一台机器上。
但分布式一上来,问题就指数级放大。网络分区、时钟漂移、消息乱序、部分失败——这些分布式系统的经典难题,在 Agent 场景下全部重现,而且因为 Agent 执行时间长、状态复杂,处理起来更棘手。比如两个 Agent 协作完成一个任务,A 把子任务派给 B,B 执行到一半网络断了,A 不知道 B 到底做没做,这时候 A 该重试还是该等待?这就是典型的分布式状态机一致性问题。
3. 核心难点拆解:Agent 可靠性到底难在哪
3.1 长时运行带来的状态一致性挑战
Agent 运行时间长,意味着它跨越的时间窗口里,外部环境可能已经变了。举个具体的例子:Agent 在规划阶段读取了库存数据,决定下单 100 件商品,然后它花了 3 分钟做其他推理,等真正执行下单时,库存可能已经被别人抢光了。这就是典型的读-改-写竞态,在长时运行的 Agent 里特别常见。
解决思路有两种。一种是乐观锁:执行前再校验一次状态,不一致就重新规划。另一种是预留机制:规划阶段就把资源锁定,执行阶段直接用。前者实现简单但可能频繁重规划,后者一致性强但会降低并发。我的经验是,对于副作用大的操作(下单、支付、发消息)用预留机制,对于只读操作或幂等操作可以用乐观锁。
还有一个更隐蔽的问题:状态版本漂移。Agent A 和 Agent B 共享一份任务状态,A 更新了状态但 B 还在用旧版本,两边基于不同版本做决策,最后冲突。这个在分布式系统里靠版本号(version)或向量时钟(vector clock)解决,但 Agent 场景下很多人压根没考虑这层。
3.2 工具调用的不确定性:超时、幂等与副作用
Agent 调用工具是整个系统里最脆弱的一环。工具可能是外部 API、可能是数据库、可能是另一个 Agent,任何一个环节出问题都会传导上来。我整理过我们系统里工具调用失败的分布,大致是这样的:
- 网络超时:约 40%
- 工具本身报错:约 30%
- 参数错误:约 15%
- 权限/配额问题:约 10%
- 其他:约 5%
超时是最常见的,但超时最麻烦的地方在于你不知道对方到底执行了没有。请求发出去了,超时了,可能对方根本没收到,也可能对方收到了正在执行,也可能对方执行完了但响应丢了。这三种情况处理方式完全不同。
所以工具调用必须区分幂等和非幂等。幂等的工具(查询、计算)超时了直接重试没问题。非幂等的工具(下单、发消息)必须配合去重键(idempotency key),让工具方能够识别重复请求。我们给每个非幂等调用生成一个全局唯一的 request_id,工具方收到后先查这个 id 有没有处理过,处理过就直接返回上次结果。
提示:设计工具接口时,把"是否幂等"作为必填元数据。Agent 在决定重试策略时,先看这个标记,非幂等的绝不盲目重试。
3.3 多 Agent 协作中的死锁与活锁
多 Agent 协作最容易出的问题是死锁和活锁。死锁是大家互相等,谁也不动;活锁是大家都在动,但一直在做无用功,任务永远推进不了。
我遇到过一个典型的死锁场景:Agent A 等 Agent B 的输出才能继续,Agent B 等 Agent A 释放某个资源才能开始,两边就这么僵住了。这种在传统并发编程里靠资源排序解决,在 Agent 协作里则需要明确的依赖图和超时机制。每个 Agent 等待其他 Agent 时,必须设一个最大等待时间,超时后要么降级处理,要么上报给协调者。
活锁更隐蔽。比如两个 Agent 都在尝试解决同一个冲突,A 改一下、B 改回来、A 再改、B 再改回来,来回拉锯。这种要靠冲突解决策略——要么指定优先级,要么引入随机退避,要么让一个中立的协调 Agent 来裁决。
3.4 记忆系统的读写竞争与污染
Agent 的记忆系统是可靠性的重灾区。短期记忆(上下文)还好,主要是长度管理问题。长期记忆(向量库)就麻烦了:多个 Agent 同时读写同一个记忆库,可能出现写覆盖、读到脏数据、记忆污染。
记忆污染特别值得说。如果 Agent A 往记忆库里写了一条错误信息,Agent B 读到之后基于它做了错误决策,又把新的错误信息写回去,错误就会像病毒一样扩散。我们后来加了一个记忆写入审核机制:重要的记忆写入前要经过校验(比如跟已有记忆做一致性检查),冲突的记忆标记为待人工确认,而不是直接覆盖。
4. 可靠性基础设施的搭建:从持久化到可观测
4.1 状态持久化:选对存储,设计对模型
状态持久化的第一原则是按状态类型选存储,不要一刀切。我们的实践是这样的:
- 瞬时状态:放内存,进程内变量,不持久化
- 会话状态:放 Redis,设 TTL,支持快速读写和过期清理
- 持久状态:放关系型数据库(PostgreSQL),强一致,支持事务
- 向量记忆:放专用向量库,配合元数据过滤
关键设计点是状态快照(snapshot)。Agent 每完成一个关键步骤,就把当前完整状态序列化存一份。这样进程崩溃后,可以从最近的快照恢复,而不是从头再来。快照的粒度要权衡:太频繁影响性能,太稀疏恢复代价大。我们的经验是每个工具调用前后各存一次,因为工具调用是状态变化最剧烈的点。
序列化格式我推荐用 JSON 或 MessagePack,别用 pickle 这类跟语言绑定的格式。原因很简单:Agent 系统往往是多语言混布的,Python 写的 Agent 可能要把状态交给 Go 写的协调器,格式必须跨语言。
4.2 幂等设计:让重试变得安全
幂等是可靠性的基石。我前面提了去重键,这里展开讲讲怎么落地。核心思路是:每个会产生副作用的操作,都要有一个全局唯一的标识,并且操作方要记录这个标识的处理状态。
具体实现上,我们建了一张operation_log表,字段包括request_id、operation_type、status(pending/success/failed)、result、created_at。工具方收到请求后:
- 用
request_id查表 - 如果记录存在且 status=success,直接返回缓存的 result
- 如果记录存在且 status=pending,说明有并发请求,返回"处理中"
- 如果不存在,插入一条 pending 记录,执行业务逻辑,完成后更新为 success
这套机制配合数据库的唯一索引,能保证即使请求重复到达,副作用也只发生一次。代价是每次操作多一次数据库读写,但对于非幂等操作来说,这个代价完全值得。
4.3 超时、重试与熔断的协同策略
超时、重试、熔断这三个机制必须协同设计,单独用任何一个都会出问题。我见过太多团队只设了重试没设熔断,结果下游服务挂了,上游疯狂重试把下游彻底压垮。
我们的策略是这样的:
| 机制 | 触发条件 | 动作 | 参数建议 |
|---|---|---|---|
| 超时 | 单次调用超过阈值 | 中断调用 | 按 P99 延迟的 2-3 倍设 |
| 重试 | 超时或可重试错误 | 重新调用 | 指数退避,最多 3 次 |
| 熔断 | 错误率超过阈值 | 快速失败 | 错误率>50% 持续 30s 触发 |
重试一定要用指数退避加抖动(exponential backoff with jitter)。固定间隔重试会让所有失败的请求在同一时刻再次涌向下游,形成"重试风暴"。加抖动就是让每个请求的退避时间随机化,错开重试高峰。
熔断的状态机是"关闭→打开→半开"三态。关闭时正常放行,错误率超标后打开(直接拒绝),过一段时间进入半开(放少量请求试探),试探成功就关闭,失败就继续打开。这个在 Agent 场景下特别重要,因为 Agent 的重试逻辑往往比普通服务更激进。
4.4 可观测性:让 Agent 的"黑盒"变透明
Agent 最难调试的地方就是它像个黑盒——你只看到输入和输出,中间发生了什么全靠猜。可观测性就是要把这个黑盒打开。三个支柱:日志、指标、追踪。
日志要结构化,每条日志带上trace_id、agent_id、step、tool_name这些字段,方便串联。指标要覆盖几个关键维度:任务成功率、平均执行时长、工具调用失败率、token 消耗、重试次数。追踪(tracing)最关键,用 OpenTelemetry 这类标准把一次 Agent 执行的完整链路串起来,每个工具调用是一个 span,能清楚看到时间花在哪。
我特别想强调**执行轨迹(execution trace)**的价值。把 Agent 的每一步思考、每次工具调用、每个观察结果都记录下来,出问题时可以完整回放。我们系统里有个"事故回放"功能,输入 trace_id 就能看到那次失败执行的完整过程,定位问题效率提升了好几倍。
5. 实操落地:一套可复现的 Agent 可靠性方案
5.1 环境与依赖准备
假设你要搭建一套带可靠性保障的 Agent 系统,我按我们的技术栈给个参考配置。语言用 Python(生态最全),框架可以用 LangGraph 或自己写状态机,存储用 PostgreSQL + Redis,向量库用 pgvector(省得再维护一套),可观测性用 OpenTelemetry + Prometheus + Grafana。
依赖清单大致是:
pip install langgraph psycopg2-binary redis pgvector opentelemetry-api \ opentelemetry-sdk opentelemetry-instrumentation prometheus-client \ tenacity pydantictenacity用来做重试,pydantic用来做状态模型校验,这两个是可靠性方案里的关键工具。别小看 pydantic,状态序列化/反序列化时它能帮你挡住大量脏数据。
5.2 状态机骨架的实现
Agent 状态机的核心是一个显式的状态定义和转换逻辑。我用 pydantic 定义状态模型:
from pydantic import BaseModel from enum import Enum from typing import Optional, List class AgentStatus(str, Enum): INIT = "init" PLANNING = "planning" EXECUTING = "executing" WAITING_TOOL = "waiting_tool" CONVERGING = "converging" DONE = "done" FAILED = "failed" class AgentState(BaseModel): agent_id: str task_id: str status: AgentStatus step: int = 0 context: List[dict] = [] pending_tool_call: Optional[dict] = None version: int = 0 last_snapshot_at: Optional[str] = None注意version字段,这是乐观锁用的。每次状态更新前先读当前 version,更新时带上WHERE version = ?,更新成功 version 加一,失败说明有并发冲突,需要重新读取再试。
状态转换函数要保证幂等:同样的输入状态,执行多次结果一致。这样即使恢复时重复执行某个转换,也不会出问题。
5.3 工具调用的幂等封装
工具调用的封装是可靠性的关键。我写了一个装饰器,自动处理幂等、重试、超时:
import uuid from tenacity import retry, stop_after_attempt, wait_exponential_jitter def reliable_tool(idempotent: bool = False, timeout: int = 30): def decorator(func): @retry( stop=stop_after_attempt(3), wait=wait_exponential_jitter(initial=1, max=10), reraise=True ) def wrapper(*args, **kwargs): request_id = kwargs.pop("request_id", None) or str(uuid.uuid4()) if not idempotent: # 非幂等操作,先查 operation_log existing = query_operation_log(request_id) if existing and existing["status"] == "success": return existing["result"] result = func(*args, **kwargs, timeout=timeout) if not idempotent: save_operation_log(request_id, "success", result) return result return wrapper return decorator这个装饰器把幂等检查、重试、超时都包进去了,业务代码只管写逻辑。注意wait_exponential_jitter就是指数退避加抖动,比固定间隔重试稳得多。
5.4 故障恢复的完整流程
故障恢复的流程我画不出图(这里不用图表),用文字描述清楚。假设 Agent 在WAITING_TOOL状态时进程崩溃了,恢复流程是:
- 加载最近快照:从数据库读出该 agent_id 的最新快照,恢复状态
- 检查 pending_tool_call:如果快照里有未完成的工具调用,先查 operation_log 确认它到底执行了没有
- 决定恢复动作:如果工具已成功,把结果填回 context,状态推进到 EXECUTING;如果工具未执行或失败,重新发起调用
- 版本校验:恢复时检查 version 是否跟数据库一致,不一致说明有其他进程改过,需要合并或放弃
- 继续执行:从恢复的状态继续跑,而不是从头开始
这套流程的关键是快照 + operation_log 双保险。快照告诉你"崩溃前在干什么",operation_log 告诉你"那个操作到底成没成"。两者结合才能准确恢复。
5.5 多 Agent 协作的协调机制
多 Agent 协作我用一个**协调者(orchestrator)**模式。协调者不干具体活,只负责:分配任务、监控进度、处理超时、解决冲突。每个工作 Agent 定期向协调者心跳,协调者维护一张全局的任务状态表。
协调者的核心逻辑是超时重分配:如果某个 Agent 超过预期时间没汇报进度,协调者把它标记为可疑,派另一个 Agent 去查证或接管。这里要注意避免重复执行——接管前必须确认原 Agent 真的挂了,而不是只是慢。我们的做法是让原 Agent 和接管 Agent 都往 operation_log 写,靠幂等机制保证副作用只发生一次。
6. 常见问题与排查技巧实录
6.1 典型故障速查表
我把我们踩过的坑整理成一张速查表,遇到问题先对号入座:
| 现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| Agent 卡在运行中不动 | 工具调用超时未处理 | 查 trace 看卡在哪个 span | 加超时+熔断 |
| 任务重复执行 | 重试无幂等保护 | 查 operation_log 有无重复 | 加去重键 |
| 状态丢失 | 该持久化的放内存了 | 查快照是否完整 | 补持久化 |
| 多 Agent 互相等待 | 依赖环 | 画依赖图找环 | 加超时+协调者 |
| 记忆读到脏数据 | 并发写覆盖 | 查记忆写入时序 | 加版本+审核 |
| token 消耗异常 | 重试风暴 | 查重试次数指标 | 加退避+熔断 |
6.2 三个最容易踩的坑
第一个坑:把超时设得太短。我们一开始按普通 API 的标准设了 5 秒超时,结果 Agent 调用大模型推理经常超时,触发大量无谓重试。后来改成按 P99 延迟的 2-3 倍设,误杀率大幅下降。教训是:Agent 的超时阈值必须基于实际延迟分布来定,不能拍脑袋。
第二个坑:重试没有区分错误类型。不是所有错误都值得重试。参数错误、权限错误重试一万次也没用,只会浪费资源。我们后来给错误分了类,只有网络类、超时类、限流类错误才重试,其他直接失败上报。
第三个坑:可观测性做得太晚。我们前两个月基本靠 print 调试,出了线上问题两眼一抹黑。后来补上 tracing 才发现,很多问题早就该被发现。可观测性不是锦上添花,是可靠性的基础设施,应该第一天就搭。
6.3 独家避坑技巧
分享几个文档里不会写、但实战中特别有用的技巧。
技巧一:给 Agent 加"心跳"和"看门狗"。Agent 每执行一步就更新一次心跳时间戳,看门狗进程定期扫描,发现心跳超时的 Agent 就标记为可疑并触发恢复。这个机制能自动发现"卡死"的 Agent,比等用户报障快得多。
技巧二:用"影子执行"验证恢复逻辑。恢复逻辑最怕的是"以为恢复了其实没恢复对"。我们的做法是定期做故障演练:随机 kill 一个 Agent 进程,看它能不能正确恢复。这种混沌工程(chaos engineering)的思路,能提前暴露恢复逻辑的 bug。
技巧三:状态快照加校验和。快照序列化时算一个 checksum 一起存,恢复时先校验 checksum,不一致说明快照损坏,直接放弃用更早的快照。这个能防止"恢复出一个损坏状态"这种更隐蔽的问题。
技巧四:给记忆写入加"冷却期"。重要记忆写入后不立即生效,而是进入一个短暂的冷却期,期间可以被修正或撤回。这能有效防止错误记忆快速扩散。
7. 关于 Agent 可靠性,我的一些个人体会
做 Agent 基础设施这两年,我最大的体会是:可靠性不是加出来的,是设计出来的。你不可能在一个没考虑幂等的系统上事后补一个重试机制就变可靠,也不可能在一个状态管理混乱的系统上事后加个监控就万事大吉。可靠性必须从架构设计的第一天就融进去。
另一个体会是,Agent 的可靠性问题,本质上是分布式系统问题的一个新变种。那些经典的分布式难题——一致性、幂等、故障恢复、可观测性——在 Agent 场景下全部重现,只是披了一层"AI"的外衣。所以别被"Agentic AI"这个词唬住,把分布式系统那套成熟的方法论搬过来,能解决 80% 的问题。
最后说个具体的:别追求 100% 可靠,追求"可恢复"。任何分布式系统都不可能不出故障,关键是出了故障能快速恢复、不丢数据、不产生错误副作用。把精力放在恢复机制上,比追求"永不失败"务实得多。我们系统现在的目标不是零故障,而是"任何单点故障都能在 30 秒内自动恢复",这个目标实际得多,也达成了。
如果你正在搭建 Agent 系统,我的建议是从第一天就把状态持久化、幂等设计、可观测性这三件事做扎实。这三件事做对了,后面加什么功能都不会太慌;这三件事没做,后面每加一个功能都是在给自己挖坑。