Agent 调用高并发优化分享
Agent 调用高并发优化分享
标签:#AI工程 #Agent #高并发 #调用链 #工程实践
Agent 调用高并发优化分享

最近压了一轮 Agent 调用。
最早看到的结果并不好看:千级并发打进来,成功和失败差不多对半。页面上像是请求超时,日志里也很容易把锅甩给入口,觉得是入口没扛住。
后面把阶段拆开看,才发现入口只是表象。很多调用已经进了系统,只是卡在了后面的某一段:上下文还没准备好,模型请求排着队,工具执行占着窗口,事件还在写,客户端还在轮询或订阅状态。任何一段慢下来,用户看到的都是"这次 Agent 调用失败了"。这种链路如果没有背压,压力会沿着调用链往后传,最后把模型、工具、写入和观察链路一起拖住。
Agent 调用和普通接口最大的差别就在这里。
普通接口大多是一进一出。请求进来,查点数据,调一两个服务,返回。Agent 调用更像一段会展开的执行过程:先读上下文,再请求模型;模型输出时可能提出工具调用;工具跑完以后,结果还要回到模型;中间还要不断给客户端推状态;最后再把关键过程和最终结果存下来。
我后来把它从"一次请求"改成"一条会持续运行的调用链"来处理。这个判断影响了后面所有改动。
先看一段本地记录
下面这段是我整理后的本地压测记录。配置按脱敏口径写成资源受限的单机环境,重点不是公布真实机器参数,而是说明这轮不是靠堆资源把数字顶上去。
压测对象:Agent 调用链
机器口径:资源受限的单机环境
并发量级:千级
早期现象:成功和失败接近对半
失败表现:客户端等待超时、运行态查询不稳定、部分调用终态回收慢
主要瓶颈:完整调用链并发失控,模型、工具、事件写入、客户端观察互相影响
调整方向:接入轻量化、执行池限流、事件分层、异步补写、上下文预算
调整后:千级并发稳定完成,单次调用持久化事件从几十条降到个位数级别
这段记录里有两个点比较有价值。
第一,机器资源不宽裕。按资源受限的单机口径去看,优化重点不在机器规格,而在调用链治理。入口、执行池、事件写入、上下文装配这些边界收住以后,同样的资源能承载更稳定的 Agent 调用。
第二,失败不是单点失败。入口、模型、工具、写入、观察任何一段抖动,最后都会表现成一次 Agent 调用失败。只看接口成功率或者总耗时,很容易把问题修到错误位置。
普通 Agent 调用记录和高并发 Agent 调用治理,也不是一回事。
| 维度 | 普通 Agent 调用记录 | 高并发 Agent 调用治理 |
|---|---|---|
| 记录目标 | 说明调用发生过 | 判断调用能不能稳定执行 |
| 主要字段 | 开始、结束、结果 | 排队、执行窗口、上下文、模型、工具、事件、观察 |
| 事件处理 | 过程事件尽量记下来 | 实时过程和长期事实分层 |
| 异常处理 | 失败后查日志 | 排队可恢复、写入可补偿、观察可重放 |
| 性能判断 | 看总耗时 | 拆阶段耗时和资源占用 |
| 扩展依据 | 请求量上升就扩容 | 单机边界清楚后再复制执行单元 |
这张表也是我后面改造时的分界线:普通记录解决"有没有",高并发治理解决"稳不稳"。
这几个边界为什么重要
只盯入口并发,很容易把问题看成“流量太大”。Google SRE 在级联故障的讨论里提到,过载会沿着系统往下传,容量规划能降低风险,但不够,入口限流和单个任务的限压也要一起做。Agent 这类长链路调用更明显,因为模型、工具、写入和观察不是独立的,它们会互相放大压力。
重试也不能靠运气。Stripe 在幂等请求文档里强调,创建和更新类操作如果要安全重试,就要带幂等键,避免重复创建或重复更新。放到 Agent 里,队列恢复、事件补写、客户端重连都属于同一类问题:同一件事重复一次,系统还能不能保持一致。
上下文同样不是越多越好。Anthropic 的 prompt caching 和 context windows 文档都在提醒一件事:长对话里,重复前缀、持续增长的历史和有限的上下文窗口会直接影响成本和行为。Agent 的上下文装配、裁剪、摘要压缩,本质上就是在控制这条边界。
单机先跑清楚
从专业角度看,Agent 调用到千级并发,肯定已经是高并发。继续往小几千、大几千推,也肯定要上多机器。执行节点要横向扩展,入口要分流,状态和观察链路也要能拆出去。
但我没有一上来就堆机器。
单机没跑清楚,多机器只是把混乱复制几份。你会看到更高的吞吐,但很难回答几个基础问题:一个节点到底能稳定跑多少条 Agent 调用链;慢在模型,还是慢在工具;上下文准备有没有拖慢整体;事件写入有没有形成写入放大;客户端观察有没有反过来压住服务端。
所以单机阶段我先看一件事:一条 Agent 调用从进来到完成,哪些地方会占资源,哪些地方会排队,哪些地方必须限制并发。
这一步做完以后,多机器扩展才有意义。后面扩的不是一团看不清的逻辑,而是一组已经知道边界的执行单元。
接入要轻,执行要排

