当你把 Agent 从“自己用”变成“多人用”,故障通常会以一种非常不体面的方式出现:
- 同一时间 50 个请求打到模型/工具,开始 429(rate limit)或超时。
- 你加了重试,结果把对方 API 打挂,自己的队列也被挤爆。
- 多个并发请求写同一个会话状态,记忆被覆盖,回答开始串台。
这篇文章不讲“多买服务器”,只讲能落地的工程解法:把 Agent 当成线上系统,围绕失败设计。
延伸阅读(与本篇强相关):
零、先看一个最常见的线上事故:429 之后的重试风暴
并发系统最容易被低估的一类故障,不是“请求太多”本身,而是请求太多以后你做了错误恢复,结果把系统从“有点慢”推成“全面失控”。
0.1 事故现象
一个客服 Agent 在工作日上午 10 点迎来流量高峰:
- 同时在线用户从 20 增长到 130
- 每个请求平均会触发 1 次模型调用 + 2 次工具调用
- 上游模型供应商开始返回 429
这时系统启动了“简单重试”:每次失败后 1 秒重试,最多 3 次。
结果 2 分钟后出现了三个连锁现象:
- 原始请求还没处理完,重试请求已经堆满队列
- 工具服务被短时间内重复打爆,开始出现 5xx
- 同一会话的后续问题被排在旧请求后面,用户感知成“卡死”
0.2 根因定位
这类事故的根因通常不止一个,而是多层叠加:
- 入口层:没有按用户、会话、任务类型做分级限流
- 重试层:固定间隔重试,没有指数退避和抖动
- 队列层:轻任务和重任务混在一个队列里,无法隔离拥塞
- 依赖层:模型与工具的失败没有触发熔断,系统持续向坏依赖施压
- 状态层:同会话并发请求没有串行,等待时间拉长后更容易出现状态竞争
0.3 修复策略
真正有效的修复一般是组合拳,而不是只改一个参数:
- 会话级串行:同一
conversation_id默认排队,先止住状态竞争 - 指数退避 + 抖动:把
1s/1s/1s改成退避重试,避免同一时刻回潮 - 重任务拆队列:长任务与轻任务分层,保证基础可用性
- 熔断坏依赖:连续失败达到阈值后短时拒绝调用,只放探活请求
- 明确降级:上游不可用时返回“稍后继续”或“先给人工步骤”,而不是无穷等待
0.4 回归测试怎么做
要证明你真的修好,至少做这几类注入测试:
- 人工注入 429,验证重试不会在 1 秒内集体回潮
- 压测轻任务与重任务混跑,验证轻任务仍能稳定返回
- 模拟某个工具持续 5xx,验证熔断会在阈值后打开
- 同一会话并发提交 3 个请求,验证状态不串台
0.5 一组可靠性指标口径
这篇文章后面会反复用到这些指标,你应该尽早固定定义:
| 指标 | 说明 | 常见问题信号 | 目标方向 |
|---|---|---|---|
p95_e2e_latency | 端到端 95 分位时延 | 高峰时明显拉长 | 稳定受控 |
tool_failure_rate | 工具调用失败比例 | 某依赖抖动时快速升高 | 可回落 |
queue_wait_time | 请求在队列中的等待时间 | 高峰拥塞、任务混跑 | 对轻任务可控 |
degrade_rate | 被降级处理的比例 | 依赖异常时显著上升 | 可观测、可解释 |
你不需要一开始就有完美基线,但必须能把“失败发生在哪里”说清楚。
一、先把“并发”说清楚:你在同时扛哪些东西?
在 Agent 系统里,并发不是一个维度,而是至少四个并发面:
- 用户并发:同时多少人发请求。
- 步骤并发:一个请求内部可能并发调用多个工具/检索。
- 外部依赖并发:模型供应商、向量库、业务 API、文件系统、数据库。
- 状态并发:同一会话多请求同时读写状态(最容易串台)。
你做可靠性设计的目标不是“永不失败”,而是:
- 失败可控(不扩散)
- 恢复可预期(可重试且幂等)
- 体验可接受(降级而不是崩溃)
- 过程可观测(能定位、能回放、能复盘)
二、第一层:入口限流与排队(别让系统一上来就炸)
2.1 不要把所有请求直接打到模型
模型/工具通常是最贵、最慢、最容易被限流的依赖。你需要“入口治理”:
- 全局限流:保护供应商配额
- 用户级限流:防止单个用户打爆系统
- 会话级串行:同一会话默认串行处理,避免状态竞争
2.2 一个可落地的三段式队列
你可以按成本和优先级拆三层:
- 快速路径(Fast path):能在 1~2 秒内完成的(纯检索/缓存命中/无需 LLM)。
- 标准路径(Normal):一次 LLM + 少量工具调用。
- 重任务路径(Heavy):多步规划、长文生成、多工具并发。
核心思想:别让重任务挤占轻任务的可用性。
2.3 一张文字版并发流程图:从入口到恢复的主链路
如果你要把可靠性架构讲清楚,下面这张文字版流程图足够当技术说明的主干:
REQUEST
-> AUTH / RATE_LIMIT
-> CLASSIFY_WORKLOAD
-> FAST_QUEUE | NORMAL_QUEUE | HEAVY_QUEUE
-> SESSION_SERIALIZE
-> EXECUTE_AGENT
-> MODEL_CALL
-> TOOL_CALL
-> STATE_WRITE
-> VALIDATE
-> IF FAIL: RETRY_OR_DEGRADE
-> OBSERVE
-> METRICS
-> TRACE
-> ALERT
这张图的重点不是“画得好看”,而是让你清楚每个故障应该归到哪一层处理。
三、第二层:超时、重试与幂等(重试不是“再来一次”)
3.1 超时要分层,不要只设一个大 timeout
- 模型调用:例如 30s(带 streaming 可更长,但必须有心跳)
- 检索/向量库:例如 2~5s
- 业务 API:例如 3~10s(看 SLA)
超时的意义是:把“坏请求”尽快变成可控失败,释放资源。
3.2 重试的正确姿势:指数退避 + 抖动 + 预算
重试至少要满足:
- 指数退避:$1s, 2s, 4s...$
- 抖动(jitter):避免所有请求同一时刻重试
- 重试预算:每个请求最多重试 N 次,或总重试时长不超过阈值
3.3 幂等是底线:否则重试会制造“重复副作用”
凡是可能产生副作用的工具调用(下单、发邮件、写数据库),必须有幂等键:
idempotency_key = user_id + conversation_id + step_id + request_id
当你在面试里讲可靠性,幂等通常是“工程味”的关键加分点。
这部分如果你想进一步细化到工具网关和写操作治理,可以直接接着看:
四、第三层:会话隔离与并发一致性(串台是致命体验)
4.1 同一会话并发的两种安全策略
策略 A:会话级排队(推荐起步)
- 同一
conversation_id的请求排队串行执行 - 代价:同一会话的并发吞吐低
- 收益:最简单、最稳定、不串台
策略 B:乐观锁 + 合并(高阶)
- 状态带
version - 写入时检查版本;冲突则合并或回滚重放
- 代价:复杂
- 收益:可以并发,但要解决“状态合并语义”
如果你还没有成熟的状态机,不建议直接上 B。
4.2 把“状态”当成数据库记录,而不是 prompt 文本
你应该能回答:
- 状态存在哪里(DB/Redis)
- 状态 schema 是什么(字段级)
- 每轮请求更新哪些字段
- 如何回放一条会话的状态演进
配合结构化输出,你可以把“状态更新”做成可校验协议:
五、第四层:降级、熔断与体验兜底(让系统“难用但可用”)
当外部依赖不稳定时,你要有明确降级路径:
5.1 模型不可用的降级
- 返回缓存的上一次成功答案(带“可能过期”提示)
- 只给“下一步需要你补充的信息”(而不是硬编答案)
- 切换到更便宜/更快的模型(质量下降但能用)
5.2 工具不可用的降级
- 返回“我无法完成步骤 X,因为工具 Y 超时”,并给替代方案
- 把任务切换为“生成操作清单/手工步骤”
5.3 熔断:防止坏依赖拖死全系统
当某个工具连续失败时:
- 短时间内直接拒绝调用(熔断打开)
- 只允许少量探活请求
- 恢复后逐步放量
5.4 一张“失败类型 -> 处理动作”矩阵
很多团队说自己做了可靠性治理,但真正高峰出问题时,值班同学并不知道“这个错该重试、该降级,还是该直接拒绝”。
你可以先把最常见的失败做成矩阵:
| 失败类型 | 常见原因 | 默认处理动作 | 是否允许自动重试 | 备注 |
|---|---|---|---|---|
| 429 限流 | 上游配额打满 | 指数退避 + 排队 + 必要时降级 | 是,但要有预算 | 必须加抖动 |
| 5xx 服务错误 | 依赖抖动或故障 | 短重试,连续失败触发熔断 | 是,次数有限 | 熔断后放探活 |
| 超时 | 网络慢、依赖卡死 | 取消执行并记录时延 | 视操作类型而定 | 写操作更谨慎 |
| 空结果 | 检索不到/工具返回空 | 追问用户或切换备选路径 | 否 | 不要盲重试 |
| 状态冲突 | 同会话并发写入 | 版本校验失败后重放或排队 | 否,优先串行化 | 容易串台 |
| 高风险写失败 | 幂等/权限/业务边界不满足 | 直接拒绝并给出明确原因 | 否 | 不要自动补偿乱写 |
把这张表写进 runbook,比抽象说“系统具备高可用设计”更有实际价值。
六、可观测性:没有 tracing,你永远在猜
Agent 的观测不只要日志,还需要“可回放”。推荐最小采集:
trace_id:贯穿一次用户请求conversation_id/user_idstep_id/tool_name/tool_latency_ms/tool_errormodel/prompt_tokens/completion_tokens/costretry_count/timeout_count/circuit_breaker_statecitations_count(如果有 RAG 引用)
6.1 先固定指标口径,再谈优化
如果没有统一口径,所谓“系统变稳定了”就只是感受。建议你最少先做下面这组仪表盘:
| 指标 | 如何采集 | 建议样本口径 | 目标方向 |
|---|---|---|---|
p95_e2e_latency | 在 API 入口和最终输出处打点 | 全量线上请求 | 高峰期可控 |
tool_failure_rate | 每次工具调用记录 success/failure | 按工具维度拆分 | 快速识别坏依赖 |
queue_wait_time | 请求入队到出队时间差 | 按 fast/normal/heavy 分层统计 | 轻任务优先 |
retry_per_request | 每次请求累计重试次数 | 按失败类型拆分 | 避免重试风暴 |
degrade_rate | 降级返回占总请求比例 | 高峰与异常窗口重点观察 | 可解释而不是失控 |
如果你要构建一个最小评估集,建议先收 20 到 50 条高峰时段失败样本,逐个回放,确认每类错误都能归因到具体处理层。
然后把失败分成可行动的类别:
- 入口限流导致的拒绝(容量问题)
- 外部依赖 429/5xx(配额/供应商问题)
- 工具超时(SLA 或网络)
- 状态冲突(并发一致性)
- 输出校验失败(协议/Prompt/Schema)
这也是“项目含金量”的核心来源:你能用数据讲清楚系统怎么变好。
七、一个最小可上线架构(你可以直接画在面试白板上)
你可以用这套骨架讲清楚“并发与可靠性”闭环:
- API Gateway:认证 + 限流 + 路由
- Job Queue:按会话/优先级排队
- Agent Worker:执行 planning + tool calling
- State Store:会话状态 + 摘要 + 版本
- Evidence Store:向量库/知识库(RAG)
- Observability:trace + metrics + replay
如果你在选题 5 做好了记忆与状态,这里就是自然延伸。
而当你的系统从单 Agent 进入协作式编排,并发治理会进一步升级到任务仲裁、预算分配和补偿回滚:
八、落地清单:从“能跑”到“能扛”
按这个顺序做,成本最低,收益最大:
- 会话级串行(排队)+ 用户级限流
- 分层超时 + 指数退避重试 + 幂等键
- 工具熔断 + 降级路径
- 状态 schema + 版本化写入
- tracing + 关键指标 + 失败分类
如果你想把这篇直接转成团队执行清单,可以再补一层发布前检查:
- 是否已经区分 fast / normal / heavy 三类任务
- 是否为同会话请求设置了串行策略或版本控制
- 是否对 429 / 5xx / timeout 分别定义了处理动作
- 是否对副作用操作启用了幂等键
- 是否有熔断阈值与恢复条件
- 是否能在一次失败请求里看到完整 trace、重试次数和降级结果
下一步如果你想继续把系统做“更复杂”,可以进入多 Agent 编排或安全权限:
- 多 Agent 协作架构(选题 7)
- Agent 安全与权限控制(选题 9)


