之前排查一个 AI Agent 线上接口的时候,遇到一个典型的性能问题:单个请求端到端要 15 秒左右,产品侧的第一反应是“换更强的模型”。我们把单次模型调用耗时打出来之后发现,模型每次回答只需要 300ms 左右,快的甚至 150ms 就能返回。真正耗时的地方,是这个 Agent 在一个任务里先后调用了 30 多次模型。
也就是说,性能瓶颈大概率不在“模型推理”,而在“提问次数”。
这篇文章围绕这个现象写一套完整分析。先解释 Agent 慢和模型慢的区别,再用一个可复现的 Python 模拟工程统计调用次数,接着讲 Harness 层的优化思路:减少重复提问、并行调用、缓存、上下文压缩等。最后附常见问题排查清单和工程建议。适合正在做 AI Agent 应用的同学,也适合做性能优化与架构设计的后端开发者。
1. 背景与核心概念
1.1 模型慢 vs Agent 慢
很多开发者容易把两个概念混在一起。
模型慢,指的是大模型单次推理的耗时。比如一次 chat completion 请求,从客户端发出到拿到完整回复,耗时 500ms。这里的耗时受输入 token 数、输出 token 数、模型规模和部署端排队情况影响。
Agent 慢,指的是用户从发起一个任务,到最终拿到任务结果的整体耗时。它不是一个模型调用,而是一串模型调用、工具调用、状态流转、数据校验等步骤的总和。
所以会出现一个非常常见的现象:
- 单次模型调用很快,300ms;
- Agent 整体很慢,15s;
- 统计后发现,一个任务里发起 30 次模型调用,光模型调用就已经 9s。
模型本身没问题,问题是 Agent 的编排方式让模型被反复询问。
1.2 提问次数的放大效应
Agent 的任务通常是这样的:用户说一句话,Agent 需要拆解目标,决定调用哪些工具,拿到工具结果后还要继续判断“下一步做什么”。
在 ReAct 这类“思考-行动-观察”循环里,模型每执行一个工具,都会回到模型再做一次决策。也就是说:
code复制用户输入 -> 模型决策 -> 工具调用 -> 模型观察结果 -> 再决策 -> 工具调用 -> ...
每经历一次“模型 -> 工具 -> 模型”,就至少产生 1 次模型调用。如果其中还有验证失败、工具结果不完整、上下文过长导致模型输出不合法等情况,就会触发重试,调用次数更多。
放大效应可以这样理解:一个工具本来执行只需要 50ms,但为了决定“要不要调用它”,模型可能已经花了 600ms。工具开销被模型决策开销放大了一个数量级。
1.3 Harness 在 Agent 性能中的角色
“Harness”这个词在 Agent 工程里出现频率越来越高,比如标题中提到的 DeepSeek Harness。理解它的定位很重要。
Harness 可以简单理解成“把大模型包起来的那一层执行器”。它负责:
- 接收用户输入;
- 维护会话状态;
- 决定何时调用模型;
- 解析模型输出中的工具调用;
- 执行工具并把结果返回给模型;
- 处理缓存、重试、超时、日志。
因此,模型只负责单次推理,Harness 则负责整个任务怎么流转。如果一个 Agent 很慢,往往不是模型的问题,而是 Harness 层的决策策略有问题:决策点太多、循环太长、上下文越来越大、重试逻辑过于激进。
后面提到的 DeepSeek Harness,可以理解为围绕 DeepSeek 模型构建的 Agent 执行层。不管名称是什么,核心优化点是一致的:让模型在它该决策的地方决策,而不是在每个环节都让模型做一次低价值判断。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与实验思路
2.1 实验环境
本文实验使用 Python 3.10+,全部基于标准库完成,不需要额外安装第三方依赖。
为了复现“Agent 慢是因为问得多”,我会构造一个模拟环境:
SimLLM模拟大模型接口,每次调用固定延迟,并维护调用次数;run_tool模拟工具执行,每次执行有少量耗时;- 串行 Agent 模拟常见的“每执行一个工具就回模型一次”的循环;
- 优化 Agent 使用规则预检 + 两次模型调用,对比调用次数和耗时。
版本信息说明一下:在实际项目中,如果使用 DeepSeek 或其他 OpenAI 兼容协议的服务,模型名称、SDK 版本、接口字段可能会有差异。本文的重点是优化思路和排查方法,代码示例会在关键位置标注“需要按实际版本调整”。
2.2 项目结构
模拟项目结构如下:
text复制agent_slow_demo/
├── llm_sim.py # 模拟 LLM 客户端、工具执行函数
├── slow_agent.py # 串行 ReAct 风格 Agent(问题次数多)
├── fast_agent.py # 规则预检 + 两次调用的 Agent(优化版)
└── real_skeleton.py # 接入真实大模型接口的代码骨架
后面会逐个文件说明。
2.3 为什么先用模拟环境
真实大模型接口有费用,有网络波动,还有排队波动。如果直接在线上排查“调用次数”问题,容易出现数据抖动,很难判断到底是模型慢,还是问得多。
模拟环境的好处是:
- 单次模型延迟稳定,便于对比;
- 调用次数清晰可见;
- 不依赖 key、网络和服务状态;
- 任何读者都可以直接跑通。
先把问题在模拟环境里复现出来,再回到真实接口排查,思路会更清楚。
3. 核心原理:一次 Agent 任务的延迟构成
3.1 单次模型调用的耗时构成
单次模型调用的端到端耗时可以拆成几个部分:
text复制T_call = T_network + T_queue + T_prefill + T_decode
T_network:网络传输,包括上行请求和下行响应;T_queue:模型服务排队时间,并发高时会明显增加;T_prefill:处理输入 token,也就是“读取并理解上下文”;T_decode:自回归生成输出 token,是 LLM 推理的主要耗时。
如果输入上下文越来越长,T_prefill 会变大,T_decode 也可能受长上下文注意力机制影响。所以 Agent 任务越到后面,单次调用往往越慢。
3.2“每步都问”的本质:Agent 版的 N+1 问题
数据库开发里有一个经典问题叫 N+1 查询:先查一次列表,再循环对每条记录发起一次查询,导致查询次数爆炸。
Agent 也有类似问题:为了完成一个目标,每执行一个工具,都要回到模型“汇报结果,等待下一步指令”。
假设一个任务有 7 个步骤需要工具支撑,串行 ReAct 的调用次数如下:
text复制第 1 次:模型决定执行第 1 个工具
第 2 次:模型观察第 1 个工具结果,决定执行第 2 个工具
第 3 次:模型观察第 2 个工具结果,决定执行第 3 个工具
...
第 7 次:模型决定执行第 7 个工具
第 8 次:模型观察第 7 个工具结果,生成最终回复
一共 8 次。如果单次模型调用 600ms,那么仅模型调用就耗时 4.8s。而 7 个工具如果直接执行,可能总共只需要 500ms。
这就是“问得多”导致的慢。慢在模型调度次数,而不在单次模型能力。
3.3 失败重试会让调用次数继续膨胀
真实环境不是理想环境。模型可能返回格式错误,解析不出 action 字段;工具可能超时;模型可能连续两次给出同一个工具的调用;上下文太长可能导致模型输出中断。
这些情况都会触发重试。一次重试就多一次模型调用。如果重试逻辑不加以限制,可能出现“模型在循环里反复调同一工具”的无效循环。
所以生产环境里的 Agent 调用次数,往往会比理想值高很多。比如理论上 8 次的流程,实际可能跑到 15~20 次。
3.4 延迟估算公式
Agent 串行执行时,端到端时长可以简化为:
text复制T_total = Σ T_model_call + Σ T_tool + T_overhead
如果所有模型调用几乎串行:
text复制T_total ≈ N × T_model_call_avg + M × T_tool_avg + overhead
这里 N 是模型调用次数,M 是工具调用次数。
假设:
text复制T_model_call_avg = 0.6s
T_tool_avg = 0.1s
N = 20
M = 15
那么:
text复制T_total = 20 × 0.6 + 15 × 0.1 = 12 + 1.5 = 13.5s
如果只优化模型速度,让单次调用从 0.6s 降到 0.3s,总耗时变成 20 × 0.3 + 1.5 = 7.5s,省了 6s。
但如果优化 Harness 策略,把模型调用次数从 20 降到 6,即使单次模型调用还是 0.6s,总耗时变成 6 × 0.6 + 1.5 = 5.1s,省了 8.4s。
在很多场景下,少问几次比换更快模型更有效。
4. 实战:统计一个 Agent 的提问次数
4.1 编写模拟 LLM 客户端
文件路径:agent_slow_demo/llm_sim.py
python复制import time
# 模拟计划:一个完整结算流程需要串行执行的工具
PLAN = [
"query_user",
"query_balance",
"query_order",
"verify_discount",
"calc_refund",
"apply_refund",
"send_notify",
]
class SimLLM:
"""模拟大模型接口,记录调用次数并模拟延迟。"""
def __init__(self, latency=0.6):
self.latency = latency
self.calls = 0
def chat(self, message):
self.calls += 1
time.sleep(self.latency)
step = self.calls - 1
if step < len(PLAN):
return (
f"thought: 第 {step + 1} 步需要调用 {PLAN[step]}\n"
f"action: {PLAN[step]}"
)
return "final: 任务执行完毕,已生成用户回复。"
def run_tool(tool_name):
"""模拟工具执行,用于观察工具调用过程。"""
time.sleep(0.05)
return f"{tool_name} 执行成功"
def extract_action(response):
"""从模型回复中解析 action 字段。"""
for line in response.splitlines():
if line.startswith("action:"):
return line.split(":", 1)[1].strip()
return None
这段代码的核心是 calls 计数器。真实项目里,可以在请求大模型接口的封装层加一个同样的计数器,用于打印 calls 数。
4.2 编写串行 ReAct 风格 Agent
文件路径:agent_slow_demo/slow_agent.py
python复制from llm_sim import SimLLM, extract_action, run_tool
def run_slow_agent(user_input):
llm = SimLLM(latency=0.6)
print(f"用户输入: {user_input}")
response = llm.chat(user_input)
print(f"LLM 调用 {llm.calls}: {response}")
while True:
action = extract_action(response)
if action is None:
break
tool_result = run_tool(action)
print(f"工具调用: {action} -> {tool_result}")
# 每执行一个工具,就让模型再做一次决策
response = llm.chat(f"工具结果: {tool_result}")
print(f"LLM 调用 {llm.calls}: {response}")
print(f"总模型调用次数: {llm.calls}")
if __name__ == "__main__":
run_slow_agent("帮我把这笔订单申请退款")
这是一个典型的“下一步决策依赖模型”的串行流程。每执行一个工具,都会发起新一次模型调用。
4.3 运行与结果说明
运行命令:
bash复制cd agent_slow_demo
python slow_agent.py
预期输出:
text复制用户输入: 帮我把这笔订单申请退款
LLM 调用 1: thought: 第 1 步需要调用 query_user
action: query_user
工具调用: query_user -> query_user 执行成功
LLM 调用 2: thought: 第 2 步需要调用 query_balance
action: query_balance
工具调用: query_balance -> query_balance 执行成功
...
LLM 调用 8: final: 任务执行完毕,已生成用户回复。
总模型调用次数: 8
可以看到,模拟计划里有 7 个工具,最终产生 8 次模型调用。
如果每步还出现解析失败重试,调用次数会更多。可以把 latency 调大,比如 SimLLM(latency=1.0),再用 time 模块计算总耗时,会非常直观。
4.4 接入真实大模型的代码骨架
模拟环境验证思路之后,真实项目里通常是这样的结构。文件路径:agent_slow_demo/real_skeleton.py
python复制# 示例思路,需按实际 DeepSeek / OpenAI 兼容 SDK 版本调整
from openai import OpenAI
client = OpenAI(
base_url="你的模型服务地址",
api_key="你的密钥",
)
# 统计调用次数的封装
class CallStats:
def __init__(self):
self.calls = 0
stats = CallStats()
def ask_model(messages, tools=None):
stats.calls += 1
response = client.chat.completions.create(
model="deepseek-chat", # 以官方文档提供的模型名为准
messages=messages,
tools=tools,
)
return response.choices[0].message
需要注意几个点:
model名称要以实际服务文档为准;- 如果使用流式输出,统计逻辑要放在请求发起处;
- 不仅要统计次数,还要统计每次耗时和 token 数;
tools参数在部分 OpenAI 兼容协议里可能叫functions,需要根据版本调整。
在真实 Agent 代码里,只要在 ask_model 外面加一层计数和计时,很快就能发现问题。
5. 优化案例:从 8 次提问压到 2 次
5.1 优化思路:区分“需要模型决策”的步骤和“固定流程”的步骤
上一节的串行 Agent 之所以慢,是因为它在每个工具执行后都问模型“下一步做什么”。
但业务里很多流程是固定的。比如“查询用户 -> 查询余额 -> 查询订单 -> 算退款 -> 执行退款 -> 发通知”,这类步骤可以用规则引擎直接执行,根本不需要模型参与决策。
模型只需要做两件事:
- 在入口处理解用户意图,抽取关键参数;
- 在所有工具执行完后,生成用户可读的最终回复。
这就是把“每个工具都问一次”压成“只问两次”的核心思路。
5.2 编写优化后的 Agent
文件路径:agent_slow_demo/fast_agent.py
python复制from llm_sim import SimLLM, PLAN, run_tool
def run_fast_agent(user_input):
llm = SimLLM(latency=0.6)
print(f"用户输入: {user_input}")
# 第 1 次调用:只识别意图和关键参数,不规划工具顺序
intent_response = llm.chat(user_input + " 请只返回intent字段")
print(f"LLM 调用 {llm.calls}: {intent_response}")
# 规则引擎直接执行固定流程,不需要模型参与
for step in PLAN:
result = run_tool(step)
print(f"规则执行: {step} -> {result}")
# 第 2 次调用:基于所有工具结果生成最终回答
final_response = llm.chat("所有工具执行成功,请生成用户友好回复")
print(f"LLM 调用 {llm.calls}: {final_response}")
print(f"总模型调用次数: {llm.calls}")
if __name__ == "__main__":
run_fast_agent("帮我把这笔订单申请退款")
运行命令:
bash复制python fast_agent.py
这个版本的模型调用次数固定为 2,规则执行时完全不接触模型。
5.3 结果对比
| 指标 | 串行 ReAct Agent | 规则预检 + 两次调用 |
|---|---|---|
| 模型调用次数 | 8 | 2 |
| 模型调用理论耗时 | 8 × 0.6 = 4.8s | 2 × 0.6 = 1.2s |
| 工具执行次数 | 7 | 7 |
| 工具执行理论耗时 | 7 × 0.05 = 0.35s | 7 × 0.05 = 0.35s |
| 是否依赖模型决策 | 每一步都依赖 | 仅意图识别和最终生成依赖 |
在相同模型延迟、相同工具耗时的前提下,优化后省掉了 6 次模型调用,端到端耗时明显降低。
这里不是否定 ReAct。ReAct 适合任务边界不清、需要动态规划的场景。但如果业务步骤已经明确,就应该用规则或工作流来固定路径,把模型调用次数降到最低。这也是 DeepSeek Harness 这类执行层优化的核心方向:让大模型做真正需要推理的事,把机械步骤交给确定性代码。
5.4 并行提问:当多个独立判断必须由模型完成
还有一种情况,任务里有多个互相独立的判断,但每个判断又确实需要模型参与。
比如:
- 判断用户情绪是否需要安抚;
- 判断订单是否满足退款条件;
- 判断是否需要人工介入。
这三个判断不依赖彼此,就可以并行发起模型调用,而不是串行等待。
如果一次串行耗时 0.6s × 3 = 1.8s,并行后理论耗时会接近 0.6s。用 Python 的并发实现时,可以考虑 asyncio.gather 或者线程池。真实项目中要注意并发请求对模型服务的压力,以及限流策略。
下面是一个思路示例:
python复制import asyncio
async def ask_parallel(prompts):
results = await asyncio.gather(
*(ask_single(p) for p in prompts)
)
return results
这样可以把多次独立提问的时间重叠起来。
6. Harness 层面的工程优化
6.1 把调用范围最小化
Harness 层最容易犯的错误,是把所有输入都交给模型处理。比如:
- 用户输入先让模型做意图识别;
- 意图识别完让模型做参数抽取;
- 参数抽取完让模型决定用哪个工具;
- 工具结果出来让模型写总结;
- 写完总结再让模型检查一遍回答是否合规。
每一步看起来都有道理,但每一步都是一次模型调用。理想情况下,Harness 应该能裁剪掉低价值调用:
- 能通过正则、枚举、规则识别的内容,不要让模型做;
- 能通过结构化数据直接映射参数的,不要走自然语言抽取;
- 能直接在代码层面校验的,不要交给模型“检查”。
模型是昂贵的判断器,要把它用在刀刃上。
6.2 上下文压缩与裁剪
Agent 慢的另一个隐藏加速器,是上下文无限膨胀。每轮工具结果都追加到 messages 里,第二轮调用携带第一轮的输出,第三轮携带前两轮的输出。
问题在于:模型处理输入 token 也需要时间。上下文越长,T_prefill 越慢,而且你还在为重复的 token 付费。
常见处理方式:
- 限制单条工具结果长度,超长只保留摘要;
- 工具结果返回后不全部塞进 messages,只保留关键字段;
- 使用滑动窗口,只保留最近几轮;
- 使用上下文压缩模型,把历史对话压缩成摘要后再传给主模型。
6.3 缓存与重试策略
如果同一个问题反复出现,Harness 层可以做缓存。
缓存粒度可以是:
- 完整用户问题的最终结果缓存;
- 子步骤的工具调用结果缓存;
- 模型对同一意图的决策缓存。
比如“查询订单状态”这类工具结果,短期内不会变化,就可以设置 TTL 缓存,避免同一个 Agent 在不同环节重复查询同一订单。
重试策略也需要精细化。模型返回格式解析失败时,重试 1 次还是 3 次?重试时要不要修正 prompt?要不要更换模型参数?这些都是 Harness 层可以控制的地方。
6.4 可观测性:把每次提问记录下来
没有数据,就无法优化。真正生产环境里的 Agent Harness,至少要有这样的日志字段:
json复制{
"trace_id": "xxx",
"agent_task": "refund_order",
"model_calls": 8,
"model_total_ms": 4800,
"tool_calls": 7,
"tool_total_ms": 350,
"context_tokens": [1200, 2400, 5100, 7800, 12000],
"retry_count": 2,
"final_ms": 5300
}
有了这个数据,才能回答“慢在哪里”:
- 如果
model_calls很大,优先优化决策次数; - 如果
model_total_ms大但model_calls不多,说明单次调用慢,再看输入 token 数是否膨胀; - 如果
retry_count大,重点看解析和工具稳定性的问题。
7. 常见问题与排查思路
7.1 高频问题表
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| Agent 整体很慢,单次模型调用正常 | 模型调用次数过多 | 打印 LLM 调用次数,检查是否每执行一个工具都回模型一次 |
| 工具执行很快,但总耗时很高 | 工具结果带回模型后又触发新一轮决策 | 把不依赖模型的结果用固定流程执行,模型只负责必要判断 |
| 任务越往后越慢 | 上下文 token 膨胀 | 裁剪工具结果、滑动窗口、上下文压缩 |
| 模型频繁重试 | 输出格式不稳定、工具异常 | 加格式约束,限制最大重试次数,重试前校验工具入参 |
| 并发一高就慢 | 模型服务排队或限流 | 增加缓存、并行化、限流策略,把不重要请求降级 |
| 部分任务进入死循环 | 模型反复调用同一个工具 | 配置最大迭代次数或最大模型调用数,检测重复 action |
7.2 排查 Checklist
遇到 Agent 慢,按下面顺序走一遍:
- 先看总耗时,确认慢在模型调用还是工具调用;
- 给模型调用加计数器,打印调用次数;
- 打印每次模型调用的 token 数和耗时;
- 打印每个工具的执行耗时;
- 检查 messages 上下文长度变化曲线;
- 检查是否存在无意义的重复 action;
- 在同样的任务上做最小复现,排除偶发网络波动;
- 对比串行流程和固定流程的调用次数。
基本只要到第 2 步,就能定位是不是“问得太多”。
8. 最佳实践与工程建议
8.1 给 Agent 设置调用预算
生产环境一定要给 Agent 任务配置上限。最简单的是两个参数:
python复制MAX_MODEL_CALLS = 10
MAX_TOOL_CALLS = 20
超过预算就结束任务,进入人工兜底或者返回错误提示。这能避免模型在循环里空转,也是成本控制的基础手段。
8.2 模型当决策者,代码当执行者
很多 Agent 慢,是因为把模型当成了“所有逻辑的执行者”。
更好的分工是:
- 确定性逻辑用代码;
- 分支判断用小模型或规则;
- 复杂推理用大模型;
- 固定流程用工作流引擎;
- 模型只处理“需要自然语言理解”和“需要推理”的部分。
这样既能控制延迟,也能降低 token 成本。
8.3 工具结果要“瘦身”
工具返回的结果常常是完整对象,比如订单详情、用户信息、库存列表。这些对象可能非常大。
在把工具结果交给模型前,应该做一层裁剪:
python复制def summarize_tool_result(tool_result, max_len=500):
text = str(tool_result)
if len(text) > max_len:
return text[:max_len] + "...(已截断)"
return text
这样上下文不会快速膨胀,模型也不需要处理大量无关字段。
8.4 异步与并行
如果 Agent 有多个独立步骤,优先考虑异步并行。常见的做法:
- 多个独立工具并行执行;
- 多个独立模型判断并行发起;
- 长耗时工具放到后台任务,前端先返回中间状态;
- 使用流式输出,让用户先看到部分内容。
并行不是银弹,如果步骤之间有依赖关系,强行并行反而增加复杂度。可以先画出任务依赖图,再决定哪些步骤能并行。
8.5 关注成本
“问得多”不只是慢,还意味着成本高。每次模型调用都有 token 费用。
优化前,可以先用日志统计出:
text复制平均每个任务模型调用次数
平均输入 token 数
平均输出 token 数
优化后对比这三个指标。你会发现,减少提问次数往往同时降低了延迟和成本。
9. 总结:先算账,再换模型
回到最开始的问题:Agent 慢,是不是模型不够强?
我的建议是,先不要急着换模型,先算一笔账:
- 这个任务有多少次模型调用?
- 每次调用传了多少 token?
- 有多少调用是在决定“下一步做什么”?
- 有多少调用是在做重复劳动?
如果模型调用次数很高,那优化的方向就是减少提问次数,而不是提升单次模型速度。如果模型调用次数不多但单次很慢,再考虑换更大参数模型、优化部署、提升并发或者升级硬件。
这次排查给我的最大启发是:AI Agent 的性能问题,很多时候不是一个“模型问题”,而是一个“工程问题”。把 Harness 层的决策策略做好,少问几次,比盲目升级模型更有效,也更省钱。
如果你也在做 Agent 类应用,建议下一步就把模型的调用次数埋点打出来,先看看自己的 Agent 到底问了多少次。