早期的问题之一,是调用一进来就很快展开完整执行。千级请求一起进来,等于千级 Agent 同时抢上下文、模型、工具和写入资源。
我先把接入阶段变轻。
用户发起调用后,系统只做几件小事:创建调用记录,返回调用 ID,写入已接收或排队状态,让客户端知道这次调用已经进入系统。完整执行交给后面的执行池消费。这个阶段的目标不是完成任务,而是把调用可靠地纳入调度面。
这个改动看起来朴素,但对 Agent 很有用。因为 Agent 后面不是一个短任务,它可能要跑模型、跑工具、写多轮事件。如果入口直接放开,后面每个环节都会被同一波流量打满。
接住调用以后,还要保证它不会被重复执行。队列里会记录已经入队的调用,执行池只领取自己该处理的任务。进程重启或发布切换后,系统会把还停在排队状态的调用重新捞出来,放回执行队列。这里的重点是幂等和可恢复,不能只把它当成一次普通的后台任务提交。
伪代码大概是这样:
onAgentRequest(input):
# 接入阶段只做轻量记录,不展开完整 Agent 链路
callId = createCall(status = "queued")
# 入队前做去重,避免同一次调用被重复执行
if not queue.contains(callId):
queue.push(callId)
return {
callId,
status: "queued"
}
executionLoop():
while active:
callId = queue.take()
# 执行槽是并发闸门,拿不到就回到队列等待
if not acquireExecutionSlot(callId):
queue.retryLater(callId)
continue
# 只有进入执行槽的调用才会开始上下文、模型、工具链路
runAgentChain(callId)
普通调用记录通常只回答"这次调用有没有发生过"。高并发 Agent 调用还要多回答一个问题:异常之后,这条记录还能不能继续执行。
限住完整链路
入口能接住千级,不代表系统可以同时跑千级完整 Agent。
一次完整 Agent 调用会占住上下文准备、模型请求、工具请求、状态写入、结果汇总、客户端状态同步。入口放过去以后,重活都在后面。
所以我限制的是完整调用链的并发。
入口先收住,队列排住,执行池按固定容量消费。这样千级请求不会一起冲进模型和工具层,系统也不会因为一波流量把所有阶段同时拖慢。执行池实际上承担的是并发闸门,保证完整 Agent 链路在可控水位里运行。
发布时还要处理新旧执行单元。Agent 调用可能跑很久,如果新旧版本同时领取同一批任务,就会出现重复执行或状态交叉。现在只有当前激活的执行单元继续领取新调用,旧执行单元把手上的调用处理完就退场。
这类边界不显眼,压测时却很救命。很多高并发问题,最后都落在这些地方:该排队的地方没排,该限住的地方没限住。
模型和工具分开算
一开始只看总耗时,几乎没有判断价值。
Agent 里模型和工具的压力完全不同。模型通常慢,而且会占住执行窗口。工具调用次数不固定,有的任务一次都不调,有的任务会连续调好几轮。再叠上上下文准备、结果写回、客户端观察,总耗时变长时,你很难知道该改哪里。
后来我把阶段指标拆开:接收后排队多久,上下文准备多久,第一次模型请求什么时候发出,模型什么时候开始输出,工具什么时候开始规划,第一个工具什么时候开始跑,最终结果什么时候落地。
拆完以后,定位变得直接很多。
排队长,看执行池和调度。上下文慢,看读取、裁剪和缓存。模型慢,看模型请求和输出。工具慢,看工具规划、执行和结果回写。客户端看到失败,也先别急着算 Agent 执行失败,还要看查询、订阅、轮询有没有问题。
普通 Agent 调用记录记开始、结束、结果,低并发时够用。到千级并发以后,这种记录太粗。你需要知道慢在哪一段,否则很容易修错地方。
流式过程别全进历史

