Agent性能优化:不要盲目砍Prompt,先定位这四大瓶颈 很多团队第一次把 Agent 接进真实业务时最常见的反馈不是“它不够聪明”而是“它真慢”。一个原本人工几秒就能完成的查询操作Agent 跑起来可能要十几秒如果中间某次工具调用还失败重试整体体验就更糟。面对这种状况不少人的第一反应出奇一致把 system prompt 砍短。这个反应的逻辑看起来很自洽prompt 越长模型要处理的 token 越多处理速度当然越慢。但真正实践过的人会发现砍完 prompt 后单轮调用的响应速度可能稍微快一点整个 Agent 任务的总耗时却几乎没变化甚至因为删掉了关键约束任务结果还变差了。这正是“骑车放屁”动作是做了但车没更快路没更顺人还白白费了一股劲。为什么用这个有点粗俗的比喻因为骑车时放屁这个动作本质上与“让车更快”这件事毫无因果关系。Agent 变慢也一样在很多真实项目里慢的根源根本不在 prompt 长度上而在循环次数、上下文累积量、工具调用往返、模型推理配置这些更底层的环节。砍 prompt 只是在一个错误的地方做了一次自我感动。这篇文章想解决的是当你接手一个慢 Agent 时应该从哪些维度定位瓶颈砍 prompt 在什么前提下有效、什么前提下无效四个真正值得投入精力的性能瓶颈是什么以及如何用日志和链路数据而不是拍脑袋和直觉来驱动性能优化。如果你正在做 Agent 应用或者准备把 Agent 从 demo 推到生产环境后面的内容建议收藏。1. 这个标题到底在说什么一次砍Prompt优化的失败现场先描述一个在团队里反复出现的场景。Agent 已经能在测试环境跑通核心流程但响应延迟始终压不下来。用户问一句“帮我查一下上周所有异常订单并总结原因”Agent 内部可能要做三到五次大模型推理中间还要查订单库、查日志、算统计最后再汇总。整个链路下来十秒起步是常态。于是有人提出把 system prompt 里那些“角色设定”“注意事项”都删了把 few-shot 示例也去掉只留一句“你是智能助手”。理由是“减少输入 token模型处理更快”。从单次 HTTP 请求的视角看这个推理没有错。模型接口的响应时间确实包含输入处理阶段输入 token 越多首 token 延迟越高。但 Agent 场景的特殊性在于它不是一个“单次请求-响应”模型而是一个带循环、带工具调用、带状态累积的多轮执行系统。总耗时的构成远比你想象中复杂。我见过最典型的情况是system prompt 原本 3000 个 token砍到 500 个 token单次模型调用确实快了。但整个 Agent 任务要循环 8 轮每一轮都会把工具返回的长文本塞进上下文等到第 5 轮时上下文已经涨到 3 万 token。这时候你再怎么优化最初的 system prompt也救不回来。还有一个容易忽略的副作用提示词里的指令往往承担着“防止 Agent 跑偏”的作用。你为了速度删掉“必须先查订单再查库存”“遇到异常不要自行猜测”这样的约束结果 Agent 开始自作主张。最后你会发现你省下的那几百毫秒全都在重试、纠错、人工介入中加倍还了回去。所以这个标题真正想表达的是当 Agent 变慢时砍 prompt 是投入产出比极低的操作。它不是不能用而是不应该被当成第一优先级的优化手段。真正高效的做法是先搞清楚慢到底发生在哪个环节再选择对应的优化策略。2. Agent 的慢到底是哪一种慢先明确耗时构成在讨论怎么优化之前必须先学会拆解耗时。一个 Agent 任务从接收用户输入到返回最终结果通常由下面几个阶段串联而成模型推理Agent 框架在每个决策节点调用大模型让它判断下一步执行什么。这里既包括输入处理时间也包括模型逐个生成输出 token 的时间。上下文处理每一轮循环都会把用户消息、历史对话、工具返回结果拼接成新的 prompt。工具返回的结果越长下一轮输入就越长模型 prefill 阶段就越慢。工具调用往返Agent 调用外部 API、数据库、内部服务时网络延迟、服务端处理时间、结果序列化时间都会叠加到总耗时里。编排与调度框架内部的状态管理、条件判断、错误处理、重试逻辑虽然不是大头但在极端情况下也可能成为瓶颈。解析与渲染模型返回的内容需要解析成结构化动作如果返回格式不合法往往还要重新调用模型修正一次解析失败就可能多出一轮完整循环。如果用一句话概括Agent 的慢通常不是“一个环节慢”而是“很多环节串在一起每个环节都慢一点最后累加成几十秒”。下面的表格可以帮助你快速做初步判断慢的类型典型现象主要根因优化方向单轮调用慢一次模型请求就要 5 秒以上模型较大、输出 token 多、输入过长换模型、限制输出长度、精简输入多轮循环慢单轮不慢但任务要跑很多轮决策不收敛、缺少终止条件、执行步骤过多设置 max_iterations、优化规划逻辑越跑越慢前两轮正常后面每轮都明显变慢上下文持续累积历史消息越来越长滑动窗口、摘要压缩、记忆管理卡在工具调用模型很快但总在等外部接口返回外部服务延迟高、失败重试多增加超时、结果缓存、幂等降级偶发性超时大部分时候正常高峰期频繁超时并发请求导致限流、依赖服务抖动队列削峰、并发控制、兜底策略判断出属于哪一类慢比急着改 prompt 重要得多。如果你的 Agent 属于“多轮循环慢”那砍 prompt 基本没有意义如果属于“越跑越慢”砍 initial prompt 同样无济于事因为真正的元凶是后面不断累积的工具返回结果。从经验看生产中 80% 的 Agent 性能问题都集中在“多轮循环慢”和“越跑越慢”这两类上。换句话说大多数时候你要处理的不是提示词工程问题而是上下文管理和流程控制问题。3. 砍 Prompt 能解决什么不能解决什么不能一竿子打死。砍 prompt 在特定的场景下确实有正面效果只不过它的有效范围被很多人高估了。先说有效的场景。第一种是你的 prompt 确实特别长甚至达到数万 token。一些团队喜欢把完整的业务规范、几十个示例、大量背景资料全部塞进 system prompt。这种情况下砍掉低价值内容、把固定知识移到外部检索可以显著降低模型 prefill 阶段的耗时。第二种是 Agent 每一轮都会重复发送完整 system prompt而不是只发送增量信息。此时精简重复内容相当于每一轮都省了一部分输入开销收益会被循环次数放大。第三种是 prompt 中存在大量重复、互相矛盾的指令导致模型生成时反复“纠结”输出 token 变多。这种情况清理 prompt 结构输出变短速度自然提升。再说无效的场景。如果 Agent 慢是因为循环次数太多比如模型反复在“调用工具”和“得出结论”之间来回试探那么 prompt 写得再精炼也无济于事。如果慢是因为工具调用本身要等外部接口 3 秒砍 prompt 也改变不了网络延迟。如果慢是因为模型要把 800 个 token 的最终报告逐字生成出来那你无论怎么压缩输入输出时间都省不掉。这里有一个经验法则值得记住大模型推理的时间通常由输出 token 数量主导而不是输入 token 数量。输入 5000 个 token 和输入 500 个 token 的差异在 prefill 阶段可能只差几百毫秒但模型每多生成 100 个输出 token可能就要多花 1 到 2 秒。Agent 慢的原因如果是模型生成内容太长你去砍输入侧等于在错误的方向上使劲。所以在决定要不要动 prompt 之前先问自己三个问题当前任务的完整调用链一共触发了几次模型推理如果超过 5 次优先考虑减少轮次。每次模型调用的输入里有多少是重复的历史上下文如果工具返回结果占据了大量比例优先治理工具输出。模型返回的结果是精简的动作指令还是冗长的解释文本如果输出很长优先限制输出格式和长度。把这三个问题想清楚你就知道“骑车放屁”这个比喻到底是在说谁了。4. 真正瓶颈一上下文膨胀与记忆管理上下文膨胀是 Agent 慢最隐蔽的杀手。它不像循环次数那样一眼能看出问题而是像一个不断加重的背包每跑一轮就重一点等任务进行到后半段整个系统的速度已经被拖垮。典型的 Agent 循环长这样模型接到任务后决定调用某个工具工具返回结果后框架把这个结果拼接到消息历史里模型基于新的历史再做下一步决策。问题在于工具返回的结果往往是一大段 JSON 或数据库查询内容。订单系统返回 50 条记录每条 300 个字符一次工具调用就贡献了 1 万多 token。如果这个 Agent 要连续查订单、查库存、查物流三轮下来上下文就从 5000 token 涨到 5 万 token。这个阶段的慢和你在 system prompt 里写了多少字已经没有任何关系了。即使你把 system prompt 砍到只剩十个字上下文里膨胀的部分依然是那几万 token 的工具返回结果。优化思路有三条第一条滑动窗口。只保留最近几轮消息早期历史直接丢弃或折叠成一条摘要。大多数 Agent 任务中最近一两轮的观察结果对当前决策影响最大更早的历史信息价值密度很低。第二条摘要压缩。把旧历史交给模型或规则算法生成一段简洁摘要替换原始长文本。这个方法能保留必要的背景信息同时把 token 消耗降到原来的十分之一甚至更低。第三条结构化记忆。把工具返回结果里的关键字段提取出来只保存结构化数据而不是保存完整原文。比如查询订单后只保留订单号、状态、金额、时间这几个字段而不是把整条 JSON 都塞进历史。下面是一个简单的滑动窗口实现演示了如何在 Agent 循环中控制上下文长度 文件路径examples/context_window.py 演示Agent 循环中动态控制上下文窗口 from typing import List, Dict def trim_history( history: List[Dict[str, str]], max_messages: int 8, reserved_last: int 2, ) - List[Dict[str, str]]: 保留最近 max_messages 条消息。 当历史超过窗口时把更早的消息折叠成一条摘要占位。 if len(history) max_messages: return history keep_last history[-reserved_last:] older history[:-reserved_last] # 实际项目中这里可以调用模型生成摘要也可以按规则提炼关键信息 summary { role: system, content: f已省略 {len(older)} 条历史消息早期对话已压缩。 } return [summary] keep_last # 模拟 ReAct 循环中逐步累积的 history history [] for step in range(20): history.append({role: user, content: f第 {step} 轮观察工具输出}) history.append({role: assistant, content: f第 {step} 轮思考与行动}) trimmed trim_history(history, max_messages8) print(f原始消息数: {len(history)}) print(f裁剪后消息数: {len(trimmed)}) for msg in trimmed: print(f{msg[role]}: {msg[content][:50]})运行这段代码你会看到历史消息从 40 条被压缩到几条早期的轮次被折叠成一条摘要。在实际项目里你可以把这个摘要从“一句话占位”替换成“对早期任务目标和已完成步骤的概括”这样既能控制 token 数量又不至于丢失关键上下文。上下文管理是整个 Agent 性能优化里收益最明显的部分。很多人花大量时间调 prompt但从来不看每一轮实际发送给模型的上下文到底是什么。你可以打开你正在使用的 Agent 框架的日志一一查看每轮请求的 token 数。我敢打赌多数情况下你会发现认真清理一轮工具返回结果比你改十版 system prompt 都管用。5. 真正瓶颈二串行循环与编排层开销第二个瓶颈来自 Agent 的串行循环结构。经典的 ReAct 模式是“思考-行动-观察”循环模型每执行一步就要停下来等一次完整的大模型推理。Plan-and-Execute 模式则需要先规划一个任务列表然后逐个执行更复杂的 Multi-Agent 场景还要多个子 Agent 之间互相通信。每一步都是串行的总耗时等于所有步骤耗时的加和。这里有一个常常被忽略的事实大模型推理本身就不快一次调用几百毫秒到几秒很正常。如果 Agent 要跑 10 轮循环就算每轮模型调用只要 1 秒再加上中间的工具执行总耗时就可能超过 20 秒。而这种循环次数很多情况下并不是任务真的需要而是 Agent 的停止条件设置得太宽松。很多框架默认会给 Agent 一个 max_iterations比如 5 次或 10 次。但如果你的任务本身只需要两步而模型在第三步就给出了结论框架仍然会继续循环直到达到上限。另一种情况是模型每次都给出一个“中间结论”但不够自信非要再调用一次工具确认导致轮次被反复拉长。优化串行循环的核心思路是给 Agent 的运行边界加上硬约束。第一设置合理的最大循环次数。不要无脑用默认值而是根据任务类型调整。简单查询类任务3 次以内就应该结束复杂分析类任务也建议控制在 8 次以内。超出限制直接返回当前结果而不是无限循环。第二设置整体超时时间。在外部调度层给整个 Agent 任务设置一个总超时避免某个子任务卡死拖垮全局。第三允许并行执行独立工具调用。如果模型规划出“查订单”和“查库存”两个互不依赖的动作好的编排框架会并发执行而不是串行等待。Dify、Coze 这类可视化平台的“并行分支”节点本质上就是在解决这个问题。下面是一个带迭代上限和整体超时的 Agent 循环示例 文件路径examples/agent_loop.py 演示给 Agent 循环加上迭代上限和整体超时 import time class AgentLoopTimeout(Exception): pass def run_agent_with_limits( task: str, step_func, max_iterations: int 5, max_seconds: int 20, ): step_func(task, iteration) 表示框架内部执行一次“思考 工具调用”的函数。 start time.time() last_output None for iteration in range(1, max_iterations 1): if time.time() - start max_seconds: raise AgentLoopTimeout( fAgent 执行时间超过 {max_seconds}s已终止 ) print(f[iteration {iteration}] 正在执行...) last_output step_func(task, iteration) if last_output.get(finished): print(Agent 判断任务已完成提前退出循环) return last_output print(f已达到最大迭代次数 {max_iterations}使用当前结果返回) return last_output # 使用示例 def simple_step(task: str, iteration: int): # 实际项目里这里会调用 LLM 工具执行器 time.sleep(1) return { finished: iteration 4, result: f{task} 处理到第 {iteration} 轮, } if __name__ __main__: result run_agent_with_limits(订单查询, simple_step) print(最终结果:, result)这个示例的要点有两个一是循环次数有上限二是整体时间有上限。在实际项目中你还需要在 step_func 里记录每一轮的大模型调用耗时和工具调用耗时这样才能知道循环次数到底花在了哪里。还有一个容易被忽视的细节模型返回的格式解析失败会触发额外轮次的“纠正”。如果模型不按你要求的 JSON 格式返回框架可能自动重新调用一次模型来修正这就等于白白多了一次完整的大模型推理。解决方法是使用模型服务商提供的结构化输出能力比如 JSON Mode 或 Function Calling从源头保证返回格式正确。6. 真正瓶颈三工具调用的成本与失败重试如果上下文膨胀是“隐性杀手”那么工具调用慢就是“显性元凶”。很多 Agent 慢不是模型慢而是它在等外部服务。工具调用的成本由三部分组成网络往返时间、服务端执行时间、结果传输与解析时间。如果工具调用的是内部 RPC 服务单次可能在几十到几百毫秒如果调用的是第三方 HTTP 接口动辄一两秒很正常。Agent 每执行一次决策都要先发起一次工具调用再等结果回到模型中这个等待时间是纯串行叠加的。更糟糕的是失败重试。外部接口不稳定时框架默认的重试策略可能会连续重试三次每次等待时间还递增。一次本来只需要 1 秒的查询如果连续失败重试可能变成 5 秒甚至 10 秒。而每一次失败模型都要重新生成一次“调用工具”的决策,成本又被放大。优化工具调用重点不是改 prompt而是改调用策略。给每个工具调用设置超时时间。不要让 Agent 无限等待一个已挂起的请求建议超时控制在 1 到 3 秒。减少重试次数并且使用退避策略。连续失败两次后应该降级返回错误信息给模型让模型改变策略而不是盲目重试同样一个请求。对工具返回结果做长度限制。只返回模型决策必需的字段不要返回完整的大字段内容。很多 Agent 慢就是因为工具把 100 条订单记录全量返回模型根本不需要看完这些数据但它仍然被迫处理。对幂等查询类工具做结果缓存。同一个任务中模型可能重复查询同一个订单号或同一份配置信息。缓存可以避免重复的网络开销。为工具调用建立白名单和权限边界。Agent 只能访问它真正需要的服务避免模型因幻觉调用一些不存在的工具白白浪费请求。这里还要专门提醒一点工具返回的结果格式最好在工具侧就完成结构化。比如数据库查询结果不要直接返回原始 SQL 查询结果集而是先用一个聚合函数或格式化模块把数据处理成模型容易消费的摘要形式。否则模型拿到一大段原始数据往往还要额外花一轮去“整理”又慢又容易出错。7. 真正瓶颈四模型选择与推理配置前三个瓶颈都在系统结构层面第四个瓶颈则跟模型本身有关。很多团队对所有 Agent 任务都用同一个大模型不管任务简单还是复杂都用最大的模型、最高的上下文上限、最宽松的生成参数。这本质上也是一种资源浪费。不同任务对模型能力的要求差异很大。判断用户意图、抽取结构化字段这类任务用轻量级小模型就够了而复杂的多步规划、代码生成、长文本推理才需要更大的模型。一种常见的方案是“任务路由”先用一个小而快的模型判断任务类型再根据类型把请求分配给不同的模型。复杂任务走大模型简单任务走小模型整体延迟和成本都能降下来。推理参数同样值得关注。下面是一个比较稳妥的模型调用配置参考具体参数名以你实际使用的服务商为准{ model: your-model-name, temperature: 0, max_tokens: 1024, stop: [|end|], response_format: { type: json_object } }temperature 调成 0 或接近 0能让模型输出更确定减少无意义的发散。max_tokens 不要设成无限大应该根据任务的预期输出长度设定一个合理上限如果模型不需要生成 2000 个 token 的答案你给它一个 4096 的上限它反而可能“为了凑满”而输出冗余内容。stop 参数设置终止符能避免模型生成一些无关的结尾。response_format 指定 JSON 结构化输出可以让后续解析环节少很多工作量。这里需要说明的是模型推理配置和 prompt 长度有一定关系但更主要的影响来自“输出长度”和“模型规模”。所以这部分的优化重点应该是你选对模型了吗你给了它合理的输出边界吗另外一个常有争议的点是上下文窗口大小。很多 Agent 框架默认将模型的上下文上限设置成最大值这会导致模型在容量充足的假象下不断积累大量无用历史。更合理的方式是把上下文窗口当作一个预算来管理而不是一个可以随便填满的仓库。你的任务需要多少上下文就分配多少超出部分执行压缩或截断而不是无限扩张。8. 用数据优化给 Agent 加上链路计时与日志我见过太多团队做 Agent 性能优化时靠的是“体感”和“猜测”。有人说“可能是 prompt 太长”有人说“可能是模型太慢”但没有一个人能拿出每一轮调用的耗时数据。这种讨论最终只会变成一场比嗓门大的会议。正确的方式是给 Agent 加上可观测性。你不需要一开始就接入很复杂的链路追踪系统最简单的一步是在框架的关键节点打印日志模型调用耗时、输入 token 数、输出 token 数、工具调用名称与耗时、当前循环次数、当前上下文长度。把这几条数据打到日志里跑一次任务你立刻就能定位慢在哪个环节。下面是一个简单的计时装饰器示例可以用在 Agent 内部的方法上 文件路径examples/timing_decorator.py 演示给 Agent 内部各环节加计时和上下文长度统计 import time from functools import wraps def timed_step(name: str): def decorator(func): wraps(func) def wrapper(*args, **kwargs): start time.perf_counter() result func(*args, **kwargs) cost time.perf_counter() - start context_tokens result.get(context_tokens, 0) output_tokens result.get(output_tokens, 0) print( f[timing] {name}: {cost:.2f}s | fcontext{context_tokens} output{output_tokens} ) return result return wrapper return decorator # 模拟一次 LLM 调用和一次工具调用 timed_step(llm_reason) def call_llm(task: str): time.sleep(1.2) return { context_tokens: 12000, output_tokens: 300, content: 调用结果, } timed_step(tool_search) def call_tool(name: str): time.sleep(0.8) return {result: 工具返回值} call_llm(查询用户订单) call_tool(order_api)运行这段代码你会得到类似下面的输出[timing] llm_reason: 1.20s | context12000 output300 [timing] tool_search: 0.80s | context0 output0这个信息量已经足够帮你做初步判断。如果某次任务中 llm_reason 被调用了 6 次总共耗时 7.2 秒而 tool_search 总共只耗时 1.6 秒那么性能瓶颈显然在模型调用轮次上而不是工具延迟。这时候你该做的是减小循环次数、优化上下文、精简输出而不是去砍 prompt。在更复杂的生产环境中建议用 OpenTelemetry 或类似的工具把日志结构化把每次任务的耗时拆成模型耗时、工具耗时、框架耗时三个大块再按任务类型聚合。这样你就有了一个“慢任务报表”而不是一堆分散的日志文件。有了数据之后再做优化决策就踏实很多了。9. 常见问题排查清单与最佳实践最后把实战中容易踩的坑整理成一张排查表方便你在遇到性能问题时快速定位。问题现象可能原因排查方式解决方案单轮模型调用就要 5 秒以上模型过大或输出 token 过多查看请求日志里的 output_tokens换更小模型限制 max_tokens前两轮正常后面每轮越来越慢上下文持续累积历史消息过长打印每轮请求的 context_tokens滑动窗口、摘要压缩、清理工具返回结果任务要循环 10 次以上才能结束缺少终止条件或模型决策不收敛统计实际循环轮次设置 max_iterations、优化规划逻辑、提前终止总耗时里工具等待占比很大外部接口慢或重试过多给工具调用单独加计时日志设置超时、结果缓存、降级策略、减少重试模型返回经常解析失败导致额外轮次输出格式不稳定查看框架日志中的 parse error用 Function Calling 或 JSON Mode 结构化输出报错 Error rendering prompt with jinja templateprompt 模板变量渲染失败查看模板引擎错误详情检查模板变量、函数调用和截断逻辑不要急着砍 prompt 内容报错 invalid prompt was flagged内容策略拦截而非性能问题检查触发关键词和服务端策略调整触发词或更换模型服务与性能优化分开处理在最佳实践层面下面几条来自实际项目的经验排序值得优先执行先治理工具返回结果。每次模型调用前检查上下文里有没有多余的大段原始数据。把工具返回内容裁剪到模型决策真正需要的范围是收益最高的操作。再设置循环边界。所有 Agent 任务无论看起来多简单都必须有最大迭代次数和整体超时。这是防止性能失控的最后一道防线。然后选择模型和推理参数。根据任务难度分层使用模型合理设置 temperature、max_tokens 和结构化输出减少无意义生成和解析失败。最后才考虑 prompt 优化。如果前面的环节都做完了任务仍然慢再去看 prompt 是否存在冗余、重复或导致模型输出过长的结构问题。关于 Agent 安全Agent 的工具权限必须遵循最小授权原则外部输入不要直接注入到 prompt 里工具调用要记录审计日志。性能优化的前提是行为可控一个跑得快的失控 Agent比一个慢但安全的 Agent 危险得多。另外一个容易被忽略的工程习惯是每次只改一个变量。比如这轮只优化工具返回结果长度下轮再只调整 max_iterations。不要同时改 prompt、改模型、改工具否则任务变快了你也不知道是哪一步起的作用。把每次修改的耗时数据记录下来对比才能形成可持续优化的闭环——这里说的闭环是指“修改-测量-对比-再修改”这个工程循环而不是一个空洞的词汇。结尾回到标题。Agent 慢、砍 prompt 是“骑车放屁”并不是说 prompt 完全不用管而是说要分清解决的顺序。性能优化的起点永远是数据和链路分析而不是一个听起来合理的猜测。下次再有人提议“把 prompt 砍短点试试”你可以先请他回答一个问题慢到底慢在循环、上下文、工具调用还是模型推理如果答不上来先上日志再上剪刀。提示词工程依然重要但它在 Agent 体系里只是其中一环。把上下文管理、循环控制、工具调优和模型配置做扎实往往比纠结那几百个 token 的 system prompt 更值得投入。希望这篇文章能让你在下次面对一个慢 Agent 时不再只会砍 prompt而是能准确指出病根在哪里。