Agent 性能问题优化
从链路拆分、模型路由、并行工具、上下文预算到缓存与评测,定位并优化 Agent 的延迟、成本和吞吐。
Agent 的“慢”很少只慢在模型。一次任务可能依次经历鉴权、加载历史、检索知识、模型规划、多个工具、再次推理和结果持久化。盯着总耗时看,只会得到一个没有行动价值的数字。
优化前先把问题分成四个目标:
- 延迟:用户多久看到首个反馈,多久拿到完整结果;
- 吞吐:系统同时能处理多少 run;
- 成本:每个成功任务消耗多少模型与基础设施费用;
- 质量:任务是否真的完成,工具和引用是否正确。
它们经常互相拉扯。换小模型可能更快,却增加重试;提高并发可能提升吞吐,也可能把下游 API 打挂;砍掉上下文可以省 token,却让 Agent 忘记关键约束。
先画出一次运行的瀑布图
为每个 run、模型调用和工具调用记录 trace,并至少采集:
| 指标 | 说明 |
|---|---|
| queue time | 任务等待 worker 的时间 |
| time to first token | 从请求到首个可见 token |
| model latency | 每次模型调用耗时 |
| tool latency | 每个工具的排队、连接和执行耗时 |
| input/output tokens | 输入和输出规模 |
| step count | 一次 run 经过多少轮模型与工具 |
| success rate | 不靠人工补救的任务完成率 |
| p50 / p95 / p99 | 常态与长尾延迟 |
总耗时拆开后,优化顺序通常很直白:若 70% 时间耗在串行工具上,换一个生成速度快 10% 的模型意义不大;若工具很快但模型来回规划八轮,就该先减少 step。
平均数还会掩盖问题。用户最容易遇到的是 p95 的超时、冷启动和限流,因此容量规划应围绕分位数,而不是只看一次顺利演示。
减少没有必要的模型轮次
每次模型请求都有网络往返、排队和生成成本。常见浪费包括:先用一轮判断要不要检索,再用一轮改写查询;让模型提取一个可以用确定性代码解析的字段;工具返回后无条件再让模型总结一次。
可以按以下顺序检查:
- 能用普通代码、SQL 或规则完成的,不调用模型;
- 能合并到同一轮的判断与结构化输出,合并处理;
- 彼此独立的模型或工具请求并行执行;
- 必须串行的步骤保留,因为后一步确实依赖前一步结果。
不要为了“单次调用”把互不相关的十件事塞进一个巨大 prompt。请求变少不代表系统必然更快,超长上下文和复杂输出同样会拖慢推理,还会降低可测试性。
模型路由按任务难度,而不是按页面
同一 Agent 中的步骤难度差别很大:分类、意图识别、格式转换往往不需要最强模型;跨文档推理、复杂代码修改和最终审核才值得使用能力更强的模型。
路由可以综合任务类型、上下文长度、风险等级与历史失败率:
type TaskClass = 'classify' | 'retrieve' | 'reason' | 'review'
function chooseModel(task: TaskClass, risk: 'low' | 'high') {
if (risk === 'high' || task === 'reason' || task === 'review')
return 'quality-tier'
return 'fast-tier'
}
这里的字符串应映射到配置,而不是写死某家厂商的型号。切换策略前用同一组评测题比较成功率、延迟和成本,不能只看单价。
还可以设计升级路径:快速模型先处理,只有低置信、格式校验失败或工具选择异常时才升级。要把升级条件写成可观测规则,否则“模型自评信心不足”很容易失真。
控制上下文,而不是粗暴清空历史
输入过长会增加成本、传输与处理时间,也可能让关键指令淹没在噪声里。上下文应按用途分层:
- 固定且稳定的系统指令;
- 当前任务必须知道的业务状态;
- 最近对话窗口;
- 旧对话的结构化摘要;
- 本轮检索出的证据;
- 真正可能使用的工具定义。
为每一层设置 token 预算。超预算时优先删除重复工具结果、无关寒暄和已被摘要覆盖的历史,不要从字符串末尾直接截断,因为最末尾可能正好包含用户最新要求。
工具很多时,先按意图搜索候选工具,再只暴露少量定义。几十个冗长 JSON schema 每轮全部发送,既占上下文,也提高选错工具的概率。
让稳定前缀更容易缓存
不少模型服务支持 prompt prefix caching。常见前提是前缀逐项一致,因此应把系统规则、固定工具定义、长期不变的示例放在前面,把用户输入、时间戳、请求 id 等动态内容放在后面。
具体匹配规则仍要看模型服务。以当前 OpenAI GPT-5.6 为例,默认的隐式断点可能包含最新消息中的动态内容;只调整顺序不一定命中。可在稳定前缀末尾设置显式 breakpoint,并按文档配置 prompt_cache_key 与缓存模式。该系列还会单独计量 cache write,因此要同时观察 cache_write_tokens、cached_tokens、延迟和实际费用,不能把“写入了缓存”等同于“已经省钱”。
供应商的 prompt_cache_key 用于路由或前缀匹配,不等于应用自己的结果缓存,也不能代替权限校验。下面这些应用侧缓存,key 才需要纳入租户、权限、prompt/索引版本和模型版本,避免跨用户复用不该共享的结果:
应用侧也可以缓存确定性结果:
- embedding 与文档解析结果按内容哈希缓存;
- 只读工具按参数、权限和数据版本缓存;
- 查询改写和 rerank 结果按查询、权限与索引版本设置短 TTL;
- 完整回答只有在来源、身份和时效边界明确时才缓存。
工具写操作、个人数据和依赖实时状态的结果不适合随意缓存。
并行工具要有边界
天气与日历查询互不依赖,可以并行;“创建文档后把链接发到群里”则必须串行。执行器可以根据依赖关系组成 DAG,而不是把所有 tool call 一股脑 Promise.all。
并行还要受控:
- 每个用户、租户和工具设置并发上限;
- 对外部 API 使用连接池、超时和指数退避;
- 遇到
429尊重服务端的重试提示; - 使用带抖动的退避,避免所有 worker 同时重试;
- 非关键工具失败时允许降级,不拖死整次 run;
- 写操作用幂等键,避免超时后重复创建。
当下游已经拥塞时,继续提高并发只会扩大排队。系统需要背压:限制新任务进入、把非实时任务移入队列,并在界面上显示真实等待状态。
流式输出优化的是感知延迟
流式响应不一定缩短总耗时,但能显著改善首屏反馈。页面可以先展示任务已接收、当前步骤和工具进度,再逐段显示文本。
不过不要流出尚未确认的高风险结论。涉及审批、付款或删除时,先渲染结构化操作预览;检索型回答也可以等引用绑定完成后再展示完整句子,避免后续大幅撤回。
对于长任务,前端不应一直维持一个脆弱连接。服务端把事件持久化,客户端断线后按 event id 续传,后台 run 则继续执行。
RAG 的性能先查召回链路
知识库场景常见的慢点包括查询改写、两路召回、rerank 和过多片段进入生成。可以这样处理:
- 关键词与向量检索并行;
- 先用元数据过滤缩小搜索空间;
- 控制召回候选和 rerank 数量;
- 对相同文档版本缓存 embedding;
- 去重后再组上下文,避免重复 token;
- 低风险、高置信 FAQ 可走直接命中快路径。
不要只为提速降低 Top K。先看评测集中正确证据的 Recall@K,再决定能削到哪里。召回少一次错误答案,可能比多花几十毫秒更重要。
把失败控制在预算内
一次 run 应有明确预算:最大模型轮次、工具调用数、总 token、总耗时和金额。达到上限时返回已完成步骤、失败原因和可恢复状态,而不是无限循环“换一种方式再试”。
重试策略按错误分类:
- 网络抖动、临时限流:带抖动的指数退避;
- 参数校验失败:最多让模型修正一次;
- 权限拒绝:不重试,直接请求授权或换方案;
- 业务校验失败:保留证据并交给用户处理;
- 不确定的写操作结果:先按幂等键查询状态,不直接重放。
熔断器可以隔离持续故障的工具,降级响应则告诉模型该能力暂不可用。让模型在工具已坏时反复尝试,只会把一次外部故障放大成整个平台拥塞。
优化后必须做回归评测
性能改动常以质量退化为代价。每次调整模型、prompt、上下文、并发或检索参数,都应在固定评测集上比较:
任务成功率 / 工具选择准确率 / 引用忠实度
p50、p95 首 token 与总延迟
平均模型轮次 / 输入输出 token / 单次成功成本
超时率 / 重试率 / 人工接管率
最值得优化的指标通常是“每个成功任务的成本和耗时”,而不是“每次模型调用”的数字。一个便宜但经常失败的方案,会通过重试和人工补救把总成本推得更高。
面试回答可以怎么组织
遇到“怎么优化 Agent 性能”,先说明性能包含延迟、吞吐、成本和质量;然后提出用 trace 拆解模型、检索和工具耗时;接着按瓶颈讨论减少轮次、模型路由、上下文预算、缓存、并行和背压;最后补上预算、降级与评测闭环。
比起罗列“换小模型、加缓存”,能说清楚测量方式和质量边界,更接近真实工程问题。