Agent 调用会产生很多过程事件。
模型开始输出,输出一小段,又输出一小段。模型准备调工具,工具开始执行,工具返回进度,工具返回结果。然后 Agent 进入下一步。
这些信息对用户有用,对排查也有用,但不应该每一条都写成长期历史。
早期压测里,单次 Agent 调用可能产生几十条需要持久化的事件。并发一上来,事件写入自己就成了压力源。这个问题本质上是写入放大:一次 Agent 调用还没结束,过程片段已经把存储层打得很忙。后面我做了两件事。
先合并模型流式片段。模型本来就是一小段一小段吐出来,如果每段都写一条历史,高并发下写入量会很夸张。现在按时间窗口和长度合并,再形成可保存的事件。
再区分实时过程和长期事实。流式片段、工具进度优先走实时流,让用户看到过程。关键状态、关键工具结果、最终结果才进入长期保存。实时流解决观察体验,长期事实解决审计、恢复和复盘,两者不要混在同一条写入路径里。
这里也可以抽象成一段很简单的分流逻辑:
onAgentEvent(event):
# 高频过程事件只进入实时流,优先保证用户看到进度
if event.type in ["model_delta", "tool_progress"]:
realtimeStream.publish(event)
return
# 关键事实进入持久化队列,用于恢复、审计和复盘
if event.type in ["state_changed", "tool_result", "final_result"]:
durableEvent = compact(event)
persistenceQueue.push(durableEvent)
改完以后,单次调用的持久化事件量从几十条降到个位数级别。用户仍然能看到过程,存储层不再被过程片段拖着跑。
异步写入要能补回来
事件改成异步写以后,压力会小很多,但这不等于事情结束了。
高并发下,异步队列可能满,存储也可能短暂不可用。如果关键事件只在内存里排队,进程一重启就丢。Agent 调用又很依赖过程和终态,一旦关键事实没落地,后面的重连、查询、复盘都会出问题。
我的处理方式是,关键事件先形成批次,写到本地暂存,再进入异步持久化队列。写入成功以后删除暂存。如果队列满了,或者存储写失败,后台任务会继续扫描暂存,把没写成功的补上。
实时流管用户眼前看到的过程,本地暂存和异步补写管关键事实最后能不能落地。
普通接口未必需要做到这一步。Agent 调用会产生连续事件,用户还可能断线重连,最终状态也必须能查回来。
第一反馈提前
Agent 调用经常要等模型和工具。用户发起调用后,如果一直等到模型开始输出才看到反馈,高并发时体验会很差。
所以第一反馈不能等模型。
调用刚进入系统,客户端就能拿到调用 ID 和当前状态:已接收、排队中、准备执行。模型还没开始,工具也没开始,但用户知道系统接住了。
运行中的调用会有实时流。客户端订阅后可以持续看到事件。断线后,客户端带着上一次看到的位置回来,系统先重放最近事件,再继续推后面的事件。如果实时流已经过期,也会退回到"调用仍在运行"这类状态,而不是直接报失败。
这里我特别注意把服务端执行和客户端观察分开。服务端已经完成,不代表客户端一定看到了;客户端轮询失败,也不代表 Agent 执行失败。压测时如果把这两类失败混在一起,后面一定会改错方向。
上下文也要控预算

Agent 调用还有一个容易被低估的成本:上下文会越跑越大。
如果每次调用都把历史消息、历史结果、历史事件全部塞回模型,高并发下会同时放大读取成本和模型成本。调用越多,历史越大,后面的调用越慢。
每次模型调用前,我会先做上下文装配。只带这次模型需要的内容,比如最近几轮消息、和当前任务相关的结果、少量关键活动、压缩过的历史摘要。超过预算就继续裁剪旧消息、旧活动和不相关结果。再不够,就把更早的历史压成摘要。这里要控制的是上下文预算,不只是提示词长度。
这一层不需要复杂,关键是先有预算,再装上下文:
buildModelInput(thread, task):
# 先按相关性装配上下文,而不是整段历史全量塞入模型
context = pickRecentMessages(thread)
context += pickRelatedResults(task)
context += pickKeyEvents(task)
context += loadSummary(thread)
# 超过预算就裁剪低相关内容,避免上下文成本失控
while estimateTokens(context) > tokenBudget:
context = pruneLeastRelevant(context)
# 仍然超限时,把更早的历史压成摘要
if stillTooLarge(context):
context = compactHistory(context)
return context
这看起来像上下文管理,实际也是高并发治理。模型上下文不是免费的。单次调用多带一点历史,千级并发下就是成倍的读取、序列化和推理成本。
压测后清楚了什么
改完以后,千级 Agent 并发调用可以稳定完成。单次调用的持久化事件量从几十条降到个位数级别。早期成功失败接近对半的问题也压下去了。
后面继续往小几千推的时候,问题也不再糊成一团。
第一反馈慢,就看接入和客户端观察。排队长,就看执行池容量和调度。上下文慢,就看装配、裁剪和缓存。模型慢就看模型阶段,工具慢就看工具阶段。最终状态慢,就看写入队列、本地暂存和补写。客户端报失败,先分清是服务端执行失败,还是观察链路失败。
大几千并发肯定要多机器。这点不用犹豫。单机治理的价值在于把 Agent 调用链拆清楚。拆清楚以后,多机器扩展才有抓手:哪些执行单元可以复制,哪些状态要共享,哪些事件可以只走实时流,哪些事实必须持久化。
如果你也在做 Agent 调用高并发,先别只盯入口 QPS。把一次 Agent 调用拆开看:接入、排队、上下文、模型、工具、事件、终态、客户端观察。每一段都能测量,每一段都有边界,系统才有机会从千级继续往上推。
更多推荐



所有评论(0)