1. 先搞清楚:多 Agent 并发到底在消耗什么资源
很多人一上来就问"16 核 32G 能扛多少个 Agent",这个问题本身就问错了。Agent 的资源消耗跟传统 Web 服务的并发模型完全不是一回事。一个 HTTP 接口的并发,瓶颈通常在 CPU 和连接数上,请求进来、查库、返回,生命周期短则几毫秒长则几百毫秒。但一个 AI Agent 的一次"运行",可能包含十几轮 LLM 调用、若干次工具执行、向量检索、文件读写,生命周期从几十秒到几分钟不等。
这意味着你不能用"QPS"那套思路来估算 Agent 的并发容量。你得先拆开看,一个 Agent 实例在运行期间,到底占着哪些资源、占多久、什么时候释放。
1.1 Agent 运行时的四类资源占用
我把一个典型 Agent(以 FastAPI + LangChain/LangGraph 这类技术栈为例)运行时的资源占用拆成四块:
第一块是进程本身的基础内存。一个 Python 进程加载完 LangChain、向量库客户端、各种工具依赖之后,常驻内存通常在 300MB 到 800MB 之间。这个数字跟你的依赖有多重直接相关——如果你还引入了 PyTorch 做本地 embedding,那基础内存直接飙到 2GB 以上。这部分内存是"每个进程都要付的固定成本",跟并发量无关。
第二块是单次 Agent 运行的工作内存。对话历史、中间推理步骤、工具返回结果、检索到的文档片段,这些都存在内存里。一次复杂任务的上下文可能累积到几十 KB 到几 MB 的文本。如果 Agent 会处理图片、PDF,那内存占用还要再上一个量级。
第三块是 LLM 调用的等待时间。这是最容易被忽略的。Agent 在等模型返回的时候,CPU 是空闲的,但连接、上下文、内存都还占着。一个 Agent 一次运行如果有 10 轮 LLM 调用,每轮等 3 到 8 秒,那光等待就占了一分钟。这段时间里,你的进程资源被"锁"住了,但没干活。
第四块是工具执行时的突发资源。比如 Agent 调用代码解释器跑一段 Python,或者调用浏览器工具抓页面,或者做一次本地向量检索。这些操作会瞬间拉高 CPU 或内存,然后又降下来。
把这四块想清楚,你才能理解为什么"16C32G 支持多少并发"没有标准答案——它取决于你的 Agent 是"重推理轻工具"还是"轻推理重工具",取决于 LLM 是走 API 还是本地部署,取决于单次任务的平均时长。
1.2 为什么传统并发估算方法在这里失效
传统 Web 服务估算并发,用的是利特尔法则:并发数 = 到达率 × 平均处理时间。这套公式在 Agent 场景下依然成立,但问题在于"平均处理时间"这个变量极不稳定。
一个客服 Agent 回答"退货政策是什么",可能 5 秒就结束。同一个 Agent 处理"帮我对比这三份合同里的违约条款",可能要跑 3 分钟,中间调用十几次模型、读好几个文件。如果你按平均值去规划资源,高峰期一定会被打爆。
更麻烦的是,Agent 的并发不是"请求-响应"式的短连接,而是"会话-任务"式的长生命周期。一个用户可能同时开着三个 Agent 会话,每个会话都在跑不同的任务。这时候你的并发单位不是"请求数",而是"活跃任务数"。
我自己的经验是:规划 Agent 服务器资源,先别算并发数,先算"同时活跃的任务数"和"单任务的平均资源占用曲线"。这两个数据拿到手,容量规划才有意义。
1.3 一个真实的资源画像示例
举个我实际测过的例子。一个基于 FastAPI + LangGraph 的文档分析 Agent,走的是云端 LLM API,本地只做向量检索和文件解析。单进程配置如下:
| 资源项 | 空闲状态 | 单任务运行中 | 峰值 |
|---|---|---|---|
| 常驻内存 | 420MB | 650MB | 1.1GB |
| CPU 占用 | 1% | 15% | 60%(解析大文件时) |
| 网络连接 | 2 | 8 | 12 |
| 单任务时长 | - | 45 秒 | 180 秒 |
从这个画像能看出来,内存是主要约束,CPU 反而很闲。因为大部分时间在等 LLM 返回。这种情况下,一台 32G 内存的机器,扣掉系统和预留,大概能跑 20 到 25 个并发任务,而不是按 CPU 核数去算。
但如果你的 Agent 做本地 embedding、本地 rerank,甚至本地跑小模型,那 CPU 和 GPU 立刻变成瓶颈,结论完全反过来。所以下面必须分开讨论。
2. 内存才是第一约束:Agent 并发规划的核心账
我见过太多团队在 Agent 项目上翻车,不是因为 CPU 不够,而是因为内存被吃爆,进程被 OOM Killer 干掉。Agent 场景的内存问题比传统服务更隐蔽,因为它不是线性增长的。
2.1 常驻内存与工作内存要分开算
规划内存时,一定要把"每个进程的固定开销"和"每个任务的浮动开销"分开。
固定开销包括:Python 解释器、框架代码、依赖库、连接池、模型客户端。这部分不管你跑不跑任务都在。假设单进程固定开销是 500MB。
浮动开销包括:对话上下文、检索结果、工具中间产物、序列化缓冲。这部分随任务复杂度变化。假设单任务平均浮动开销是 300MB,峰值能到 800MB。
那么 N 个并发任务的内存需求大致是:
总内存 = 进程数 × 固定开销 + 活跃任务数 × 平均浮动开销 + 安全余量注意这里"进程数"和"活跃任务数"不一定相等。如果你用多进程模型(比如 Gunicorn 多 worker),每个 worker 是一个独立进程,各自有固定开销。如果你用单进程异步模型(asyncio),那固定开销只付一次,但所有任务共享一个进程的内存空间,一个任务内存泄漏会拖垮全部。
提示:异步模型省内存,但隔离性差;多进程模型隔离性好,但固定开销翻倍。这个取舍在 Agent 场景下尤其关键,因为 Agent 任务时长差异极大,一个卡住的任务可能拖累整个进程。
2.2 上下文累积是内存的隐形杀手
Agent 跟普通接口最大的区别是:它会累积上下文。一次多轮对话,历史消息不断追加;一次多步推理,中间结果不断堆叠。如果你不做上下文裁剪,内存会随任务时长持续上涨。
我踩过的一个坑:一个 Agent 处理长文档时,把整篇文档切块后全部塞进上下文,结果单任务内存从 400MB 涨到 3GB。后来改成滑动窗口 + 摘要压缩,同样的任务内存稳定在 600MB 左右。
具体做法有这么几种,按效果排序:
- 滑动窗口:只保留最近 N 轮对话,最省内存,但会丢失早期信息。
- 摘要压缩:把早期对话用 LLM 总结成一段话,兼顾内存和信息保留。
- 外部存储:把历史消息存到 Redis 或数据库,需要时按需加载,内存占用最低但增加 IO。
- 向量检索替代全量上下文:不把所有内容塞进 prompt,而是检索最相关的片段。
这几种可以组合用。我的建议是:默认用滑动窗口 + 外部存储,对信息保留要求高的场景再加摘要压缩。全量上下文只在任务很短、文档很小的时候才用。
2.3 内存泄漏在长生命周期 Agent 里会被放大
普通 Web 服务,进程定期重启,内存泄漏影响有限。但 Agent 任务动辄跑几分钟,如果每个任务泄漏几 MB,跑几十个任务后进程就危险了。
常见的泄漏点:
- 全局字典缓存了任务对象,任务结束后没清理。
- 事件监听器注册了没注销。
- 异步任务创建了没 await,异常被吞掉。
- 第三方库(尤其是向量库客户端、HTTP 客户端)的连接没关闭。
排查手段我常用这几个:用tracemalloc抓内存快照对比,用objgraph看对象引用链,或者直接上memory_profiler逐行看。生产环境更简单粗暴的办法是给进程设内存上限,超了就重启,配合任务队列保证不丢任务。
注意:Agent 进程重启成本比普通服务高,因为可能有正在跑的长任务。所以重启策略要配合任务状态持久化,重启后能恢复。
2.4 一个可落地的内存容量计算公式
综合上面几点,我给一个实操中能用的公式:
可承载并发任务数 = (总内存 - 系统预留 - 进程固定开销 × 进程数) / 单任务峰值内存举例:32G 内存的机器,系统预留 4G,单进程固定开销 500MB,跑 4 个 worker 进程,单任务峰值内存 800MB。
(32 - 4 - 0.5 × 4) / 0.8 = (32 - 4 - 2) / 0.8 = 26 / 0.8 ≈ 32理论上能跑 32 个并发任务。但这是峰值内存,实际运行中不可能所有任务同时达到峰值。所以可以再乘一个经验系数 0.7 到 0.8,得到 22 到 26 个并发任务比较稳妥。
这个数字比"16C32G 支持多少并发"这种拍脑袋答案靠谱得多,因为它是从你自己的资源画像推出来的。
3. CPU 与 GPU:什么时候它们才是真正的瓶颈
内存算完,接下来看计算资源。Agent 场景下 CPU 和 GPU 的角色,取决于你的 Agent 是"调用型"还是"本地推理型"。
3.1 纯 API 调用型 Agent:CPU 基本不是瓶颈
如果你的 Agent 所有 LLM 调用都走云端 API,本地只做编排、工具调用、轻量检索,那 CPU 占用会低得让你意外。我实测过一个编排型 Agent,单任务 CPU 占用平均 10% 到 15%,峰值也就 40% 到 60%,而且峰值只出现在文件解析、JSON 序列化这种瞬间操作上。
这种情况下,CPU 核数不是主要约束,网络 IO 和连接数才是。因为每个 Agent 任务可能同时持有多个到 LLM API 的 HTTP 连接,连接池配置不当会导致任务排队等待。
我的配置经验:
- HTTP 连接池大小设为预期并发任务数的 1.5 到 2 倍。
- 设置合理的超时(连接超时 5 秒,读取超时按模型最长响应时间设,通常 60 到 120 秒)。
- 开启 keep-alive 复用连接,减少握手开销。
- 对 LLM API 做限流和重试,避免瞬时打爆配额。
CPU 方面,4 到 8 核足够支撑几十个编排型 Agent 任务。真正吃 CPU 的是本地 embedding、rerank、文件解析这些操作,如果这些量大,再单独加核。
3.2 本地推理型 Agent:GPU 显存决定一切
一旦你的 Agent 需要本地跑模型——不管是 embedding、rerank,还是完整的小参数 LLM——GPU 就成了核心约束,而且约束的是显存,不是算力。
显存占用分三块:
- 模型权重:7B 模型 FP16 大约 14GB,INT8 量化约 7GB,INT4 约 4GB。
- KV Cache:跟并发数和上下文长度成正比。上下文越长、并发越高,KV Cache 越大。
- 中间激活:跟 batch size 相关。
很多人只算模型权重,忽略了 KV Cache,结果一上并发就爆显存。KV Cache 的估算公式大致是:
KV Cache 大小 ≈ 2 × 层数 × 注意力头维度 × 序列长度 × 并发数 × 精度字节数这个公式不用背,你只要记住一个结论:并发数翻倍,KV Cache 翻倍;上下文长度翻倍,KV Cache 也翻倍。所以本地推理型 Agent 的并发容量,是被显存死死卡住的。
举个例子,一张 24GB 显存的卡,跑 INT4 量化的 7B 模型,权重占 4GB,剩 20GB 给 KV Cache 和其他开销。如果单任务上下文 4K,那大概能支撑 8 到 12 个并发。上下文拉到 16K,并发直接掉到 2 到 3 个。
3.3 GPU 租用与自建的取舍
本地推理型 Agent 面临一个现实问题:GPU 从哪来。自建要考虑采购成本、机房、散热、运维;租用要考虑单价、可用性、数据合规。
我的判断逻辑是这样的:
- 任务量稳定且大:自建或长期租用更划算,单位成本低。
- 任务量波动大:按需租用,用多少付多少。
- 数据敏感:必须本地部署,租用也要选合规的私有环境。
- 只是验证阶段:先用云端 API 跑通逻辑,别急着上 GPU。
这里有个容易忽略的点:GPU 租用的计费通常按小时,但 Agent 任务可能几分钟就结束。如果你的任务稀疏,GPU 大部分时间在空转,成本反而比 API 高。所以稀疏任务优先用 API,密集任务才考虑本地 GPU。
3.4 混合架构:把合适的活分给合适的资源
实际生产里,最经济的方案往往是混合的:
- 高频、短上下文的任务走云端 API。
- 低频、长上下文、数据敏感的任务走本地 GPU。
- embedding 和 rerank 用本地小模型,省 API 调用成本。
- 复杂推理用大模型 API,保证质量。
这种架构下,资源规划要分两套账:一套算 API 的并发配额和成本,一套算本地 GPU 的显存和吞吐。两套账分开管,别混在一起算。
4. 并发模型选型:进程、线程还是异步
资源算清楚了,接下来是并发模型。这个选择直接决定了你的资源利用率和稳定性。
4.1 三种模型的适用场景对比
| 模型 | 内存开销 | 隔离性 | 适合场景 | 坑 |
|---|---|---|---|---|
| 多进程 | 高(每进程独立) | 强 | CPU 密集、需要隔离 | 固定开销翻倍,进程间通信麻烦 |
| 多线程 | 中 | 弱 | IO 密集、共享状态多 | GIL 限制,Agent 场景收益低 |
| 异步 | 低 | 弱 | IO 密集、高并发 | 一个阻塞拖垮全部,调试难 |
Agent 场景下,因为大量时间在等 LLM 返回(IO 等待),异步模型理论上最省资源。但异步的坑也很明显:任何一处同步阻塞调用(比如用了同步的 HTTP 库、同步的文件读写),都会卡住整个事件循环。
我的实际选择是:异步为主,关键路径上用多进程做隔离。具体来说,用 FastAPI + asyncio 处理请求编排,把重 CPU 的操作(文件解析、本地推理)丢到独立的进程池或任务队列里,避免阻塞主事件循环。
4.2 异步模型下最容易踩的阻塞坑
异步 Agent 最常见的性能问题,就是某个环节偷偷用了同步调用。我列几个高频的:
- 用了
requests而不是httpx.AsyncClient调 LLM API。 - 用了同步的数据库驱动,比如
pymysql而不是asyncmy。 - 文件读写用了
open()而不是aiofiles。 - 向量库客户端是同步的,检索时阻塞事件循环。
- 日志、序列化用了重 CPU 的库。
排查办法:在事件循环里加一个监控,定期检查 loop 的延迟。如果延迟突然飙高,说明有阻塞。Python 3.11 之后可以用asyncio的调试模式,会打印慢回调的警告。
提示:不确定某个库是不是异步安全的,最稳妥的办法是把它丢到
run_in_executor里跑,用线程池隔离。虽然多了一点开销,但不会拖垮主循环。
4.3 任务队列:把长任务从请求链路里剥离
Agent 任务时长差异大,如果全部同步处理,一个长任务会占着连接不放,用户体验差,资源也浪费。更好的做法是引入任务队列。
架构大致是:请求进来,创建一个任务,丢进队列,立即返回任务 ID;后台 worker 从队列取任务执行;客户端通过任务 ID 轮询或订阅结果。
这样带来的好处:
- 请求链路短,连接快速释放。
- 任务可以排队、重试、优先级调度。
- worker 数量可以独立于 Web 进程调整。
- 长任务不会拖垮 Web 层。
队列选型上,轻量用 Redis + RQ 或 Celery,重一点用 RabbitMQ 或 Kafka。Agent 场景我倾向 Redis 系,因为部署简单,而且 Agent 本身可能已经在用 Redis 做缓存。
4.4 并发数控制:别让 Agent 无限扩张
异步模型有个危险:你可以轻松创建成千上万个协程,但它们会一起抢资源,最后谁都跑不动。所以必须做并发控制。
控制手段有两层:
- 信号量(Semaphore):限制同时运行的任务数,超过就等待。
- 队列长度限制:队列满了就拒绝新任务,返回"系统繁忙"。
信号量的值怎么定?回到第 2 章的内存公式,算出可承载并发任务数,信号量就设这个值。比如算出 25,信号量就设 25。这样保证不会因为任务过多导致 OOM。
队列长度可以设成信号量的 3 到 5 倍,给突发流量一点缓冲。超过就拒绝,保护系统。
5. 监控与压测:让容量规划有数据支撑
前面讲的都是估算,真正上线前必须压测,上线后必须监控。没有数据的容量规划都是耍流氓。
5.1 压测 Agent 和压测接口是两回事
用 JMeter 压普通接口那套方法,直接搬到 Agent 上会失真。因为 Agent 任务时长不稳定、资源占用非线性、还依赖外部 LLM API。
压测 Agent 要注意几点:
- 模拟真实任务分布:别只用一种任务压,要按生产环境的任务类型比例混合。
- 控制外部依赖:LLM API 要么 mock,要么用真实但限速的调用,否则压测结果被 API 限流干扰。
- 观察资源曲线:不光看响应时间,要看内存、CPU、连接数的变化曲线。
- 压到崩溃为止:找到系统的真实上限,而不是压到"看起来还行"就停。
我一般会做阶梯压测:从 5 并发开始,每 5 分钟加 5 个并发,观察资源曲线和错误率。找到错误率开始上升、内存接近上限的那个点,就是容量上限。实际部署时留 30% 余量。
5.2 必须监控的几个关键指标
Agent 服务的监控,除了常规的 CPU、内存、网络,还要加几个特有的:
- 活跃任务数:当前正在运行的任务数量,这是最核心的指标。
- 任务队列长度:排队等待的任务数,反映系统压力。
- 单任务时长分布:P50、P95、P99,看长尾有多长。
- LLM 调用延迟和错误率:外部依赖的健康度。
- 单任务内存峰值:找出内存大户。
- 进程重启次数:频繁重启说明有泄漏或 OOM。
这些指标用 Prometheus + Grafana 就能搭起来。关键是设好告警阈值,比如活跃任务数超过容量的 80% 就告警,内存超过 85% 就告警。
5.3 从监控数据反推容量规划
监控跑一段时间后,你会拿到真实的资源曲线。用这些数据反推容量,比任何估算都准。
具体做法:取一周的峰值时段数据,看活跃任务数、内存占用、CPU 占用的关系。如果发现内存是瓶颈,就按内存算容量;如果发现 CPU 是瓶颈,就按 CPU 算。然后根据业务增长预期,预留扩容空间。
我自己的习惯是每月复盘一次容量数据,看看是否需要调整信号量、worker 数量、队列长度。Agent 业务变化快,容量规划不是一次性的,是持续迭代的。
6. 几个实操中反复踩到的坑
最后分享几个我在 Agent 并发场景下反复踩到的坑,都是文档里不会写、但实际会要命的。
6.1 连接池配置不当导致的假死
有一次线上 Agent 突然大面积超时,CPU 内存都正常,就是任务卡住不动。排查半天发现是 HTTP 连接池太小,所有连接都被长任务占着,新任务拿不到连接,一直等。表现就是"假死"——资源没满,但服务不可用。
解决办法:连接池大小要大于并发任务数,并且设置连接获取超时,拿不到连接就快速失败而不是无限等待。
6.2 上下文没裁剪导致的内存缓慢上涨
Agent 跑得越久,内存越高,重启后恢复。这是典型的上下文累积问题。解决办法就是第 2 章讲的裁剪策略。我的经验是:任何进入上下文的文本,都要问一句"这个真的需要一直留着吗"。大部分时候答案是不需要。
6.3 本地模型和 API 混用时的资源争抢
混合架构下,本地 GPU 推理和 CPU 密集操作可能互相争抢资源。比如 embedding 在 GPU 上跑,文件解析在 CPU 上跑,如果调度不当,两者会互相拖慢。解决办法是给不同类型的任务分配独立的资源池,或者用优先级队列保证关键任务优先。
6.4 任务没有超时控制导致的资源泄漏
Agent 任务如果卡在某个环节(比如 LLM API 不返回、工具执行死循环),没有超时控制的话,这个任务会一直占着资源。跑多了资源就耗尽了。
解决办法:给每个任务设总超时,给每个 LLM 调用设单次超时,给每个工具执行设超时。超时就终止任务,释放资源,记录日志。
6.5 日志和追踪本身也是资源消耗
Agent 任务步骤多,如果每步都打详细日志、都做全链路追踪,日志量和追踪开销会非常可观。我见过一个案例,日志写入占了 20% 的 CPU。
解决办法:分级日志,生产环境只打关键节点;追踪采样,不是每个任务都全量追踪;日志异步写入,别阻塞主流程。
这些坑的共同点是:它们不会在低并发时暴露,一旦并发上来就集中爆发。所以容量规划不只是算资源,还要把这些工程细节都考虑进去。真正稳定的 Agent 服务,是资源规划 + 并发模型 + 监控告警 + 工程细节共同作用的结果,缺一不可。