Agent性能排查:为什么砍了Prompt还是慢? 📅 发布时间:2026/9/8 5:28:54 👁 浏览次数: 从一次真实的 Agent 性能排查说起。最近在优化一个基于大模型的 Agent 项目时团队内部第一轮讨论就出现了“把 System Prompt 砍短一点”的声音。因为大家观察到每次请求都带上了接近两千 Token 的上下文直觉上认为上下文长是响应慢的元凶。可是砍完 Prompt 之后整体耗时几乎没有变化浪费了半天时间问题其实出在工具调用串行和模型多次试错上。这个场景让我想起一句很形象的话“Agent 慢砍 Prompt就是骑车放屁——劲儿使错了地方。”这句话听起来粗但用来形容性能优化里“用表面因素解释深层问题”的现象非常准确。很多人觉得 Prompt 越长模型读得越慢所以 Agent 慢一定是 Prompt 太长。实际上Agent 是一个包含模型推理、工具调用、循环决策、外部 IO 的复杂系统任何一个环节都可能成为瓶颈。如果只盯着 Prompt可能既没有解决延迟问题还因为删掉了必要约束导致回答质量下降。本文会从 Agent 的运行链路讲起分析慢的根因再给出从测量到优化的完整方案帮助你把时间和资源花在真正有效的环节上。1. Agent 慢为什么不能只盯着 Prompt1.1 “骑车放屁”到底是什么意思“骑车放屁”是一个民间调侃说一个人骑着自行车时放了一个屁以为放屁能产生反推力让车跑得更快。实际上屁的力量根本不足以推动自行车甚至可能因为裤腿漏风让车速更慢。用这个隐喻看 Agent 性能优化就是在说你花了很多力气调整一个看起来有关、实际上无关或影响很小的因素最后不仅没加速还可能因为过度裁剪而破坏了原有功能。在 Agent 项目中Prompt 是用户与大模型交互的“说明书”它确实会影响模型的理解质量和响应长度。但 Agent 慢不等于 Prompt 慢。一个完整的 Agent 请求往往要经历多次模型调用、多轮工具调用、结果解析和状态判断。假设一个任务需要调用 5 次模型每次模型调用 2 秒其中 Prompt 带来的 prefill 时间只有 0.8 秒那么即使你把 Prompt 砍成原来的十分之一总耗时的下降也极其有限。1.2 Agent 慢带来的真实代价Agent 的响应时间不仅仅影响用户体验还会直接抬高成本。大模型推理是按 Token 计费的如果 Agent 因为循环设计不合理多调用了几次模型或者因为上下文管理不当导致输入 Token 越来越大每一次失败重试都会产生额外费用。线上问题排查时如果没有监控数据很难判断时间到底消耗在模型层、工具层还是计算逻辑上。更麻烦的是延迟一旦变高用户会频繁重试重试又会叠加外部工具的压力形成“慢 - 超时 - 重试 - 更慢”的恶性循环。所以优化 Agent 性能的第一步不是改 Prompt而是先搞清楚延迟分布再决定动哪里。1.3 砍 Prompt 什么时候有效什么时候无效砍 Prompt 在部分场景下确实有效但不能无脑砍。当 Prompt 中包含了大量重复的历史对话、过时工具说明、冗余示例时删除这些内容能明显缩短输入长度降低每次模型调用的 prefill 延迟。比如一个客服 Agent 的 System Prompt 里塞了二十条业务规则和五个长示例但当前用户的问题只涉及其中两条规则这时把无关规则拆走是合理的做法。但如果 Prompt 本身已经足够精简Agent 慢的根因是工具接口响应慢、模型输出 Token 太长、循环次数太多或者多工具调用是串行执行的继续砍 Prompt 就不会有显著效果甚至可能让模型丢失关键约束导致它无法正确选择工具或提前终止反而多跑几轮。优化之前必须先判断瓶颈在哪。2. Agent 运行链路与性能瓶颈拆解2.1 Agent 的完整处理流程为了分析性能我们先把 Agent 的执行过程拆开。不同框架的实现细节不一样但整体链路可以概括为接收用户任务、理解任务、制定计划、调用工具、观察结果、循环决策、输出最终答案。每一步都可能产生延迟。理解任务和制定计划需要模型推理调用工具需要等待外部服务响应观察结果需要解析数据循环决策意味着上述步骤可能反复多次。最终用户感受到的耗时是所有这些子步骤耗时的总和而不是某一次模型调用的时间。2.2 主要瓶颈通常分布在哪里根据我做过的多个 Agent 项目经验性能瓶颈主要有以下几个来源。第一是模型推理延迟。模型推理包含 prefill 和 decode 两个阶段。prefill 是模型读取输入 Token 并计算上下文特征的过程输入越长prefill 越慢decode 是模型逐个生成输出 Token 的过程输出越长decode 越慢。不同模型、不同部署方式下这两个阶段的耗时差异很大。第二是上下文长度失控。很多 Agent 会把每一轮的工具调用结果都追加进消息列表导致后续轮次的输入越来越长。比如第一轮输入是 1000 Token第三轮可能变成 5000 Token第五轮就可能超过 8000 Token。输入长度增长后prefill 时间会明显增加模型在大量噪声中提取关键信息的难度也会变大。第三是工具调用次数和串行执行。一个任务可能需要调用搜索、数据库、计算器等工具如果这些调用彼此没有依赖关系但代码里写成了串行那么总耗时就是各个工具耗时的简单相加。如果某个工具本身响应就要 1 秒调 5 次就是 5 秒。第四是循环轮次过多。Agent 经常通过“规划-执行-观察”循环来完成任务。如果 Prompt 里没有设置清晰的终止条件或者模型反复选择错误的工具就可能陷入无效循环。每多一轮就多一次模型调用和工具调用。第五是外部服务 IO 和重试策略。Agent 依赖的搜索接口、数据库、业务 API 都可能存在网络抖动。如果重试策略是错误后等待 3 秒再重试一次短暂故障就可能让整个任务超时。2.3 Prompt 长度在总耗时中占多大比例很多人对 Prompt 长度的影响存在误解。我们常说“Prompt 越长越慢”这句话只对了一半。模型处理输入有一个 prefill 阶段输入越长 prefill 越慢但最终的响应时间还包含 decode 阶段。如果一个 Agent 的任务是“写一篇 800 字文章”输出 Token 可能有两三千decode 时间会占大头此时 Prompt 长短的影响就被稀释了。反过来如果一个 Agent 的任务是“根据用户问题返回一个工具调用的 JSON”输出很短输入却很长那么 prefill 占比就很高削减 Prompt 会看到明显收益。所以不能笼统地说“砍 Prompt 没用”或“砍 Prompt 一定有用”而要看当前场景中 prefill 和 decode 的占比。3. 实验设计先给 Agent 做个性能画像3.1 实验环境说明在优化之前我们先搭一个最小可运行的 Agent 示例给整个流程做个性能画像。以下示例使用 Python 3.9 环境只依赖标准库time和concurrent.futures不依赖具体的大模型 SDK。真实项目中你可以把llm_call换成 OpenAI SDK、Claude SDK 或自己部署的模型服务但统计思路完全一致。为了避免误导我不在本文中写死任何框架版本。大模型 SDK 和 Agent 框架更新很快你只要把握两个要点一是每个关键步骤都要有时间记录二是每个决策都基于数据而不是直觉。3.2 构造一个最小 Agent 执行流程下面这段代码模拟了一个最简单的 Agent先让模型制定计划然后循环调用工具最后生成答案。为了演示llm_call函数用time.sleep模拟模型推理耗时输入字符数影响 prefill 耗时output_len影响 decode 耗时。# agent_demo.py import time def llm_call(prompt: str, output_len: int 200) - str: 模拟一次大模型推理调用。 用 sleep 模拟时延 - 输入越长prefill 越慢 - output_len 越大decode 越慢 prefill_delay 0.02 * len(prompt) / 100 decode_delay 0.005 * output_len time.sleep(prefill_delay decode_delay) return f[模拟回复] 输入 {len(prompt)} 字符输出 {output_len} 字符 def call_tool(tool_name: str, params: dict) - dict: 模拟外部工具调用。 time.sleep(0.3) return {tool: tool_name, result: ok, params: params} def run_agent(task: str, system_prompt: str, max_turns: int 3): start time.time() steps [] # 第一步模型根据任务制定计划 t0 time.time() plan_prompt f{system_prompt}\n任务{task}\n请先给出计划。 plan llm_call(plan_prompt, output_len80) steps.append((plan, time.time() - t0)) # 第二步循环执行工具调用 for i in range(max_turns): t0 time.time() tool_prompt f{system_prompt}\n当前计划{plan}\n请选择工具并给出参数。 tool_decision llm_call(tool_prompt, output_len50) steps.append((fdecide_tool_{i}, time.time() - t0)) t0 time.time() result call_tool(search, {q: task}) steps.append((fcall_tool_{i}, time.time() - t0)) # 第三步生成最终答案 t0 time.time() answer_prompt f{system_prompt}\n任务{task}\n工具结果{result}\n请生成最终答案。 answer llm_call(answer_prompt, output_len300) steps.append((final_answer, time.time() - t0)) return answer, steps, time.time() - start if __name__ __main__: system_prompt ( 你是一个智能助手。你的职责是帮助用户完成任务。 你需要先制定计划然后调用工具最后给出答案。 ) answer, steps, total run_agent(帮我查询北京今天的天气) print(总耗时: %.2fs % total) for name, cost in steps: print(%s: %.2fs % (name, cost))这个代码可以直接运行。你会在输出中看到类似下面的结果总耗时: 3.58s plan: 0.14s decide_tool_0: 0.16s call_tool_0: 0.30s decide_tool_1: 0.16s call_tool_1: 0.30s decide_tool_2: 0.18s call_tool_2: 0.30s final_answer: 1.04s注意这个输出是模拟数据实际运行会因为机器性能不同而变化。关键是通过埋点你能一眼看出哪一步最贵。3.3 对比实验砍掉 Prompt 后到底能快多少我们在上面代码的基础上做一个简单对比。把system_prompt从 500 个字符缩短到 50 个字符再运行一次 Agent。你会发现plan、decide_tool、final_answer这几个步骤的耗时都有轻微下降但call_tool的耗时完全不变。如果业务场景里工具调用占了总耗时的一半那么砍 Prompt 只能优化另外的一半总耗时下降比例自然很有限。这就是为什么不能凭感觉砍 Prompt而要先做性能画像。这个对比实验也解释了文章标题里的比喻在很多 Agent 场景中砍 Prompt 就像骑车放屁看起来在用力但真正的“车速”由工具调用、循环轮次和模型输出长度决定。4. 实战从“砍 Prompt”转向系统性优化4.1 优化前的性能画像要怎么做在生产项目中建议把 Agent 的每一次运行都记录成结构化的性能日志至少包含以下字段任务 ID、用户问题、System Prompt 长度、历史上下文长度、工具调用列表、每一步耗时、模型调用次数、输出 Token 数、是否重试、最终状态。有了这些数据你可以统计同一个任务的耗时分布定位到底哪一步是主要矛盾。常见工具包括 OpenTelemetry、Langfuse、LangSmith也完全可以先用 JSON 日志输出到本地或 ELK。简单的做法是在start_span和end_span之间埋点用一个统一的函数记录。如果没有可观测性数据就贸然优化很容易出现“今天改 Prompt 明天改参数”的盲目状态。4.2 Prompt 不是不能砍而是要看怎么砍虽然文章标题在调侃盲目砍 Prompt但这不代表 Prompt 不需要优化。正确的做法是根据性能画像决定砍哪里、保留哪里。首先把 System Prompt 结构化。一个清晰的 Prompt 模板至少包含角色、任务、约束、输出格式、示例五部分。下面是一个简化模板system_prompt 你是{role}。 任务{task} 约束 - {constraint1} - {constraint2} 输出格式{output_format} 示例 {examples} 这样拆分的好处是你可以按需拼接不同的约束和示例而不是把所有规则一次性打包。比如用户问天气问题时不需要把“订单退款规则”也塞进上下文。其次把不常用的大段示例移出主 Prompt改成动态检索。当模型需要处理某一类任务时再从知识库或配置中心拉取对应的 few-shot 示例。这样既保证了模型质量又控制了输入长度。最后避免在每一轮循环中都重复注入相同的完整工具说明。工具描述可以只在第一轮注入后续轮次只传工具名和必要的参数说明。4.3 缩短上下文摘要与记忆压缩Agent 运行过程中历史消息会不断累加。工具返回结果可能很长多轮之后上下文会膨胀。单纯的滑窗会丢失重要信息更好的方式是对旧的历史消息做摘要。下面是一个简化的历史消息压缩逻辑当消息总长度超过阈值时调用一次模型生成摘要把旧消息替换成摘要。注意这是示例思路真实项目中的摘要粒度、消息结构需要根据框架调整。# context_compress.py示例片段 def compress_history(messages, max_chars2000): total_chars sum(len(m) if isinstance(m, str) else len(m.get(content, )) for m in messages) if total_chars max_chars: return messages # 保留第一条系统消息和最后一条用户消息 head messages[:1] tail messages[-1:] body messages[1:-1] summary_prompt 请把以下对话内容压缩成摘要保留关键事实和结论\n summary_prompt \n.join( m if isinstance(m, str) else m.get(content, ) for m in body ) summary llm_call(summary_prompt, output_len100) return head [{role: system, content: 历史对话摘要 summary}] tail这个函数的核心思路是不要让模型每次都读取所有原始历史而是用一次模型调用换后半程多次调用的输入缩减。在长对话场景下这种做法通常能显著降低总体耗时和成本。除了摘要还可以使用向量数据库做记忆检索。把所有历史内容切成片段并向量化每次需要的时候只检索与当前问题最相关的片段。这样上下文长度不再是线性增长而是大致保持在一个固定范围内。4.4 减少无效模型调用缓存与路由很多 Agent 任务存在大量重复请求。同样是查天气、查订单状态、计算公式如果每次都重复调用大模型既慢又贵。一个简单的语义缓存可以解决一部分问题。下面的示例使用functools.lru_cache做精确匹配缓存。生产环境中更常用的是把用户请求、Prompt 模板和模型响应存到 Redis同时对请求做归一化或向量化命中相似请求时直接返回缓存结果。from functools import lru_cache lru_cache(maxsize128) def cached_llm_call(prompt: str) - str: # 正常情况下这里会调用真实模型 return llm_call(prompt, output_len200)路由是另一个低成本方案。不同难度的任务可以使用不同规格的模型简单分类、抽取任务走小模型复杂推理、长文本生成走大模型。下面是一个极简路由示例def route_task(task: str) - str: simple_keywords [翻译, 计算, 时间, 天气] if any(k in task for k in simple_keywords): return small-model return large-model真实项目中你可以用一个专门的小模型或规则引擎先做任务分类再决定调用哪个模型。这样能明显降低平均耗时和成本。4.5 工具调用与并行化工具调用是很多 Agent 项目的最大延迟来源。如果多个工具调用之间没有依赖关系就不应该串行等待。下面是用线程池并行调用多个工具的示例from concurrent.futures import ThreadPoolExecutor def call_tool_wrapper(item): tool_name, params item return call_tool(tool_name, params) tasks [ (search, {q: 北京天气}), (search, {q: 北京限行}), (calculator, {expr: 12}), ] with ThreadPoolExecutor(max_workers3) as pool: results list(pool.map(call_tool_wrapper, tasks)) for r in results: print(r)并行调用能把总耗时从三个工具耗时相加变成最慢的一个工具耗时。但使用时要小心并发访问数据库或第三方接口可能触发限流甚至造成数据一致性问题。建议在测试环境中充分验证再上线到生产流量。此外要检查工具接口本身是否慢。有时一个查询接口没有使用索引或者第三方服务的响应体过大会拖慢整个 Agent。这些问题不是 Prompt 能解决的需要从接口层面优化。4.6 流式输出与超时控制当 Agent 最终需要生成很长的回答时用户等待的时间主要是 decode 时间。我们可以通过流式输出让用户在模型生成第一个 Token 后就开始阅读极大改善体感延迟。主流大模型 SDK 基本都支持streamTrue参数。除了流式超时控制也非常重要。如果外部工具一直没有返回模型服务一直没有响应Agent 可能会长时间卡住。一个简单的兜底方案是在调用时传入超时时间并且在整体任务级别设置最大执行时间。下面是一个配置示例具体字段需要根据你使用的框架调整agent: max_iterations: 5 max_execution_time: 60s parallel_tool_calls: true request_timeout: 30s cache: enabled: true ttl: 3600这里的核心思想是Agent 必须有明确的“止损”机制不能无限循环不能无限等待。超时与重试策略要分开设计超时之后是直接失败、降级返回还是换一条路径重新规划需要提前想清楚。4.7 Agent 框架选型与参数调整目前常见的 Agent 框架有 LangChain、LlamaIndex、AutoGen、CrewAI 等各有特点。框架会提供一些现成的 Agent Executor但它们的默认参数未必适合你的场景。你需要重点调整的参数包括最大迭代次数、最大执行时间、是否允许多工具并行、重试次数、上下文窗口大小、记忆管理策略。不要拿着默认参数直接上生产要先在测试数据集上跑一遍结合耗时和成功率调整。通常我会用一个配置中心或独立的 YAML 文件管理这些参数而不是写死在代码里。这样在排查性能问题时可以快速修改参数并做 A/B 对比。5. 常见误区与高频问题排查5.1 常见问题排查表下面这张表总结了 Agent 性能优化中经常遇到的问题、原因和解决思路。问题现象常见原因解决思路任务总是超时模型调用次数过多Agent 陷入无效循环限制max_iterations优化终止条件Prompt 很短但响应依然很慢输出 Token 太长decode 阶段耗时高限制max_tokens使用流式输出每次调用工具都很慢外部接口延迟高或工具串行调用并行化工具调用优化外部接口砍了 Prompt 没有效果没找到真实瓶颈工具调用或输出阶段是主因先做性能画像再决定优化方向后面几轮越来越慢历史消息不断膨胀上下文太长做消息摘要、滑窗或向量检索模型反复做出错误工具选择Prompt 中工具描述不清晰或缺少约束精简工具描述增加工具使用示例外部接口短暂故障导致整体卡死重试策略不合理缺少超时控制配置超时和指数退避加上熔断5.2 为什么改了 Prompt 还是慢很多开发者在优化 Agent 时遇到的最大困惑是“已经删了很多 Prompt为什么响应速度没变化”。原因通常是性能瓶颈根本不在输入长度上。你可以做一个简单实验保持业务逻辑不变只把系统 Prompt 改成空字符串运行同一个任务。如果耗时只减少了 10%那说明 Prompt 不是主要矛盾。这时再去看工具调用耗时、模型输出长度和循环次数。还有一个容易被忽略的点模型提供商或部署平台可能存在冷启动、限流、排队等情况。如果在某个时间段内所有请求都慢可能不是你代码的问题而是服务端负载变高了。5.3 如何避免无效的 Prompt 删减删减 Prompt 前先问自己三个问题第一这段内容是否会在每次模型调用中都反复出现如果只是某个子任务的提示放主 Prompt 里就是浪费。第二这段内容是给模型看的约束还是给开发者自己看的备注如果是注释应该扣在代码里而不是塞进模型上下文。第三删除后是否会影响模型准确率建议准备一个小的评测集覆盖常见任务在删减前后分别跑一遍比较成功率、格式正确率和用户满意度。只有在评测集通过后才算一次有效的 Prompt 优化。6. 工程最佳实践与可观测性建议6.1 先定指标再谈优化Agent 性能优化不能只看“快不快”还要看“好不好”。建议至少关注以下指标响应时间、Token 消耗、模型调用次数、工具调用次数、成功率、重试率、成本。每个指标都不是孤立的。比如缩短 Prompt 可能降低 Token 消耗但可能导致模型理解能力下降增加重试次数最终总成本更高。所以在优化时要同时监控多个指标而不是只盯着某一个。6.2 Prompt 版本管理与评估把 Prompt 当成代码一样管理。不要把 Prompt 直接写死在 Python 文件里最好放到配置中心或独立文件中并记录版本号。每次修改 Prompt都记录修改人、修改时间、修改原因和评估结果。建立一个小型评测集非常重要。你可以把线上最常见的 100 个用户问题做成数据集每次调整 Prompt 后自动跑一遍评测集比较输出质量、工具选择准确率、终止条件是否有效。这样才能防止“为了提速牺牲准确率”。在真实项目中Prompt 的效果会随着模型版本升级而波动。定期回归评测集才能保证 Agent 长期稳定。6.3 可观测性日志与链路追踪Agent 的可观测性比传统接口更复杂因为一次用户请求会经历多次模型调用和工具调用链路很长。建议在关键节点输出结构化日志至少包含以下内容{ task_id: task_20250101_001, user_query: 帮我查北京天气, prompt_length: 1200, context_length: 3500, steps: [ {step: plan, duration_ms: 140}, {step: decide_tool_0, duration_ms: 160}, {step: call_tool_0, duration_ms: 300}, {step: final_answer, duration_ms: 1040} ], model_calls: 5, total_tokens: 4200, success: true }如果使用 OpenTelemetry可以把每个子步骤都记录成一个 Span这样在 Jaeger 或 SkyWalking 里能看到完整调用链。可观测性到位后性能问题的定位会快很多。6.4 安全边界与生产变更注意事项优化 Agent 性能时要注意几个安全边界。第一API 密钥和模型服务的访问凭证不能硬编码在代码中建议使用环境变量或密钥管理服务。第二涉及数据库查询、删除、更新等工具调用时必须遵循最小权限原则Agent 默认应该只有只读或受控权限。第三生产环境变更前先在小流量或测试环境验证不要直接把新 Prompt 和新参数推送到全量流量。另外外部工具调用需要考虑限流和熔断。如果某个工具服务不稳定Agent 不能无限制地重试否则可能把故障放大。建议配置最大重试次数、退避策略和熔断阈值。6.5 成本与性能的平衡追求极致速度的前提是不能把成本打爆。模型路由、缓存、上下文压缩、批处理都是控制成本的有效手段。例如一个客服 Agent 可能 70% 的请求是常见的“查余额”“查订单状态”这类请求完全可以用缓存或小模型解决只有剩下的复杂问题才交给大模型。这样平均延迟和单次成本都会下降。另外有些运行时间较长的 Agent 任务可以改成异步模式。前端先返回“任务已提交”后台任务完成后再通过 Webhook 或轮询通知用户。虽然单次任务绝对耗时没有变化但用户感知延迟大大降低了。7. 总结与下一步学习路线这篇文章的核心观点是Agent 慢的时候不要第一时间想到砍 Prompt而要先搞清楚慢在哪。你可以先用最小示例和日志埋点做性能画像再针对性地优化 Prompt 结构、上下文管理、模型调用次数、工具调用并发和超时策略。砍 Prompt 只是众多手段之一不是万能钥匙。如果你想继续深入建议按下面的路线学习。第一学好 Prompt Engineering但核心目标是“让模型一次做对”而不是“让 Prompt 变短”。第二阅读常用 Agent 框架的源码重点看 Agent Executor 的循环逻辑和参数配置。第三学习可观测性工具把每次模型调用和工具调用都变成可视化数据。第四多做实验记录不同参数组合下的延迟、成本和成功率。如果你遇到 Agent 性能问题不妨先从日志和埋点看起。数据不会骗人而“骑车放屁”式的优化除了让自己感觉在做努力并不会让车跑得更快。希望这篇文章能帮你少踩一些坑把力气用在真正有用的地方。