Agent 慢,不是模型慢,是问得太多。这句话第一次看到时,可能觉得反直觉。毕竟大家习惯把性能问题归到模型头上:单次请求延迟高、显存不够、推理引擎优化不到位。但真正把 Agent 拖慢的,往往是一个很容易被忽略的放大系数:调用次数。
先用一个倍数关系说明问题。如果一次模型问答 1 秒,Agent 执行一个任务要连续“问”30 次,光等待模型就是 30 秒。更麻烦的是,每一轮都会把越来越长的历史记录带回模型,模型需要重新读一遍再回答。次数越多、上下文越胖,单次就越慢,端到端耗时呈乘法增长。所以优化 Agent,与其盲目换更高性能的模型服务,不如先把“问得太多次”这件事控制住。
本文围绕这个性能优化观点展开。我会先拆解 Agent 慢的延迟来源,再讲 Harness 约束层的设计思路,然后给出一套可落地的观测、验证、批量改造方法。适合自研 Agent 循环、使用 LangChain/LangGraph 等编排框架、以及基于 DeepSeek 开源模型做本地 Agent 的读者直接收藏。
1. 核心观点:Agent慢不是模型慢,而是“问得太多”
1.1 端到端延迟放大效应
Agent 的端到端延迟不能用单次模型推理时间来衡量。它更接近下面这个公式:
text复制T_total = N × T_llm + T_tool + T_schedule
其中:
- N 是完整任务中调用 LLM 的总次数;
- T_llm 是每次调用 LLM 的平均延迟;
- T_tool 是工具执行耗时;
- T_schedule 是编排、调度、网络传输等额外耗时。
很多项目只盯着 T_llm,却忽略了 N 这个放大系数。举个例子,假设单次调用平均 1.2 秒,工具执行每次 0.2 秒,Agent 跑了 30 轮,光模型等待就是 36 秒。再把上下文从 2K tokens 涨到 20K tokens,单次调用延迟很可能从 1 秒涨到 2 秒以上。50 轮调用时,总耗时很容易超过 100 秒。
这里的数字只是为了解释倍数关系,实际数据一定要以你自己环境的日志为准。但结论很明确:N 和上下文长度,是比单次推理速度更容易忽略的瓶颈。
1.2 “问得太多”的四种典型表现
第一种是循环无终止。Agent 在 Plan-Act-Observe 循环里反复执行,每个动作之后都要问一次大模型“下一步怎么办”,直到撞上 max_iterations 才停下来。很多任务其实在第 3 轮就已经可以收敛,却因为缺少终止条件,硬跑了 20 轮。
第二种是每步都要“看一眼”。工具执行完毕后,Agent 习惯把完整结果全部塞回模型,再让模型总结“这个结果对不对”。实际上大部分工具结果可以直接用规则判断:文件存在、状态码 200、返回值为空,这些都不需要大模型再次介入。
第三种是上下文重复传输。每一轮都把完整对话历史、工具输出、中间推理过程原封不动带进请求,没有压缩也没有缓存。预填充阶段的时间会随上下文长度线性增长,最终表现为“越跑越慢”。
第四种是多 Agent 之间互相 ping-pong。规划 Agent、执行 Agent、验证 Agent 各自独立调用模型,又互相传递结果,形成嵌套式的“问得太多”。这种情况下,肉眼看到的模型调用次数已经失真,最严重的时候,一次业务请求背后可能触发了上百次 LLM 调用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Agent性能参数速览:优化前先看哪些数据
在做任何优化之前,先建立一套可观测的指标。下表是 Agent 性能优化时最值得关注的数据项:
| 参数 | 说明 | 观察方式 |
|---|---|---|
| 单次 LLM 调用延迟 | 从请求发出到完整返回的时间 | API 返回耗时、SDK 日志 |
| Agent 端到端延迟 | 整个任务从开始到结束的时间 | 任务链路埋点 |
| LLM 调用次数 | 单个任务内实际调用模型的次数 | 请求计数器、日志去重 |
| 上下文长度变化 | 每轮请求携带的 tokens 数量 | usage.prompt_tokens 字段 |
| 工具执行耗时 | 工具、插件、数据库查询的耗时 | 工具函数计时 |
| 输出 tokens 数量 | 模型每轮生成的文本长度 | usage.completion_tokens 字段 |
| 无效决策占比 | 没有改变状态的模型调用比例 | 手动抽样分析 |
这里有一个简单的判断标准:如果 Agent 端到端延迟很高,但单次 LLM 调度延迟正常,那么问题大概率出在调用次数和上下文管理上,而不是模型本身。
3. Harness层:把“自由问答”变成“受控执行”
顺着一开始的分析,标题里提到的 DeepSeek Harness 就好理解了。它并不是某一个固定的开源仓库名,而是一类围绕 DeepSeek 等开源模型构建 Agent 执行框架时的约束层方案。Harness 的核心作用不是让模型变聪明,而是约束它“什么时候该回答、回答成什么样、什么时候不用回答”。
3.1 为什么需要一层“约束框架”
直接让大模型自由发挥,适合聊天,不适合任务执行。Agent 需要的是稳定决策,而不是每次回复都重新脑补一套流程。交互约束落实到代码上,通常体现为系统提示词、工具定义、上下文管理、循环终止条件这几块的组合,合起来就是 Harness 层。
没有这层约束时,Agent 会不停向模型发起对话式询问,因为这符合模型默认的行为模式。有了 Harness 层,模型只在关键节点被调用,其余步骤用规则和程序逻辑完成。
3.2 Harness 的四个控制点
控制 LLM 调用入口。不是所有场景都需要模型介入,参数校验、状态判断、结果校验优先交给规则。只有真正的规划和异常处理才走模型。
控制输出格式。要求模型返回结构化 JSON,而不是长段自然语言,这样程序可以直接解析并执行下一步,减少“读模型回复再判断”的额外调用。
控制上下文大小。只保留当前子任务最需要的片段,工具输出做截断和摘要,历史记录做滚动压缩,而不是每轮全量携带。
控制终止条件。同时设置成功完成、失败退出、最大轮次、超时时间四类终止条件,避免隐形死循环。
下面是一个结构化输出约束示例:
json复制{
"type": "object",
"properties": {
"reasoning": {
"type": "string",
"description": "简短决策依据,不超过50字"
},
"action": {
"type": "string",
"enum": ["search", "calculate", "api_call", "terminate"]
},
"params": {
"type": "object"
}
},
"required": ["action", "params"]
}
模型输出被约束成上述结构后,程序可以直接读取 action 和 params 执行,不需要额外让模型解释“你刚才是什么意思”。
4. 典型慢链路拆解:先用日志证明“问得太多”
优化不是靠感觉,而是靠数据。先把 Agent 的每一次 LLM 调用记录下来,才能回答“到底是不是问得太多”这个问题。
4.1 一个“问太多次”的 Agent 主循环
先看一段典型的慢链路伪代码:
python复制def agent_loop(task, max_iterations=30):
state = {"task": task, "history": []}
for i in range(max_iterations):
# 每步都把完整历史塞回模型,问一次“下一步做什么”
prompt = build_prompt(task, state["history"])
response = call_llm(prompt)
action = parse_action(response)
state["history"].append(response)
if action == "terminate":
break
tool_result = execute_tool(action)
# 工具结果又原封不动塞进下一轮 prompt
state["history"].append({"tool_result": tool_result})
return state
这段代码的问题很明显:每一步的决策都依赖下一轮模型调用,工具执行结果没有规则校验,历史列表无限膨胀,整体轮次接近 max_iterations。任务简单时可能 5 轮能跑完,实际却走了 20 轮。
4.2 给 LLM 调用加日志和计时
在改造之前,先给 LLM 调用函数包一层计时和日志:
python复制import time
import json
def tracked_llm_call(name, llm_fn, *args, **kwargs):
start = time.perf_counter()
result = llm_fn(*args, **kwargs)
elapsed_ms = (time.perf_counter() - start) * 1000
usage = result.get("usage", {})
log_entry = {
"call_name": name,
"elapsed_ms": round(elapsed_ms, 1),
"input_tokens": usage.get("prompt_tokens", 0),
"output_tokens": usage.get("completion_tokens", 0),
}
# 把日志追加到本地文件,方便后面统计
with open("agent_llm_log.jsonl", "a", encoding="utf-8") as f:
f.write(json.dumps(log_entry, ensure_ascii=False) + "\n")
return result
落地时的建议是:每次调用记录四个字段就够,调用名、耗时、输入 tokens、输出 tokens。输入 tokens 用来观察上下文膨胀,耗时用来观察单次是否变慢,调用名用来判断这次调用是否真的必要。
4.3 如何从日志判断瓶颈位置
日志跑出来后,按以下思路定位:
如果单次调用耗时一直稳定,但整体调用次数远超业务步骤数,说明问题是无用决策太多。比如一个“查天气”任务跑了 12 次 LLM 调用,其中有 9 次都没有改变状态。
如果 input_tokens 持续上涨,且耗时同步上涨,说明上下文膨胀是主因。需要做压缩和缓存。
如果工具执行时间占比很小、等待模型的时间占比很高,同样说明问得太多。这种情况换更大算力只会小幅改善,减少轮次才能解决问题。
5. 减少“问得太多”的工程策略
看完日志,下一步就是改造。下面这组工程策略按落地成本从低到高排列,建议逐条做。
5.1 计划先行:决策与执行分离
把 ReAct 式的“执行一步、问一步”改成“先规划一批,再执行”。
第一步用一次 LLM 调用生成整条执行计划,包含步骤列表和每步的输入输出;第二步由程序依次执行各步骤,只有遇到异常时才调大模型处理。这样,一个 10 步任务最多只调 1 到 2 次模型,而不是 10 次。
规划响应同样使用结构化格式:
json复制{
"plan": [
{"step": 1, "action": "search", "params": {"query": "Agent性能优化"}},
{"step": 2, "action": "summarize", "params": {"source": "step_1_result"}}
]
}
5.2 结构化输出:一次拿到可执行结果
让模型每次输出固定 schema,程序直接解析字段执行。避免“模型输出了一句话,程序还要再调一次模型去理解这句话”的情况。停留在自然语言输出阶段的 Agent,是最容易不知不觉多问一轮的。
5.3 上下文瘦身:压缩、缓存与截断
长上下文是 Agent 慢的放大器。可以采用三种手段:
第一,前缀缓存。DeepSeek 等模型 API 和部分本地推理框架支持 prompt 缓存,相同前缀在短时间内不会重复计算。尽量把系统提示词、工具定义、固定背景信息放在前缀位置,让多次调用的公共前缀保持一致。
第二,历史摘要。超过设定长度的旧对话不原样保留,而是压缩成一段摘要。摘要仍然需要一次模型调用,但只在上下文真正超过阈值时触发,节省的是后续所有轮次的 prefill 时间。
第三,工具结果截断。工具返回的大段文本只保留与任务相关的关键片段,或先用规则提取关键字段,再拼进下一轮 prompt。
5.4 工具调用合并与规则分流
工具结果的成败判断优先用规则,不交给模型。API 返回 200、文件存在、查询结果为空,这些都可以用条件判断处理。
多个独立请求可以在一次工具调用里完成。比如 Agent 需要同时查三个接口,就把三个请求合并成一个批量接口执行,再统一返回结果。减少单独调用的次数,也就减少了不必要的上下文写入。
规则无法判断的异常情况,才升级到大模型介入。升级时把异常信息作为输入,让模型输出新的处理方案。
6. 批量任务场景:并发与队列优化
单个 Agent 任务优化完后,批量任务还要独立设计。批量任务最容易犯的错误是串行执行:N 个任务依次跑,总耗时是单任务耗时的线性叠加。这个问题在“问得太多”的基础上,又叠了一层调度浪费。
6.1 批量任务为什么要单独设计
如果单个任务已经优化到 5 次 LLM 调用、耗 8 秒,批量 100 个任务串行跑就是 800 秒。如果改成 4 个并发,理论上可以压到 200 秒左右。再配合任务内部的轮次控制,效果会更明显。
6.2 并发调用示例
用 Python 的 ThreadPoolExecutor 做一个通用批量执行模板:
python复制from concurrent.futures import ThreadPoolExecutor, as_completed
import logging
def execute_one_task(task_item):
"""
单任务执行入口。
内部也要遵守减少 LLM 调用次数的原则,别把并发当银弹。
"""
return run_agent(task_item)
def run_batch(tasks, max_workers=4):
results = []
with ThreadPoolExecutor(max_workers=max_workers) as pool:
future_map = {
pool.submit(execute_one_task, item): item for item in tasks
}
for future in as_completed(future_map):
task_item = future_map[future]
try:
result = future.result()
results.append({"task": task_item, "status": "success", "data": result})
except Exception as exc:
logging.error("task %s failed: %s", task_item, exc)
results.append({"task": task_item, "status": "failed", "error": str(exc)})
return results
并发数不要盲目调大。如果模型 API 有速率限制,先确认 RPM/TPM 上限,再设置合理的 max_workers。本地推理服务同样受显存和 batch 大小限制,并发过高会导致单次请求排队变慢。
6.3 失败重试策略
批量任务必须有重试机制,但重试也不要走模型。先用规则判断错误类型:限流错误做指数退避重试,参数错误直接跳过,模型接口异常做有限次重试。
python复制import time
def call_with_retry(fn, retries=3, base_delay=1.0):
for attempt in range(retries):
try:
return fn()
except RateLimitError:
time.sleep(base_delay * (2 ** attempt))
except ParamError:
raise
raise RuntimeError("retry exhausted")
6.4 批量后的效果验证
批量改造后,对比三组数据:单个任务平均耗时、100 个任务的总体耗时、任务失败率。如果单任务耗时没有大变化但总体耗时下降,说明并发生效;如果两者都没有变化,优先检查是不是任务内部仍然在大量调用 LLM。
7. 常见问题与排查方法
把 Agent 慢的问题整理成排查表,方便在项目里直接对号入座。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| Agent 响应很慢,单次模型调用正常 | LLM 调用次数过多 | 统计任务内 LLM 调用次数 | 增加 Harness 约束层,减少无用决策轮 |
| 越到后面越慢 | 上下文膨胀 | 观察每轮 input_tokens | 做历史摘要、工具结果截断、前缀缓存 |
| 任务一直在转圈不结束 | 缺少终止条件 | 检查循环退出逻辑 | 设置 max_iterations、timeout、成功终止判断 |
| 批量任务排队时间长 | 串行执行 | 观察任务提交间隔 | 改用并发执行并设置并发上限 |
| token 消耗异常高 | 重复上下文+过长输出 | 对比多轮 input_tokens | 压缩上下文,限制 max_tokens |
| 工具执行后总是继续问模型 | 规则分流缺失 | 查看工具结果判断逻辑 | 成功率/失败率判断交给规则 |
| API 调用超时 | 单次上下文过长或服务排队 | 查看耗时日志 | 削减上下文、增加超时时间、重试 |
| 多 Agent 互相触发 | 循环对话无深度限制 | 查看调用链日志 | 设置最大嵌套深度、引入仲裁规则 |
出现性能问题时,先打开 LLM 调用日志,不要急着改模型。绝大多数 Agent 慢的问题,打开日志后才发现模型很无辜。
8. 最佳实践与使用建议
第一,先埋点再优化。任何 Agent 项目上线前,至少要保留调用次数、耗时、tokens 三个指标。没有日志的 Agent 项目,遇到性能问题只能靠猜。
第二,默认给 Agent 设置硬性限制。最大迭代轮次、单任务超时时间、最大上下文长度,都属于必备配置。宁可提前终止,也不要无限循环。
第三,计划与执行分离。规划用大模型,执行用规则。凡是规则能判断的分支,都不要把一个简单判断交给模型。
第四,工具返回内容必须先结构化再进入上下文。原始返回可能很长,抽取关键字段是最有效率的瘦身方式。
第五,批量任务加并发上限和失败重试。并发解决整体吞吐,重试解决偶发失败,两者一起做才能稳定提速。
第六,注意合规与授权。Agent 在读取用户数据、调用业务接口、处理版权素材时,必须确认调用方有合法授权。基于 DeepSeek 等模型做本地部署时,同样要对输入数据范围做限制,不能把未授权的数据随意交给模型服务处理。
9. 总结与下一步
Agent 提速,最值得先做三件事:给 Agent 加上调用日志,统计每个任务的 LLM 调用次数与耗时;给循环加终止条件,压掉无意义的决策轮;给工具结果加规则分流,减少“看完结果再问一遍”的情况。
最容易踩的坑是只优化模型服务,不看调用次数。换更强的推理引擎、加更大的显存,都只能优化单次调用的速度。如果一次任务要问 30 次,这些优化都会被放大系数吃掉。
后续可以继续扩展的方向包括:更细粒度的缓存策略、模型路由(简单任务用小模型、复杂任务用大模型)、多 Agent 场景下的任务仲裁、以及面向具体业务的 Harness 规则库。真正快起来的 Agent,不是每次回答更快,而是能少问的时候就少问。
