之前做 Agent 应用时遇到一个很典型的现象:用户点了一下按钮,前端就开始转圈圈,一等就是十几秒,甚至几十秒,体感上总觉得“这个模型太慢了”。后来把链路拆开分析才发现,模型单次推理其实只需要一两秒,真正把时间吃掉的是 Agent 在后台不停地“问”——一次任务可能问了模型几十次,累计下来,端到端延迟自然就爆炸了。
这篇文章想把“Agent 慢的真正原因”这件事讲透。我们会从延迟模型拆解开始,聊到 token 消耗的放大效应,再结合 Harness 这类评测框架说明如何量化 Agent 的调用链路,最后给出减少无效询问的具体优化策略。不管你是正在做 AI Agent 应用开发,还是准备接入大模型做自动化流程,这篇文章都能帮上忙。
1. 背景与核心概念
1.1 什么叫“Agent 慢”
在大模型应用里,Agent 通常指一个能自主完成任务的智能体:它接收用户目标,自己决定调用哪些工具、按照什么顺序执行、中间如何修正结果,直到最终产出答案。
比如一个“查天气并安排出行”的 Agent,流程可能是:
- 理解用户需求,生成初始计划。
- 调用天气查询工具,获取天气数据。
- 根据天气结果,调用地图搜索工具找路线。
- 汇总信息,生成出行建议。
这个过程中,每一步“思考”都可能触发一次大模型调用。问题就出在这里:用户感知到的“Agent 很慢”,往往不是某一次模型推理慢,而是 Agent 在同一个任务里反复调用模型,把单次延迟累加成了几十倍的端到端延迟。
1.2 单次推理延迟与端到端延迟的区别
很多人在排查性能问题时会陷入一个误区:只看单次模型响应时间。比如模型接口返回需要 1.5 秒,看起来很快,那就没问题。
但 Agent 的真实耗时公式是这样的:
code复制端到端延迟 ≈ 单次 LLM 调用延迟 × 调用次数 + 工具执行时间 + 网络开销 + 排队时间
这里最关键的是“调用次数”。如果一次任务里模型被调用了 30 次,即使每次只要 1.5 秒,光模型耗时就已经 45 秒了。再加上工具调用的时间,用户在页面上看到的就是一个漫长的等待过程。
换句话说,模型快不快是下限,Agent 问得次数多不多才是上限。这是理解本篇文章核心论点的基础。
1.3 Agent 为什么会“问”那么多次
Agent 的典型执行模式是 ReAct(Reasoning + Acting),也就是“推理-行动-观察”循环。逻辑上看起来没问题,但实际工程实现中,很多 Agent 框架会把一些不需要模型介入的环节也交给模型处理,导致调用次数失控,常见的情况包括:
- 工具返回结果后,Agent 要“总结”一下结果,产生一次调用。
- 每执行完一个子步骤,Agent 都要重新规划下一步,又产生一次调用。
- 模型生成的内容格式不合法,触发重试,再产生一次调用。
- 强行要求模型输出“最终答案”之前先做长篇推理,白白增加 token 消耗。
这些调用里有一部分是必须的,但有相当一部分是可以通过工程手段减少的。后面第 6 节会针对这些场景给优化方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 延迟模型拆解:先算清楚慢在哪里
2.1 建立量化视角
在做 Agent 性能优化之前,建议先给 Agent 建立一套延迟模型,用数据说话。基本公式可以展开成:
code复制总延迟 = 模型调用延迟(累计) + 工具调用延迟(累计) + 调度与 IO 开销
其中模型调用延迟又可以细分:
- prefill 时间:处理用户输入和历史上文的时间,跟输入 token 数成正比。
- decode 时间:生成输出 token 的时间,跟输出 token 数成正比。
- 排队时间:服务端负载过高时请求排队等待的时间。
很多人在工程实践里低估了 prefill 时间。Agent 每一轮都会把之前所有对话历史重新发送给模型,历史越长,prefill 越慢,这是一个典型的二次方增长问题:输入越长,处理越久,token 越多;token 越多,下一轮输入更长,处理更久,形成恶性循环。
2.2 一个简单的延迟估算示例
假设一个 Agent 完成一次任务需要调用模型 20 次,单次模型响应平均耗时 2 秒,工具调用平均耗时 0.5 秒。
code复制模型调用总耗时 = 20 × 2s = 40s
工具调用总耗时 = 20 × 0.5s = 10s
端到端耗时 ≈ 40s + 10s + 调度开销 ≈ 50s+
如果通过优化把调用次数从 20 次降到 6 次,那么模型耗时变成 12 秒,工具耗时变成 3 秒,端到端延迟直接下降了 70% 左右。
下面给出一个不同调用次数下的延迟预估表,方便你建立直观感受(假设单次模型响应 2 秒,工具调用 0.5 秒):
| 调用次数 | 模型累积耗时 | 工具累积耗时 | 端到端预估 |
|---|---|---|---|
| 5 次 | 10 秒 | 2.5 秒 | 约 13 秒 |
| 10 次 | 20 秒 | 5 秒 | 约 25 秒 |
| 20 次 | 40 秒 | 10 秒 | 约 50 秒 |
| 30 次 | 60 秒 | 15 秒 | 约 75 秒 |
从这个表可以直观看到,调用次数翻一倍,端到端延迟基本跟着翻一倍。这还没有考虑长上下文导致单次 prefill 变慢的情况,实际差距会更大。
2.3 工具调用占比与模型调用占比
很多 Agent 框架会把日志打印出来,但默认只显示每次的耗时,不够直观。建议把延迟拆成两个核心维度,做一次占比分析:
- 模型调用占总延迟的百分比。
- 工具调用占总延迟的百分比。
- 其他开销(JSON 解析、网络传输、回调等)占比。
通常你会发现,模型调用占比可以达到 70% 到 90%。这恰恰说明“Agent 慢,不是模型慢,是问得太多”——问一次不慢,问几十次就慢了。
3. 核心瓶颈:Token 消耗与“问得太多”的关系
3.1 每次调用都在“重复搬运”历史
LLM API 本身是无状态的,每次请求都要携带完整的对话历史。Agent 每执行一个步骤,新的工具结果、新的模型输出都会追加到历史里,下一轮调用把它全部发出去。
这意味着两个问题:
- 模型需要处理的输入越来越长,单次推理越来越慢。
- 你的账单上 token 消耗在快速上涨,成本与轮次呈二次方关系增长。
可以简单算一笔账:
code复制第 1 轮输入 1000 token
第 2 轮输入 1000 + 500 = 1500 token
第 3 轮输入 1500 + 500 = 2000 token
...
如果执行 20 轮,累计发送的输入 token 大概是:
code复制1000 + 1500 + 2000 + ... + 10000 ≈ 110000 token
而理想情况下,完成这个任务可能只需要一轮对话,输入 token 可能只有 2000 左右。这中间的差距,就是“问得太多”带来的放大成本。
3.2 无效调用与低质量调用
除了历史累积带来的成本增长,Agent 慢还跟“无效调用”密切相关。无效调用指那些没有对最终结果产生帮助的模型请求,常见形式有:
- 重复查询同一个工具,获取的结果一样,但 Agent 不相信旧结果,非要再问一次。
- 模型生成的 JSON 格式不合法,框架自动重试三次,每次都是一次完整的新请求。
- Agent 已经拿到足够信息,但缺少显式的终止条件,还在继续“反思”和“总结”。
- 多个步骤之间本可以并行,但框架默认串行执行,导致模型一次次空等。
这些问题的本质都不是模型推理能力不足,而是 Agent 的调度策略不够经济。模型本身很快,但被无效逻辑拖慢了。
3.3 为什么不能无脑加长上下文来解决
有的团队会说:“既然历史会累积,那我就把上下文窗口扩大,一次全都给模型。”这条路在成本上不划算,在性能上也可能适得其反。
上下文越长,prefill 计算量越大,单次响应时间越长,也就是 TTFT(Time To First Token)会显著增加。在很多场景里,用户第一次看到输出内容的时间反而变慢了,体验更差。
更理性的做法是:控制调用次数,精简每次调用的输入,而不是让模型一次性读完全部历史。
4. 用 Harness 评测 Agent 的调用链路
4.1 Harness 评测框架的思路
“Harness”在 AI 评测领域通常指一套用于控制、执行和分析 Agent 测试流程的框架。它的核心思路是把 Agent 放进一个受控环境中,自动执行任务,并记录执行过程中的关键指标。
以标题里提到的 DeepSeek Harness 为例,它在设计上强调让 Agent 在真实的工具调用环境中运行,关注的不只是“最终答对没有”,还包括任务执行效率。这类评测框架通常会把 Agent 的行列拆得很细,记录下面这些数据:
- 每个任务消耗的模型调用次数。
- 每轮对话的输入输出 token 数。
- 工具调用顺序与耗时。
- 成功完成任务的比例。
- 端到端延迟的分布情况。
换句话说,Harness 给你的是一把“尺子”,让你能清楚地量出 Agent 慢在哪里,而不是靠感觉推测。
4.2 评测配置的示意结构
不同评测框架的配置格式不同,这里用一个示意性的 YAML 配置讲解核心字段的含义。实际使用时要根据你选择的框架文档调整:
yaml复制# 文件路径:harness-config.yaml(示意配置)
agent:
name: "weather-travel-agent"
max_rounds: 15 # 最大轮次,超过后强制终止
early_stop: true # 满足停止条件时提前终止
tools:
weather_api:
timeout: 3000 # 工具调用超时时间(毫秒)
map_api:
timeout: 3000
metrics:
trace_calls: true # 是否记录每次模型调用
count_tokens: true # 是否统计 token 消耗
compute_delay: true # 是否计算端到端延迟
output:
report_format: "json"
这份配置想表达的关键思想是:评测 Agent 不能只看最终结果,必须记录过程指标。只有把每次调用的消耗都暴露出来,才能定位“问得太多”的具体环节。
4.3 应该关注哪些核心指标
在 Agent 性能优化中,推荐优先关注以下指标:
| 指标名称 | 含义 | 优化方向 |
|---|---|---|
| 平均调用轮次 | 单次任务平均调用模型次数 | 降低 |
| 每任务 token 消耗 | 完成单次任务的输入输出总量 | 降低 |
| 端到端延迟 | 用户从发起到拿到结果的时间 | 降低 |
| 工具调用成功率 | 工具调用是否一次成功 | 提高 |
| 上下文压缩比例 | 每轮输入相对上一轮的新增比例 | 降低 |
通过评测框架拿到这些指标之后,你就能针对性地做优化,而不是盲目地把模型从大换到小,或者单纯加长超时时间。
5. 实战:构建一个最小 Agent 并统计调用次数
这一节我们动手写一个最小的 Agent 模拟程序,不依赖任何复杂框架,核心目的是让你在本地亲眼看到“一次简单任务到底消耗了多少次模型调用”。
5.1 创建项目结构
先建一个简单的 Python 项目目录:
code复制minimal-agent/
├── agent.py
├── tracker.py
└── fake_llm.py
每个文件的职责:
fake_llm.py:模拟一个可以设置延迟的 LLM 客户端。tracker.py:用一个装饰器统计每个方法的调用次数和耗时。agent.py:核心 Agent 循环。
5.2 编写 Fake LLM
先创建一个假的 LLM 客户端,方便我们在本地模拟多轮调度的耗时:
python复制# 文件路径:minimal-agent/fake_llm.py
import time
class FakeLLM:
def __init__(self, delay_ms=1500):
self.delay_ms = delay_ms
def chat(self, messages):
# 模拟模型推理延迟
time.sleep(self.delay_ms / 1000)
# 模拟一个固定回复:指示调用查询工具
return "我需要查询一下天气数据,请调用 get_weather 工具。"
def get_tool_result(self, tool_name, arg):
# 模拟 Agent 总结工具结果
time.sleep(self.delay_ms / 1000)
return f"工具 {tool_name} 返回结果:{arg} 地区当前晴天,气温 25 度。"
这段代码里的延时就是模拟一次“模型被问一次”的开销。后面运行后你就会发现,即使所有逻辑都是假的,只要调用次数多,总耗时一样很可观。
5.3 编写调用统计装饰器
为了让 Agent 每次调用模型都留下记录,写一个简单的 tracker:
python复制# 文件路径:minimal-agent/tracker.py
import time
from functools import wraps
call_log = []
def track(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
cost_ms = (time.time() - start) * 1000
call_log.append({
"method": func.__name__,
"cost_ms": round(cost_ms, 2),
"args_count": len(args) + len(kwargs)
})
return result
return wrapper
这个装饰器不做任何复杂事情,就是把每个被标记方法的耗时和参数数量记录到全局列表里,方便最后输出统计。
5.4 编写核心 Agent 循环
现在写一个模拟的 ReAct 循环,故意让它执行多次“无意义”的工具调用,用来还原真实场景中 Agent 反复询问的情况:
python复制# 文件路径:minimal-agent/agent.py
from tracker import track, call_log
from fake_llm import FakeLLM
llm = FakeLLM(delay_ms=1500)
class WeatherTravelAgent:
def __init__(self):
self.max_rounds = 10
self.observed = []
@track
def plan(self, user_query):
# 模拟第一次规划
return f"我已经理解了用户需求:{user_query},需要查询天气"
@track
def call_tool(self, tool_name, city):
# 模拟工具调用
return f"get_weather('{city}')"
@track
def summarize(self, tool_result):
# 模拟总结
return f"天气查询结果是:{tool_result}"
def run(self, user_query):
messages = []
for _ in range(self.max_rounds):
plan = self.plan(user_query)
messages.append(plan)
tool_call = self.call_tool("get_weather", "北京")
messages.append(tool_call)
result = self.summarize("北京 晴天 25 度")
messages.append(result)
# 模拟提前发现已满足条件
if len(messages) > 3:
break
return messages[-1]
if __name__ == "__main__":
agent = WeatherTravelAgent()
final = agent.run("北京明天适合出行吗")
print("最终结果:", final)
print("\n=== 调用统计 ===")
print(f"总调用次数:{len(call_log)}")
total_time = sum(item["cost_ms"] for item in call_log)
print(f"模型累计耗时:{total_time / 1000:.2f} 秒")
for i, item in enumerate(call_log, 1):
print(f"第 {i} 次调用:{item['method']},耗时 {item['cost_ms']}ms")
运行方式:
bash复制cd minimal-agent
python agent.py
5.5 预期输出与分析
运行后你会看到类似下面的输出:
code复制最终结果: 天气查询结果是:北京 晴天 25 度
=== 调用统计 ===
总调用次数:30
模型累计耗时:45.00 秒
你可以看到,这个模拟 Agent 实际上在第 4 次循环就已经拿到了想要的信息,但它仍然继续执行剩余循环,直到触发 break 条件。这就是“问得太多”最直观的体现:Agent 不缺少推理能力,缺少的是“知道什么时候该停”的机制。
在实际项目中,这种问题更隐蔽,因为模型不会每次都给出同样的回复,存在随机性。Agent 框架为了保住成功率,往往会设置较高的最大轮次,结果就是延迟飙升。
6. 优化策略:减少无效询问
这一节进入重头戏,给出真正能落地到工程里的优化方案。核心原则只有一个:想尽办法减少模型被调用的次数。
6.1 收敛规划频率:不要每次都重新规划
很多 ReAct 实现的默认行为是:每执行一步工具调用,就让模型重新规划一次“下一步做什么”。这在复杂任务里是有价值的,但在大多数业务场景里,前几步的规划往往是重复的。
优化思路:
- 只有工具返回结果异常时,才触发重新规划。
- 工具返回结果符合预期时,直接按原定顺序执行下一步,跳过规划调用。
- 把“规划”从循环中抽离出来,只在任务开始时规划一次。
伪代码示意:
python复制plan = llm.plan(user_query)
steps = parse_steps(plan)
for step in steps:
result = execute_tool(step.tool_name, step.args)
if step.requires_plan_update(result):
plan = llm.update_plan(user_query, result)
这种方法的收益在于,把一次任务里的模型调用从“步骤数 × 2”降为“步骤数 + 1 或 2”,效果非常明显。
6.2 接入提前终止机制
前面模拟程序的教训就是没有提前终止。工程上至少要做两件事:
- 设置最大轮次上限,超过后强制返回当前最优结果。
- 设置“停止条件”:当模型已经生成最终答案并调用结束工具时,立刻终止循环。
伪代码:
python复制max_rounds = 10
for round_index in range(max_rounds):
response = llm.chat(messages)
messages.append(response)
if response.is_final_answer():
return response.content
if len(messages) > max_rounds:
return build_fallback_output(messages)
在真实框架里,可以通过判断模型输出中的 finish 标记或工具名来实现。关键是不要把所有希望寄托在模型的自觉上,要让 Agent 在代码层面有明确出口。
6.3 使用结构化输出约束单轮信息密度
一个减少轮次的思路是:让模型在一轮回复里输出更多有用信息,而不是挤牙膏。标准做法是使用结构化输出格式,比如 JSON,要求模型一次性返回多个决策。
例子:
json复制{
"tool_calls": [
{"tool": "get_weather", "args": {"city": "北京"}},
{"tool": "search_flight", "args": {"from": "上海", "to": "北京"}}
],
"thought": "用户需要综合天气和航班信息来决定出行"
}
一次性让模型给出多个工具调用参数,Agent 就可以并行或串行执行多个工具,然后再把结果一次性交回给模型总结,减少来回询问次数。对比一下:
- 原来的方式:问天气一次,等返回,问航班一次,等返回,再总结一次。3 次模型调用。
- 优化后:问一次,模型返回两个工具调用,执行完,再总结一次。2 次模型调用。
如果任务涉及 5 个工具,差距会更明显。
6.4 上下文压缩与历史裁剪
当 Agent 轮次比较多时,历史上下文会成为单次 prefill 慢的主要原因。推荐做两件事:
- 只保留跟当前目标相关的历史片段,删除已经完成的中间步骤。
- 用模型或规则把长工具结果压缩成短摘要,再追加到历史里。
典型的压缩策略:
- 工具原始返回 2000 token,压缩成一句话:
get_weather(北京): 晴天 25度。 - 上一轮规划结果已经执行完毕,不再需要完整保留。
- 超过 N 轮后,使用滑动窗口只保留最近 K 轮历史。
这里要提醒一下:不要过度压缩到模型丢失关键信息。更合理的做法是区分“必留信息”和“可丢弃信息”。
6.5 并行工具调用
如果 Agent 的目标需要多个相互独立的工具结果,尽量让工具并行执行,而不是串行等待。串行时每多一个工具就多一轮等待;并行时一轮模型调用就可以触发多个工具同时运行,最后再统一汇总。
实现时要注意:
- 工具之间不能有数据依赖。
- 对公共 API 加上频控,避免触发限流。
- 并行数量要有限制,避免资源竞争反而更慢。
6.6 预判式回答:能不问就不问
最后一个容易被忽略的优化点:Agent 不是每一轮都需要调用工具。很多时候模型已经具备足够知识可以回答用户的部分问题,或者工具要查的数据其实是静态的、可以缓存的。
工程实践建议:
- 对相同参数的查询做缓存,命中缓存时直接返回,不调用模型。
- 对高频问题配置快速回复模板。
- 在模型 prompt 里加上“如果你已经知道答案,不需要调用工具直接回答”的指令。
这些看似简单的规则,在真实场景里可以砍掉 30% 以上的无效模型调用。
7. 常见问题与排查思路
7.1 常见现象与解决方向
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| Agent 整体很慢,但单次模型响应正常 | 轮次太多,历史累积长 | 输出调用日志,统计每轮耗时与 token 数 |
| 同一工具被反复调用 | 结果未缓存或缺少终止逻辑 | 增加工具结果缓存与去重逻辑 |
| JSON 解析频繁失败导致重试 | 模型输出不稳定 | 使用结构化输出或大模型直出 JSON 模式 |
| 上下文越来越长,单次响应变慢 | 历史无裁剪 | 实现上下文压缩和滑动窗口策略 |
| 并行工具少,任务被串行拉长 | Agent 调度策略串行 | 识别独立工具,改为并行执行 |
| 任务已达成,Agent 还在继续 | 缺少提前终止条件 | 加入 max_rounds 与 final 输出判断 |
7.2 排查步骤
遇到 Agent 慢时,推荐按下面顺序排查:
- 先看调用次数:在 Agent 的每次模型调用处打印日志,记录方法名、耗时、输入 token 数。
- 再分析工具延迟:用性能分析工具看每个工具的执行时间,排除外部接口慢的影响。
- 算延迟占比:把模型耗时和工具耗时分开统计。
- 看历史长度变化:观察每轮请求体大小是否在快速增长。
- 看重试次数:统计 JSON 解析失败、工具调用异常引发的重试。
只要把日志补齐,这类问题通常半天内就能定位。
7.3 日志打点示例
推荐加一段类似下面的日志代码:
python复制import time
import logging
logging.basicConfig(level=logging.INFO)
def log_llm_call(messages, response, start_time):
input_tokens = estimate_tokens(messages)
output_tokens = estimate_tokens(response)
cost_ms = (time.time() - start_time) * 1000
logging.info(
"LLM call | input_tokens=%d output_tokens=%d cost_ms=%.1f",
input_tokens, output_tokens, cost_ms
)
通过日志积累数据后,你甚至可以给每次任务生成一张“调用链路耗时表”,精确知道哪一步消耗最大。
8. 最佳实践与工程建议
8.1 把“轮次预算”写进需求评审
在开发 Agent 功能前,建议像评估接口性能一样评估轮次预算。比如产品约定:一次典型任务最多允许 8 次模型调用,如果超出则视为异常。没有这个约束,Agent 的调度就会不断膨胀,直到延迟失控。
8.2 对 Agent 做好可观测性
Agent 的不可控性比普通接口高得多,必须把可观测性也纳入开发范围。建议记录:
- 每次模型调用的模型名称、输入输出 token、耗时。
- 每次工具调用的参数、返回状态、耗时。
- 每轮循环的决策日志,方便回溯“为什么这一步会调用模型”。
- 任务结束时的汇总指标:总轮次、总 token、总耗时。
有了这些数据,优化才是可持续的。
8.3 上线前做压测与回归评测
Agent 改造之后,别只看一两个 demo。建议用 Harness 评测框架或自建脚本,把一批典型任务全部跑一遍,对比优化前后的指标:平均轮次、p50 延迟、p95 延迟、成功率。
需要特别关注的是“成功率”这个指标有没有因为减少调用而下滑。优化目标是“在不影响正确率的前提下降低延迟”,如果为了追求快而牺牲太多成功率,反而得不偿失。
8.4 区分成本敏感与延迟敏感场景
不同业务对延迟和成本的敏感度不一样,优化手段的优先级也不同:
- 对延迟敏感场景:优先做提前终止、并行工具调用、上下文压缩。
- 对成本敏感场景:优先做缓存、结构化输出、减少重复调用。
- 对正确率敏感场景:优先做结果校验、人工确认流程兜底。
没有一套配置适合所有 Agent,合理的方式是给 Agent 建立分级策略:简单任务走轻量流程,复杂任务走完整推理。
8.5 关注模型选型与服务端压力
最后提醒一点:Agent 慢也可能是服务端被高并发打满了。如果多个用户同时触发 Agent 任务,每个任务几十次请求,后端服务的排队延迟会急剧上升。这时候要关注服务端的吞吐和排队指标,必要的时候做一层请求合并或限流保护。
很多 Agent 性能问题,最后都会收敛到“调用次数失控”这一个点上。单次模型的响应速度通常不是瓶颈,瓶颈在于循环里的反复询问。写这篇文章的目的,就是希望你在遇到“Agent 很慢”的时候,不是简单地换一个更大的模型或者增加超时时间,而是先从调用链路入手,看看 Agent 到底问了多少次、每次问了些什么、有没有没必要的询问。把这些数据抓出来优化一遍,你会发现自己项目的端到端延迟可能直接下降一半还多。
