做 Agent 系统也快两年了,最深的体会就是:代码跑通只是起点,让一堆 Agent 在线上稳定地跑起来、而且账单不失控,才是真正考验架构功夫的地方。近期被问得最多的不是 Prompt 怎么写、工具怎么接,而是“我们这套 Agent 服务到底该配多少机器、多少并发、踩了量突发怎么办”。这恰好是标题里那个词——AI Agent Harness Engineering——真正要回答的问题。
Harness 这个词最早是测试里的“测试夹具”,到了 Agent 场景里,它指的是承载 Agent 运行的那一层基础设施:任务调度、上下文组装、工具调用编排、限流与重试、生命周期管理。你可以把它理解成 Agent 的“驾驶舱 + 发动机舱”,LLM 是引擎,工具是车轮,Harness 是负责把这一整套东西安全地开着上路的那部分。容量规划和弹性扩展,就是这套系统能不能扛住线上真实流量的命门。
这篇文章不讲花哨的架构图,只讲我怎么从零开始把一套 Agent 服务的容量模型算明白、把弹性伸缩调顺手的全过程。包括我自己踩过的坑、改过的参数、以及压测时发现真相的瞬间。适合正在做 Agent 平台、智能体编排系统,或者即将把 Agent 从 Demo 推到生产的团队参考。
1. Harness 在管什么:先对齐 Agent 系统里最容易被低估的一层
1.1 为什么 Agent 的容量问题比传统服务难一个量级
传统 Web 服务的容量规划相对好做:一个请求进来,处理逻辑基本固定,RT 的波动范围不会太离谱。你只要压出单机 QPS,再用高峰流量除以单机能力,乘个冗余系数,就差不多了。但 Agent 系统完全不是这个玩法。
最核心的区别在于:Agent 的一次“用户请求”,内部会展开成一段长度不固定的执行链条。同一个问题,今天模型可能两轮工具调用就结束了,明天换个输入,可能要循环七八轮才收敛。每一轮循环都意味着一次或多次 LLM 调用,每一次 LLM 调用都要携带当前全部上下文。这导致两个直接后果:单个任务的延迟不是一个稳定值,而是一个宽分布;单个任务消耗的资源也不是一个固定值,同样是一个宽分布。
我在线上见过最夸张的例子:同一个任务模板,正常轨迹平均消耗约 20 万 token,但有一批边缘输入直接把轨迹拉到了 80 万 token,执行时间从 40 秒拉到了 4 分钟。如果你按平均值去做容量规划,高峰期遇到一批复杂任务,系统会瞬间被击穿。所以我后来定了一条规矩:容量规划看的不只是均值,更要看 P95、P99 的 token 消耗和执行时长。
1.2 Harness 层要盯住的四类资源
做容量规划之前,得先搞清楚 We 在规划什么。我总结下来,Agent Harness 层至少要盯住四类资源,缺一个都会出问题:
第一类是 LLM 提供方的配额。所有商用模型接口都有 RPM(每分钟请求数)和 TPM(每分钟 token 数)限制。这是最容易被忽略的天花板,因为你的机器再多,配额打满就是打满,只能排队。
第二类是单 Worker 的计算资源。Agent 运行在 Worker 进程里,需要 CPU 做逻辑编排、序列化、工具调用处理,需要内存缓存任务状态和上下文。如果你的 Worker 还要加载本地模型或 embedding 模型,资源消耗还得重估。
第三类是上下文与存储。Agent 执行过程中,轨迹、中间结果、工具返回的数据都要暂存。这类数据量看着不大,但高并发下内存和存储的 IO 压力是实打实的。
第四类是外部工具链的配额。Agent 离不开工具调用,而每个工具背后往往是一个外部 API,它们自己也有限流。就算你的 LLM 配额充裕、Worker 足够多,搜索接口或数据库接口被限流,任务一样卡死。
这四类资源互相牵扯,规划时必须一起算。光盯机器指标,忽略 provider 配额,扩容扩到天上也是白搭。
2. 容量规划的底层逻辑:把一串不稳定的执行过程变成一个可计算的账本
2.1 用 Trajectory 而不是请求来计量消耗
容量规划的第一步,是先选对计量单位。传统服务数请求数,Agent 系统我建议直接数 Trajectory——也就是一条完整的执行轨迹。
为什么?因为一个“任务”的消耗,等于它整条轨迹上所有 LLM 调用、工具调用、token 消耗的总和。只有把轨迹作为基本单元,你才能把“业务侧每天跑多少任务”和“底层消耗多少 token、占用多少配额”这两件事对应起来。
具体怎么计量?我给每条轨迹都做了结构化记录,包含三个关键字段:LLM 调用次数、累计输入 token、累计输出 token。有了这三个字段,你就可以算出一系列派生指标:任务平均 token 消耗、任务平均执行时长、token 消耗分布。这些指标才是容量模型的原材料。
我强烈建议:从第一天起就把轨迹数据落库。哪怕早期不分析,也要存原始日志,否则等要规划容量的时候发现没有历史数据,就只能拍脑袋了。
2.2 一个能直接用的估算模型(附计算过程)
下面给一个我实际用过的估算模型,你不用照抄,但可以按同样的思路替换成自己的数。
第一步,确定目标吞吐。假设业务目标是每天跑 5 万个任务,一天 86400 秒,任务到达速率就是: 50000 ÷ 86400 ≈ 0.58 任务/秒。
第二步,算单任务的平均耗时。我这边生产环境的统计是:一个中等复杂度的任务,平均要做 7 次 LLM 调用、4 次工具调用。单次 LLM 调用平均耗时 9 秒(含网络和排队),单次工具调用平均耗时 2 秒,加上 Harness 自身的调度开销约 3 秒。单任务总耗时约为: 7×9 + 4×2 + 3 ≈ 74 秒。
第三步,用 Little's Law 算稳态并发窗口。系统里同时处于执行状态的任务数,等于任务到达速率乘以单任务耗时: 0.58 × 74 ≈ 43 个并发任务。
这意味着,你的系统最少需要支撑约 43 个任务同时在跑,否则任务就会在队列里越堆越多。这是理论下限,实际还要乘上 2 到 3 倍的峰值系数,我一般直接按 100 并发任务来设计。
第四步,算 provider 侧 token 预算。还是以我的统计为例:单次 LLM 调用平均输入 2.8 万 token、输出 1200 token。单任务累计 token 消耗约为: 输入 7×2.8万 ≈ 19.6万,输出 7×1200 ≈ 0.84万,合计约 20.4 万 token。
于是每分钟 token 需求是: 输入侧 0.58 × 19.6万 × 60 ≈ 682万 TPM,输出侧 0.58 × 0.84万 × 60 ≈ 29.2万 TPM。
如果高峰期按 2 倍算,输入侧需求就超过 1364 万 TPM。这时你可以拿着这个数字去对着所使用模型服务的配额表看了——大概率会发现自己离配额上限并不远。
第五步,算成本预算。注意:输入和输出的计费单价不一样,一般输出比输入贵好几倍。所以预算也要分开算: 每日输入 token ≈ 0.58×86400×19.6万 ≈ 9.8 亿,每日输出 token ≈ 0.58×86400×0.84万 ≈ 4210 万。再分别乘以单价、乘一个峰值和重试的损耗系数(我一般留 1.3 倍),才是真实的日消耗预算。按这个套路上线前先算一遍,基本上不会被月底账单打懵。
2.3 工具链调用:藏在 Token 之外的第二个容量瓶
Token 算完了,事情还没完。Agent 系统的容量瓶颈往往不只是 LLM,还有工具链。
每个工具调用都有外部 API 延迟,而且外部接口通常也有自己的限流规则。沿用上面的例子:一个任务平均 4 次工具调用,那么 43 个并发任务每秒产生的工具调用大约是: 43 × 4 ÷ 74 ≈ 2.3 次/秒。
如果某个工具是搜索类接口,限流是每分钟 100 次,那 2.3 次/秒已经逼近上限了。一旦 Agent 内部对某个工具的调用是循环式的——比如先搜索、再根据结果搜索下一页——一个任务就可能吃掉十几次工具配额,峰值时直接打穿外部限流。
我的处理方式分三层:第一层,给每个工具单独建一个配额计数器,独立限流,避免某一个 Agent 把共享工具的配额全吃掉;第二层,对工具返回结果做缓存,如果同一个任务里反复查询相同的关键词,直接从缓存返回;第三层,给工具调用设置“单任务调用次数上限”,超过阈值就强制让 Agent 换策略或提前结束。这些看起来是业务逻辑,实际上是容量治理的一部分。
3. 弹性扩展怎么做:三个维度得一起动
3.1 Worker 池的横向扩展与有状态问题的解法
容量规划的结论只是静态基线,线上流量是波动的,所以弹性扩展才是让系统真正稳住的机制。Agent Worker 池的横向扩展,思路和普通微服务类似,但有一个绕不开的坎:Agent 执行过程是有状态的。
什么意思?一个任务执行到一半,当前上下文、工具返回结果、步骤计数都存在 Worker 内存里。如果这个 Worker 被回收或者挂掉了,任务就断了。传统无状态服务可以随便加机器,Agent 不行,除非你把状态拿出来。
我的解法是“外部化状态”:任务的轨迹和执行进度实时写入后端存储,Worker 内存里只保留当前几步的执行快照。这样任何 Worker 都可以随时接管一个中途任务。Worker 被弹性缩掉也不怕,任务重新调度到别的 Worker 上,从快照恢复继续跑。代价是每次状态同步会多一点 IO 开销,但换来的是 Worker 池可以像无状态服务一样自由伸缩,非常划算。
实际扩容时的参数也值得说一句。不要光看 CPU 利用率,还要看队列积压和任务延迟。Agent 任务的瓶颈经常在等 LLM 响应——也就是 IO 等待,这时候 CPU 利用率很低,但队列已经积压了。我一般用“队列深度”作为扩容信号:队列里积压的任务超过阈值,就加 Worker;积压清空且持续一段时间,就减 Worker。这个信号比 CPU 直接得多。
3.2 队列与背压:把洪峰挡在系统外面
弹性扩展不是无限扩的,扩到一定规模之后,外部配额就成了天花板。所以光有扩容机制不够,还要有背压机制——在容量不足时优雅地拒绝压力,而不是硬扛。
我的 Harness 里加了一层有界队列。任务进来先进队列,Worker 从队列里拉任务执行。队列要有长度上限,超过上限就直接拒绝新任务,返回“系统繁忙”或让调用方稍后重试。这么做的好处是:洪峰来了,系统不会因为无限堆积而 OOM,也不会因为所有 Worker 都去抢任务而互相踩踏。
队列的容量设置也有讲究。我一般把队列上限设成“稳态并发 × 3”左右。沿用前面的例子,稳态并发 43,队列上限就设在 130 左右。这样即使瞬时流量翻了三倍,系统也只是排队,不会崩。
背压的另一个关键设计是分级降级。当队列深度超过 70% 时,我会自动把非核心任务降级——比如去掉日志分析的附加步骤、把多模型投票改成单模型直出、关闭非关键工具的调用。这样可以用牺牲部分质量换取核心任务的可用性。降级策略写死在 Harness 配置里,不需要人工干预。
3.3 提供方限流下的反向伸缩逻辑
很多人做弹性扩展时只盯着自己这边,忽略了一个事实:外部模型的 TPM/RPM 配额才是真正的紧箍咒。你的 Worker 扩得再快,配额用完了,请求就会被 429 拒掉,任务反而因为重试雪崩而变得更糟。
所以要理解一种“反向伸缩”逻辑:当收到大量限流错误时,正确的动作不是扩容,而是降并发、加重试间隔。我见过不止一次团队把 Worker 从 10 个扩到 50 个,结果限流错误率不降反升。因为每个新增的 Worker 都在用同样的配额,配额被争抢得更厉害。
更好的做法是加一层智能限流客户端:自己维护一个令牌桶,按 provider 的 TPM 配额来控制请求发送速率。请求在本地排队,配额够才发出去。这样 Worker 随便扩,出口速率永远稳定在配额之下。同时配合指数退避 + 抖动重试。抖动这个点很容易被忽略——如果没有随机抖动,一堆请求在同一时刻重试,会形成新的峰值,把自己打死。
另外一个实用技巧:如果模型服务端有 Prompt Caching 之类的能力,务必开启。Agent 轨迹里相邻的 LLM 调用往往共享大段上下文,缓存命中后,重复部分的 token 计费会大幅降低,等于间接把有效容量放大了。我遇到过团队开缓存后,同一批任务的 token 成本下降了四成,配额压力也肉眼可见地缓解。
4. 实操过程:压测方法、监控指标与扩容参数怎么定
4.1 先重构压测思路:录制回放比并发脚本靠谱
容量模型算得再漂亮,最终还是要用压测验证。但 Agent 系统做压测有个难题:用传统的方式打固定接口,压出来的结果没有意义,因为 Agent 的行为是动态的。
我最终采用的方法是“录制回放式压测”。从生产环境采集一批真实任务的轨迹,记录每一步的 LLM 调用参数、工具调用参数、耗时分布,然后在一个隔离环境里按不同并发倍速重放这些轨迹。压测时,LLM 调用可以用录音数据模拟延迟,也可以接真实模型服务看真实配额消耗,两者各有用途。
录制回放的好处有三个:一是请求特征真实,包含真实的上下文长度和调用次数分布;二是可以精确控制并发倍率,比如 1×、2×、5×,观察系统在哪个水位上开始劣化;三是可重复,同一个轨迹集可以反复用于回归验证,比较版本之间的容量变化。
压测时我会重点看两个指标:一个是“任务完成延迟的 P95 是否随并发明显恶化”,另一个是“单位时间内实际消耗的 TPM 是否达到配额上限”。如果 P95 恶化但 TPM 还没到上限,说明瓶颈在 Worker 侧,加机器有用;如果 TPM 先到上限,说明瓶颈在配额侧,得换思路——开缓存、减上下文、降并发。
4.2 容量监控要看的关键数字
压测验证完,上线的监控也得跟上。我梳理了一套针对 Agent Harness 的监控面板,四个维度,缺一个都会让你在故障时变成盲人摸象。
第一维是任务侧:任务到达速率、队列深度、排队等待时长、任务完成延迟的 P50/P95/P99。这些指标反映系统整体的健康度和用户体验。
第二维是 Worker 侧:每个 Worker 的并发任务数、CPU 使用率、内存使用率、状态同步 IO 耗时。注意,Worker 的 CPU 数值参考价值有限,重点要看并发任务数是否均匀、有没有热点 Worker。
第三维是模型侧:每分钟实际消耗的 TPM/RPM、限流错误率、单次 LLM 调用的首 token 时延和完成时延。这是最容易被忽视的一块,但往往是最先出问题的。
第四维是工具链侧:每个工具的调用次数、平均延迟、错误率、配额剩余量。我在线上被工具限流坑过不止一次,所以现在工具侧的监控比模型侧还要细。
这四维数据我会汇总成一个“容量健康分”,实际上是几个核心指标的加权计算。健康分低于阈值就触发告警,高于阈值就保持。不要贪多,告警项控制在十条以内,否则运维会直接免疫。
4.3 扩容参数的估算套路
监控有了,压测做了,最后要落地的是一组可执行的扩容参数。这组参数怎么定,我总结了一个简单套路,大体分四步。
第一步,算单 Worker 的并发能力。假设单 Worker 内部最多同时跑 3 个任务(由任务对内存和状态 IO 的占用决定),那么支撑 43 个稳态并发就需要约 15 个 Worker: 43 ÷ 3 ≈ 14.3,取整到 15。
第二步,定弹性扩缩容的阈值。我一般取两个水位:队列深度超过稳态并发的 1 倍(这里是 43)时开始扩容,每增加 43 加一批 Worker;队列深度降到稳态并发的 0.3 倍以下,持续 5 分钟,开始缩容。冷却时间设置 3 分钟,避免频繁抖动。
第三步,定最大最小实例数。最小值要保证能接住基础流量,一般按 50% 稳态需求算;最大值要受到 provider 配额约束,我按“配额允许的最大并发任务数 ÷ 单 Worker 并发能力”来算。假设 provider 配额允许 150 个并发任务,那最大 Worker 数就是 150 ÷ 3 = 50。
第四步,加安全护栏。预算护栏很重要:给每个任务设单任务 token 消耗上限、给每天设总 token 消耗上限。一旦触及上限,Harness 自动降级策略,比如强制简化 tool 指令、缩短上下文、或者在完成当前步骤后直接输出结果。这个护栏救过我很多次,强烈建议所有团队都做。
5. 踩坑实录与问题速查:这些坑我基本都踩过
5.1 三个最典型的容量事故
第一个事故:Token 突刺打穿配额。上线后某个晚上,一类新任务突然占用了大量上下文,单任务 token 消耗是平均值的四倍。结果模型侧 TPM 直接打满,所有任务开始排队,整体延迟从 5 秒涨到 20 秒。复盘后发现,问题根源是上游把一段超长文档塞进了系统提示词,而容量模型没有覆盖到这种极端输入。修复方案除了对输入长度做截断外,还加了实时 TPM 用量看板和配额熔断:TPM 消耗超过配额 80% 时,新任务自动降级为简化模式。
第二个事故:冷启动拖垮延迟。某次大促流量翻倍,弹性扩容触发了 Worker 的新增,结果新增的 Worker 冷启动花了十几秒——初始化工具 SDK、加载配置、建立数据库连接池、拉取 Prompt 模板缓存。这些任务在冷启动期间全部卡在队列里,导致 P95 延迟直接爆表。后来改成预热的思路:提前扩容,而不是等到压力来了再扩。再加一层热备池,始终保有两个已就绪的备用 Worker,随时可以接任务。
第三个事故:限流重试雪崩。有一次 provider 调整了配额策略,导致大量请求 429。客户端重试逻辑写得不严谨,所有请求都在等待固定时间后重试,刚好形成一波波整齐的撞击。服务端每放开一会儿就被打回去,反反复复,连累其他正常任务也大量失败。修复方案就是前面提过的指数退避加抖动,并且给重试次数设上限,超过限制就快速失败而不是无限循环。
5.2 常见问题速查表
我把平时最容易遇到的现象、根因和对策整理成了一张速查表,方便你实锤排查。
| 现象 | 根因 | 对策 |
|---|---|---|
| 队列堆积但 Worker CPU 很低 | 瓶颈在模型侧配额,Worker 在等待响应 | 检查 TPM/RPM 用量;开启 Prompt Caching;降低并发 |
| 任务延迟随机突增 10 秒以上 | 外部工具接口被限流或变慢 | 给工具调用加超时和熔断;缓存重复调用结果 |
| 扩容后性能不升反降 | 新增 Worker 争抢同一限流配额 | 改为客户端令牌桶控制出口速率;限制最大实例数 |
| 大促时任务大量失败 | 扩容冷启动太慢,任务排队超时 | 预热 Worker 池;提前扩容;缩短冷启动流程 |
| 模型账单超预算 50% 以上 | 单任务 token 消耗上限缺失 | 设置单任务 token 护栏;强制截断超长上下文 |
| 一个 Agent 把工具配额打满 | 工具调用没有独立配额 | 每个工具单独计数限流;设单任务工具调用上限 |
| 重试导致雪崩式失败 | 重试缺少退避和抖动 | 指数退避 + 随机抖动;限制最大重试次数 |
5.3 容量规划这件事,我的几条心得
第一,容量规划绝对不是一次性工作。模型在迭代、任务模板在变、用户输入在变,你今天算出来的数字,下个月可能就完全不准了。我现在的节奏是每周自动跑一次容量报告,用过去七天的轨迹数据重新估算基线,变化超过 20% 就触发人工审视。这个习惯让我避开了很多“周五下班前流量突涨”的尴尬。
第二,预算和配额要像对待生产事故一样对待。我见过太多团队把 token 成本当作“事后看账单”的财务问题,结果就是月底被账单吓一跳。把 token 当作一等监控指标,和延迟、错误率一样放在核心告警里,你会发现成本失控的苗头在产品出问题之前就已经暴露了。
第三,尽量在模型侧做“瘦身”,而不是靠硬件硬扛。同样的功能,输入 token 从 5 万降到 2 万,容量需求直接减少一半多,成本也同步下降。优化提示词压缩、减少重复上下文、截断无用历史记录,这些看起来是小事,但对容量和成本的影响立竿见影。
最后再分享一个我自己的习惯:每次上线新任务模板或者改模型参数,我都会先在预发环境用录制回放压一遍,重点观察单任务 token 消耗分布有没有显著变化。只要这一步坚持住,容量规划上的绝大部分风险,其实都已经被挡在了生产环境之外。