Agent 工程体系 · 第 90/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。
Agent 限流与容量:并发、Token 速率、队列、GPU 配额和过载
Agent 的容量问题,不能简化为“接口每秒能处理多少请求”。一个 Agent 请求通常会经历多次模型调用、工具调用、重试、分支、人工确认和状态持久化。用户看到的是一次任务,系统实际消耗的是一组随时间变化的资源:
其中:
- :并发占用,包括正在运行的工作流、模型请求和工具调用;
- :输入 Token 速率;
- :输出 Token 速率;
- :GPU 计算或显存资源;
- :内存、连接池、文件句柄等容量;
- :数据库、搜索、HTTP、MCP 等外部依赖;
- :工作线程、进程、协程和任务槽位。
限流控制进入系统的速率或数量,容量描述系统在目标服务等级下可以稳定承载的工作量,队列保存暂时不能立即执行的工作,过载则是到达需求持续或瞬时超过某个关键资源的服务能力。
这几个概念必须同时考虑。只限制 HTTP 请求数,无法防止 Token 速率超限;只限制 Token 速率,无法防止工具连接池耗尽;只增加 GPU,无法解决外部 API 的低吞吐;只增加队列,则可能把“立即失败”变成“几十分钟后超时”。
一、先把 Agent 请求还原成资源消耗
1. Agent 请求不是一次模型调用
设一次 Agent 工作流包含:
- 初始模型调用;
- 工具调用;
- 根据工具结果再次调用模型;
- 可能的重试;
- 可能的子 Agent 或 handoff;
- 最终生成。
如果第 次模型调用消耗输入 Token 、输出 Token ,工具调用耗时为 ,则一次工作流的总模型消耗是:
这里的 不是固定常数。一个看似简单的请求,可能因为工具失败、模型判断不确定或重试而产生更多步骤。
因此,Agent 容量规划的基本单位不应只是“请求数”,而应至少记录:
- 每个工作流的步骤数;
- 每个步骤的输入、输出和总 Token;
- 工具调用次数及耗时;
- 重试次数;
- 并行分支数;
- 是否发生 handoff;
- 是否进入降级路径;
- 最终成功、超时、取消还是资源拒绝。
2. 工作流并发、模型并发和工具并发不是一回事
假设系统同时运行 100 个 Agent 工作流,但每个工作流只有一个模型步骤正在执行,模型并发可能接近 100。
如果每个工作流同时启动 4 个搜索工具调用,则:
此时工作流并发仍然是 100,但搜索服务、HTTP 连接池和下游 QPS 已经承受 400 个并行任务。
反过来,若一个工作流先调用模型,再长时间等待工具,那么工作流并发可能是 1000,而模型并发只有几十。此时限制模型并发并不能控制会话状态、内存、连接和队列占用。
实际系统通常需要至少三层并发限制:
工作流并发
├── 模型调用并发
├── 工具调用并发
├── 外部服务并发
└── 单租户 / 单会话并发
每一层都可能成为独立瓶颈。
二、并发:数量约束,不是吞吐量
1. 并发的定义
并发数表示某一时刻处于“占用资源但尚未完成”状态的任务数量。它不是单位时间内完成的任务数。
常见的并发指标包括:
active_workflows:正在运行的 Agent 工作流;active_model_calls:正在等待模型返回的请求;active_tool_calls:正在执行的工具调用;inflight_tokens:已提交但尚未完成的 Token 工作量;active_gpu_sequences:推理服务中正在处理的序列数。
“每秒处理 10 个请求”是吞吐量;“同时有 50 个请求运行”是并发数。两者通过平均完成时间联系起来,但不能互相替代。
2. Little 定律:从延迟和吞吐推算并发
稳定系统满足 Little 定律:
其中:
- :系统中的平均任务数,也就是平均并发数;
- :平均到达或完成速率;
- :任务从进入到完成的平均时间。
例如:
- 稳定吞吐为 2 个工作流/秒;
- 平均工作流耗时为 8 秒。
则平均并发为:
这只是平均值,不是安全上限。如果 P95 耗时是 30 秒,且系统需要覆盖突发流量,实际并发上限不能只按 16 设置。
3. 并发上限的作用
设置工作流并发上限 后,系统可以在入口处做出三种选择:
- 有空闲槽位:立即运行;
- 没有空闲槽位但队列未满:进入队列;
- 槽位和队列都满:拒绝、降级或异步化。
一个简单的状态机如下:
stateDiagram-v2
[*] --> Accepted: 请求到达
Accepted --> Running: 获得工作流槽位
Accepted --> Queued: 槽位已满且队列未满
Accepted --> Rejected: 队列已满或预算不足
Queued --> Running: 调度器分配资源
Running --> Completed: 成功完成
Running --> Failed: 不可恢复错误
Running --> Cancelled: 超时或客户端取消
Running --> Queued: 可重试任务重新排队
关键点是:排队状态本身也会消耗容量。排队任务需要占用消息队列、数据库记录、内存元数据、租户配额和任务租约。
三、Token 速率:模型容量的真正约束
1. Token 速率是什么
Token 速率描述单位时间内消耗或生成的 Token 数量。至少需要区分:
- 输入 Token 速率,通常包括历史消息、系统提示、工具定义和检索上下文;
- 输出 Token 速率;
- 总 Token 速率;
- 实时生成速率,即生成阶段的 tokens/s;
- 供应商或项目级 Token-per-minute 限制;
- 本地推理服务的实际吞吐。
设时间窗口为 ,期间完成的请求集合为 ,则总 Token 速率可以估算为:
但对 Agent 来说,这个公式还不够。因为一个正在运行的长请求可能已经占用大量 KV Cache 和 GPU 序列槽位,却尚未贡献到“完成请求”的统计中。因此容量系统应同时记录:
- 已完成 Token;
- 已发出但未完成的预估 Token;
- 当前上下文长度;
- 预留的最大输出长度;
- 重试造成的额外 Token。
2. 输入 Token 常被低估
一个 Agent 的输入不只是用户问题:
例如,一次包含 20 轮历史消息、30 个工具定义、两段检索结果的调用,用户输入可能只有 50 个 Token,但实际输入可能达到数万 Token。
如果工作流平均有 个模型步骤,则单请求 Token 需求近似为:
当每一步都携带不断增长的历史和工具结果时, 可能随步骤递增,使总 Token 消耗显著高于“初始上下文长度 × 步骤数”的粗略估计。
3. Token 预算必须在调度前预留
假设模型项目的安全 Token 预算为 tokens/min,当前窗口已经消耗 ,一个新任务预计需要 ,则只有在:
时才应立即准入。
这里的 不应只取平均值。更可靠的做法是按任务类型使用分位数估计:
例如:
| 任务类型 | 平均 Token | P95 Token | 预估值 |
|---|---|---|---|
| 简单问答 | 2,000 | 4,000 | 4,000 |
| 检索问答 | 8,000 | 18,000 | 18,000 |
| 多工具分析 | 20,000 | 60,000 | 60,000 |
如果所有任务都按平均值准入,长尾任务会在高峰期把 Token 配额耗尽,导致后续任务集中收到限流错误。
四、限流算法:控制到达速率和资源占用
1. 固定窗口的边界问题
固定窗口限流例如“每分钟最多 60 个请求”:
10:00:00—10:00:59:最多 60 个
10:01:00—10:01:59:最多 60 个
如果 10:00:59 集中进入 60 个请求,10:01:00 又集中进入 60 个请求,实际可能在两秒内收到 120 个请求。
固定窗口实现简单,但不适合对模型 Token 和昂贵工具做精细控制。
2. 令牌桶:允许有限突发
令牌桶有两个参数:
- :令牌生成速率;
- :桶容量,也就是最大突发额度。
每个请求消耗一个或多个令牌。对于 Token 限流,可以让请求按照预估 Token 数量消耗令牌:
from dataclasses import dataclass
import time
@dataclass
class TokenBucket:
rate_per_sec: float
capacity: float
tokens: float
last_ts: float
@classmethod
def create(cls, rate_per_minute: int, burst: int):
now = time.monotonic()
return cls(
rate_per_sec=rate_per_minute / 60.0,
capacity=float(burst),
tokens=float(burst),
last_ts=now,
)
def try_consume(self, cost: int) -> tuple[bool, float]:
now = time.monotonic()
elapsed = max(0.0, now - self.last_ts)
self.tokens = min(
self.capacity,
self.tokens + elapsed * self.rate_per_sec,
)
self.last_ts = now
if self.tokens >= cost:
self.tokens -= cost
return True, 0.0
missing = cost - self.tokens
wait_seconds = missing / self.rate_per_sec
return False, wait_seconds
bucket = TokenBucket.create(
rate_per_minute=60_000,
burst=12_000,
)
accepted, retry_after = bucket.try_consume(cost=8_000)
print(accepted, round(retry_after, 2))
这个示例的含义是:
- 平均允许 60,000 Token/min;
- 最多允许一次性消耗 12,000 Token;
- 一个预估需要 8,000 Token 的任务,会消耗 8,000 个令牌;
- 令牌不足时不应忙等,而应返回预计等待时间、进入有界队列或执行降级。
风险在于:如果每个请求一开始都按最大输出 Token 预留,容量会被过度保守;如果只按历史平均值预留,又会在长尾任务上失控。常见实现是“准入时按 P95 预留,完成后按实际值结算”。
3. 并发限流和 Token 限流必须同时存在
一个模型调用只有在同时满足以下条件时才能执行:
其中最后一个条件表示任务剩余截止时间必须足以完成预计工作。否则,即使当前有并发槽位,也不应启动一个必然超时的任务。
五、队列:把瞬时过载变成可管理的等待
1. 队列解决什么问题
队列解决的是到达速率的瞬时波动,不是永久容量不足。
设:
- 到达速率为 ;
- 服务速率为 。
当:
系统可以在平均意义上排空队列。
当:
队列会持续增长,除非拒绝、取消、降级或增加服务能力。
因此,“队列很大所以系统有容量”是错误的。无限队列只是把过载延后暴露为:
- 等待时间无限增长;
- 任务过期后仍被执行;
- 用户重复提交;
- 重试风暴;
- 消息堆积和存储成本增加。
2. 有界队列必须有等待预算
设任务截止时间为 ,估计运行时间为 ,当前预计等待时间为 ,则准入条件至少应满足:
例如:
- 用户请求总超时 30 秒;
- 当前队列预计等待 18 秒;
- Agent 执行 P95 为 20 秒。
则:
这个任务不应继续排队。继续接受只会制造一个稍后必然失败的任务。
队列应保存以下信息:
{
"task_id": "task_123",
"tenant_id": "tenant_a",
"priority": 20,
"estimated_tokens": 18000,
"deadline_at": "2026-09-01T10:00:30Z",
"retry_count": 0,
"idempotency_key": "tenant_a:request_456",
"state": "queued"
}
其中 estimated_tokens 和 deadline_at 是调度所必需的,不是仅用于监控的附加字段。
3. FIFO 不等于公平
先进先出队列容易实现,但会产生队头阻塞:
- 前面的任务需要执行 10 次模型调用;
- 后面的任务只需要一次简单回答;
- 后面的短任务必须等待前面的长任务完成。
更重要的是,单租户大量提交任务时,FIFO 会让一个租户占满整个系统。
实际调度通常需要组合:
- 租户级配额;
- 优先级;
- 每租户并发上限;
- 加权公平队列;
- 老化机制;
- 截止时间优先;
- 任务类型隔离。
一种简单的公平策略是为每个租户维护虚拟完成时间:
其中:
- :租户 当前任务的虚拟完成时间;
- :系统虚拟时间;
- :任务成本,可以是预计 Token、GPU 秒或执行时间;
- :租户权重。
调度器优先选择 最小的任务。这样,一个 Token 需求很大的任务会自然承担更高的调度成本,而不是只因为“先到”就长期占用资源。
六、GPU 配额:不是显卡数量,而是可调度的推理资源
1. GPU 配额与 GPU 容量的区别
GPU 配额是平台允许某个租户、项目或服务使用的上限,例如:
- 最多使用多少张 GPU;
- 最多创建多少个推理副本;
- 最多使用多少 GPU 时间;
- 最大显存或 MIG 分片;
- 最大并行序列数;
- 最大 batch size;
- 最大并发模型请求数。
GPU 容量是实际能达到的服务能力,例如:
- 每秒生成多少 Token;
- 在给定上下文长度下支持多少并发序列;
- P95 TTFT 是否仍满足目标;
- 工具编排和调度开销是否可接受。
拥有 8 张 GPU,不代表可以线性支持 8 倍吞吐。原因包括:
- 模型并行通信;
- 显存被 KV Cache 占用;
- 长上下文降低可容纳序列数;
- batch 过小导致利用率低;
- batch 过大导致 TTFT 上升;
- CPU、网络或存储成为瓶颈;
- 不同请求的输入输出长度分布不同。
2. GPU 推理的两个阶段
自托管 LLM 推理通常可以分成:
- Prefill:处理输入上下文,影响 TTFT;
- Decode:逐步生成输出,影响持续生成吞吐。
长输入任务可能主要消耗 Prefill 计算和显存;长输出任务则长期占用 Decode 槽位。
因此只看平均 tokens/s 不够。应分别测量:
其中:
TTFT是首 Token 延迟;TPOT是平均每输出 Token 的时间;- 是输出 Token 数。
一个系统可能总吞吐很高,但 TTFT 已经超过交互式请求的 SLA;也可能 TTFT 很低,但长输出任务使 GPU 长时间被占用。
3. 用 GPU 秒做成本和容量单位
对不同长度的 Agent 任务,可以估算 GPU 时间:
如果一张 GPU 在目标 SLA 下可提供 个有效 GPU 秒/秒,则 张 GPU 的理论容量近似是:
但有效容量还要乘以安全利用率 :
不是“让 GPU 利用率保持 70%”这样的固定经验数字,而是通过压测确定的工作点。超过该点后,队列、TTFT 或 OOM 可能非线性恶化。
4. 长上下文会降低并发容量
KV Cache 大致随以下因素增长:
其中:
- :当前并发序列数;
- :上下文长度;
- :与模型层数、注意力头数和数据类型相关的常数;
- :每个序列的批处理或缓存开销。
这意味着将上下文长度从 8k 提升到 32k,不只是让单请求变慢,也可能显著降低同一 GPU 能容纳的并发序列数。
常见错误是按照“GPU 显存 ÷ 模型权重大小”计算容量,却忽略 KV Cache、激活、运行时缓冲区和通信缓存。
七、容量规划:先找瓶颈,再计算上限
1. 多资源容量取最小值
对于同一类 Agent 任务,系统安全吞吐量可以近似写成:
每个 都应该是在目标 SLA 下的安全服务速率,而不是理论峰值。
如果:
- 工作流调度器最多 20 个任务/秒;
- 模型 Token 配额支持 12 个任务/秒;
- GPU 在 P95 TTFT 目标下支持 10 个任务/秒;
- 搜索工具支持 15 个任务/秒;
则系统容量是:
增加调度器线程不会提高整体容量,因为 GPU 已经是瓶颈。
2. 完整算例:多步骤 Agent
假设一个客服 Agent 的 P95 工作流特征如下:
- 每个请求平均 3 次模型调用;
- 总输入 Token:12,000;
- 总输出 Token:3,000;
- 平均工具调用:2 次;
- 工作流 P95 耗时:18 秒;
- 目标吞吐:每秒 5 个请求;
- 模型项目 Token 配额:每分钟 2,000,000 Token;
- 模型调用并发上限:40;
- 工具服务安全吞吐:每秒 12 次;
- 单个工作流最多允许 4 个并行工具调用。
第一步:计算 Token 速率
每个工作流需要:
目标吞吐为 5 请求/秒,因此需要:
换算为每分钟:
超过项目配额 2,000,000 Token/min,因此仅凭该模型项目无法支持每秒 5 个请求。
Token 约束下的最大吞吐是:
第二步:计算模型并发容量
按 P95 工作流耗时粗略估算:
但系统只有 40 个模型调用并发槽位。若每个工作流平均有 3 次模型调用,模型调用的平均占用不能简单等同于整个工作流耗时,还需要测量模型调用实际耗时。
假设模型调用总耗时占工作流的 40%,则:
需要的模型调用并发为:
因此模型并发上限 40 足够接近目标,但余量很小。一旦重试或长尾增加,模型并发会成为瓶颈。
第三步:计算工具容量
每请求 2 次工具调用,目标吞吐 5 请求/秒:
工具服务安全吞吐为 12 calls/s,理论上满足目标,但只有 20% 余量。
第四步:得出系统容量
系统瓶颈是 Token 配额,而不是 GPU、模型并发或工具服务。
第五步:计算队列增长
如果实际到达速率为 5 请求/秒,服务速率只有 2.22 请求/秒,则每秒净增加:
10 分钟后,理论堆积约为:
这还没有计入任务过期、用户取消和重试。如果队列不设上限,系统最终会同时出现高延迟、Token 配额耗尽和失败重试。
可行的修复方向包括:
- 将目标吞吐降至 Token 配额允许的范围;
- 将简单任务路由到更低成本模型;
- 缩短历史和工具定义;
- 减少无必要的模型步骤;
- 对长任务改为异步;
- 为不同租户分配独立 Token 预算;
- 增加模型项目配额,但不能假设配额增加后 GPU 也自动足够。
八、调度器必须处理“预估成本”和“实际成本”
1. 任务准入
一个任务进入运行态前,应经过准入检查:
from dataclasses import dataclass
from time import monotonic
@dataclass
class TaskEstimate:
workflow_slots: int
model_calls: int
estimated_tokens: int
estimated_seconds: float
priority: int
deadline_seconds: float
@dataclass
class Capacity:
workflow_used: int
workflow_limit: int
model_used: int
model_limit: int
token_available: int
def admit(task: TaskEstimate, cap: Capacity) -> tuple[bool, str]:
if cap.workflow_used + task.workflow_slots > cap.workflow_limit:
return False, "workflow_concurrency"
if cap.model_used + task.model_calls > cap.model_limit:
return False, "model_concurrency"
if task.estimated_tokens > cap.token_available:
return False, "token_rate"
if task.estimated_seconds > task.deadline_seconds:
return False, "deadline"
return True, "accepted"
task = TaskEstimate(
workflow_slots=1,
model_calls=3,
estimated_tokens=18_000,
estimated_seconds=12,
priority=10,
deadline_seconds=30,
)
capacity = Capacity(
workflow_used=7,
workflow_limit=8,
model_used=20,
model_limit=24,
token_available=50_000,
)
print(admit(task, capacity))
# (True, 'accepted')
这个示例只演示准入逻辑,生产实现还需要原子地扣减配额。若先检查、后扣减,中间存在并发竞争:
任务 A 检查:剩余 20,000 Token,需求 15,000,允许
任务 B 检查:仍看到剩余 20,000 Token,需求 15,000,允许
任务 A 扣减
任务 B 扣减
实际超出配额 10,000 Token
因此 Redis、数据库事务或集中式调度器需要使用原子操作,不能依赖应用进程内的普通变量。
2. 完成后的结算
预估值和实际值必须分开记录:
准入时:
reserve_tokens = P95 预估值
执行中:
actual_tokens = 已产生的真实 Token
actual_steps = 已完成的步骤
actual_tool_calls = 工具调用数
结束时:
释放 reserve_tokens - actual_tokens
记录超估 / 低估比例
如果一个任务预估 20,000 Token,实际只消耗 8,000 Token,则应释放未使用的预算;如果实际消耗 35,000 Token,则需要:
- 截断后续步骤;
- 降低输出上限;
- 进入成本预算保护;
- 记录估算模型失准;
- 防止无限追加 Token。
九、过载:从入口到故障的完整路径
过载不是单一错误,而是资源耗尽沿调用链传播的过程。
flowchart LR
A[请求突增] --> B[入口限流]
B -->|准入| C[工作流队列]
B -->|拒绝/降级| R[429或降级响应]
C --> D[调度器]
D --> E[模型并发池]
D --> F[工具并发池]
E --> G[Token配额]
E --> H[GPU推理]
F --> I[外部服务]
G -->|不足| X[等待/降级/拒绝]
H -->|显存或排队| Y[TTFT升高/OOM]
I -->|超时| Z[重试]
Z --> C
Y --> Z
1. 过载的常见阶段
阶段一:队列增长
表现:
- 活跃任务数接近上限;
- 队列长度持续增长;
- CPU 和 GPU 可能仍不高;
- P50 延迟开始上升。
原因可能是:
- Token 配额耗尽;
- 外部工具变慢;
- 下游 API 限流;
- 工作流步骤变多;
- 调度器没有及时释放资源。
阶段二:等待时间超过截止时间
表现:
- 任务还没有开始执行就已经超时;
- 用户重复提交;
- 客户端重试;
- 同一任务出现多个执行副本。
这说明队列没有把 deadline 纳入调度。对交互式任务而言,过期任务应在出队时丢弃,而不是继续消耗模型和工具资源。
阶段三:重试风暴
如果每个失败请求都立即重试 次,到达系统的实际工作量可能从 放大为:
其中 是一次尝试失败后再次重试的概率。
当 较高时,重试并非恢复手段,而是进一步增加过载。重试必须具备:
- 指数退避;
- 随机抖动;
- 最大次数;
- 只对幂等错误重试;
- 截止时间检查;
- 租户级重试预算;
- 对 429、超时、5xx 分别处理。
阶段四:级联耗尽
模型变慢会导致工作流占用时间变长;工作流变长会导致并发槽位不释放;并发槽位耗尽后队列增长;队列增长后客户端重试;重试又进一步消耗模型 Token 和 GPU。
这是典型的正反馈:
此时单纯“把并发上限调大”通常会使系统更快进入 GPU OOM、连接池耗尽或下游雪崩。
十、过载保护:拒绝、降级和取消必须有层次
1. 拒绝
适合以下情况:
- 队列已满;
- 预计等待时间超过 deadline;
- 租户配额耗尽;
- Token 预算不足;
- GPU 或模型资源不可用;
- 请求缺少幂等键,无法安全排队。
同步 API 应返回可区分的错误,例如:
{
"error": {
"code": "capacity_exceeded",
"reason": "token_rate",
"retryable": true,
"retry_after_seconds": 8
}
}
不要把所有资源问题都返回成普通 500。500 表示服务内部错误,而容量拒绝是系统有意识的保护行为。
2. 降级
降级不是简单地“换一个模型”。应明确降级对工作流语义的影响:
| 原路径 | 降级路径 |
|---|---|
| 多轮规划 | 单轮结构化回答 |
| 复杂检索 | 仅使用缓存 |
| 多工具并行 | 只调用关键工具 |
| 长输出 | 限制输出长度 |
| 高级模型 | 更低成本模型 |
| 实时执行 | 转为异步任务 |
降级路径也必须独立限流,否则所有请求在主路径失败后涌入备用模型,形成第二次过载。
3. 取消
取消是释放容量的重要机制。客户端断开连接并不一定自动停止后端 Agent。后端必须将取消信号传播到:
- 工作流运行器;
- 当前模型请求;
- 工具调用;
- 子 Agent;
- 等待中的重试;
- 队列租约。
如果只取消前端响应而不取消后端执行,用户虽然看到了超时,GPU 和工具仍然继续工作。
十一、观测:必须能把容量问题定位到步骤
平均延迟无法说明 Agent 为什么变慢。至少需要同时观察:
1. 容量指标
workflow_active
workflow_queue_depth
workflow_queue_wait_seconds
model_inflight
tool_inflight{tool}
token_reserved
token_actual
token_estimation_error
gpu_memory_used
gpu_active_sequences
gpu_prefill_utilization
gpu_decode_utilization
2. 延迟指标
request_latency
queue_wait
model_ttft
model_generation_time
tool_latency{tool}
step_latency{step_type}
retry_delay
3. 结果指标
completed
rejected
deadline_exceeded
cancelled
model_rate_limited
tool_timeout
gpu_oom
budget_exceeded
degraded
监控必须按以下维度切分:
- 租户;
- Agent 类型;
- 模型;
- 任务等级;
- 工具;
- 输入长度区间;
- 输出长度区间;
- 是否重试;
- 是否降级;
- 区域和实例。
4. 用 Trace 还原一次工作流
Agent 容量问题经常需要回答:
- 哪一步占用了最长时间?
- 是排队慢,还是模型慢?
- Token 是哪次调用增长的?
- 哪个工具触发了重试?
- 是否发生 handoff?
- 取消后是否仍有后台任务运行?
OpenAI Agents SDK 的 tracing 将一次工作流表示为 trace,并在其中记录模型生成、工具调用、handoff、guardrail 和自定义事件;span 具有开始和结束时间,并通过父子关系形成层级。(openai.github.io)
在长时间运行的 Worker 中,默认 trace 可能异步导出,因此任务结束后不一定立即出现在仪表盘;如果需要在任务边界保证当前缓冲数据已导出,可以在 trace 上下文退出后调用 flush_traces()。该调用会等待当前缓冲的 trace 和 span 导出完成,因此不应在 trace 尚未结束时调用。(openai.github.io)
一个概念性的埋点结构如下:
from agents import Runner, trace, custom_span, flush_traces
async def execute_task(agent, prompt, task_id):
try:
with trace(
"customer_agent",
metadata={"task_id": task_id},
):
with custom_span(
"capacity_admission",
data={"estimated_tokens": 18000},
):
pass # 在这里执行原子准入和资源预留
result = await Runner.run(agent, prompt)
return result
finally:
flush_traces()
这里的 capacity_admission 不是模型调用,而是为了让“排队、准入和资源预留”出现在同一条工作流证据链中。实际使用时,应根据 SDK 版本确认自定义 span 的参数形式,并避免将用户隐私、完整提示词或敏感工具参数直接写入 trace。
官方 Agent Evals 文档建议:调试阶段先从单条 trace 观察工作流级问题,再将代表性样本转成可重复运行的数据集和评测;trace 中可以定位模型调用、工具调用、guardrail 和 handoff 等行为。(developers.openai.com)
容量变更也应纳入评测。例如,增加并行工具调用后,不只评估答案正确率,还要评估:
- 平均工具调用数是否增加;
- P95 Token 是否增加;
- P95 TTFT 是否恶化;
- 超时率是否上升;
- 降级率是否上升;
- 每个成功任务成本是否变化。
十二、常见误解与反例
误解一:CPU 不高,所以系统还有容量
反例:模型请求都在等待远端 API,CPU 很低,但 Token 配额已耗尽,队列持续增长。CPU 利用率只能说明 CPU 资源,不说明模型、网络或外部服务容量。
误解二:GPU 利用率 100% 就代表吞吐最高
反例:GPU 利用率达到 100% 时,长上下文请求使 TTFT 从 1 秒升至 15 秒。虽然硬件利用率更高,但交互式 SLA 已经失败。容量目标必须同时约束吞吐、TTFT、P95 延迟和错误率。
误解三:提高并发上限就能提高吞吐
反例:GPU 实际只能稳定处理 32 个序列,却把应用并发从 32 调到 128。结果是更多请求进入 GPU 等待区,TTFT 增长,KV Cache 增加,最终 OOM 或超时。并发上限应该围绕目标工作点设置,而不是追求更大的数字。
误解四:队列越长,系统越稳定
反例:用户请求的截止时间是 30 秒,队列等待已经 60 秒。此时继续排队只会浪费存储、调度和重试资源。队列必须有长度上限和时间上限。
误解五:一次用户请求只算一个 Token 预算
反例:Agent 进行 4 次模型调用,每次都携带历史、工具定义和前一步结果。用户输入 1,000 Token,不代表工作流只消耗 1,000 Token。预算应按整个运行周期累计。
误解六:429 一律重试
反例:Token 配额在当前窗口内已经用尽,所有客户端立即重试,只会增加失败请求。只有在有明确恢复时间、剩余 deadline 足够且重试不会破坏幂等性的情况下,429 才适合退避重试。
十三、生产诊断路径
遇到“Agent 变慢或大量失败”时,按资源路径排查,而不是先修改一个超时时间。
第一步:确认是排队还是执行变慢
比较:
如果 QueueWait 占比高,问题在准入、调度、配额或服务速率;如果 Execution 占比高,再继续拆分模型和工具。
第二步:确认是否是 Token 速率瓶颈
检查:
- 实际 Token 速率;
- 预留 Token 速率;
- 输入 Token 是否突然增长;
- 输出是否出现长尾;
- 重试是否重复消耗 Token;
- 某个租户是否占满预算。
若只有 Token 使用接近上限而 GPU 空闲,增加 GPU 没有帮助。
第三步:确认是否是模型并发或 GPU 瓶颈
检查:
- 模型 inflight;
- active sequences;
- TTFT;
- Prefill 和 Decode;
- KV Cache;
- 显存峰值;
- OOM 和批处理退化。
若 GPU 已进入非线性延迟区,应降低并发或输入长度,而不是继续增加队列。
第四步:确认是否是工具或外部依赖瓶颈
按工具名称查看:
- 调用速率;
- P50/P95/P99 延迟;
- 超时;
- 限流响应;
- 重试次数;
- 连接池使用率;
- 单请求并行度。
如果所有 Agent 都在等待同一个搜索工具,问题可能不在模型层。
第五步:确认过载是否被重试放大
计算:
如果这个值在故障期间快速增长,优先降低重试上限、增加退避和启用熔断,而不是增加 worker 数量。
十四、把容量策略固化为服务契约
一个可运营的 Agent 服务,应明确以下契约:
单租户最大工作流并发:N
全局最大工作流并发:M
模型调用最大并发:K
单分钟 Token 预算:Q
单任务最大步骤数:S
单任务最大工具调用数:T
队列最大长度:L
最大排队时间:W
最大运行时间:D
最大重试次数:R
超载行为:拒绝 / 降级 / 异步
取消传播:是 / 否
幂等策略:键、租约、去重窗口
这些参数之间必须相互一致。例如:
- 最大运行时间 30 秒;
- 最大排队时间 20 秒;
- P95 执行时间 18 秒。
这三个配置显然不一致,因为排队和执行的总和已经超过请求总预算。容量配置不是独立开关,而是一个约束系统。
可以把单任务准入写成:
只有所有条件同时成立,任务才进入运行态。否则,调度器必须明确选择拒绝、等待、降级或异步化,而不是无条件启动。
Agent 的稳定容量不是某个单一数字,而是一个由工作流结构、Token 速率、并发槽位、队列等待、GPU 工作点、工具依赖和截止时间共同决定的可行区域。
当到达速率低于最小瓶颈服务速率时,队列可以吸收有限突发;当到达速率超过该速率时,系统必须通过拒绝、降级、取消或扩容改变负载。真正可靠的限流系统,不是让更多请求“进入”,而是在资源耗尽之前,准确判断哪些任务值得执行、哪些任务应等待、哪些任务已经不应再消耗系统容量。
系列导航与关联阅读
- 系列入口:Agent 工程完整路线:从运行循环、记忆与协议到安全、评测和生产交付
- 上一篇:Agent 多租户系统:数据、模型、工具、记忆、配额和密钥隔离
- 下一篇:Agent 流式事件协议:Delta、Tool、Usage、Finish、断线和恢复
- 延伸:Agent 并发与调度:会话隔离、任务池、优先级、公平和资源配额
- 延伸:Agent 延迟与成本:TTFT、步骤、Token、工具耗时、预算和降级
官方资料
本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论