这段时间后台经常有人问我:Agent 老是跑飞,把 Prompt 压缩到极限还是不行,是不是模型选得不对?我发现很多团队看到 token 账单之后,第一反应就是把 Prompt 剪了又剪,恨不得把所有示例都删掉。其实这个方向大概率是反的。让 Agent 少踩坑的关键不在 Prompt 压缩,而在你是否愿意为它加上一套工程兜底。我自己的体会是:把失败率从 20% 压到 2%,比省 5% 的 token 成本实在得多——省下的重试、调试、人工干预,才是真正的大头。这篇文章不聊虚的,我用一个具体项目来拆解,我是怎么让一个 Agent 从“十次里崩三次”变成“连续四十次稳定跑完”的。
1. 先算一笔账:压缩 Prompt 省下的 token,远没有重试烧掉的多
1.1 真实账单:一次失败的隐性成本
先说说省钱这回事。很多人算成本只盯着 Prompt 本身用了多少 token,但真正在烧钱的是 Agent 失败之后的连锁反应。一次调用失败,不会只损失这一次的钱——日志要打,错误要分析,可能还要重新调用一次,如果模型在同一个地方反复出错,你可能会手动改 Prompt、改参数再跑一轮。这里每一环都是成本。
我给你算一笔典型的账。假设一个 Agent 每次完整任务要调用模型 5 次,平均每次消耗 4000 个 token(输入 + 输出),单次调用按市面上主流模型的价格折算大概几厘钱。看起来一次任务也就几分钱,但如果这个 Agent 的任务失败率是 20%,意味着每 5 次任务就有 1 次要重跑。重跑不是简单再来一遍,很多场景下重跑会带着上一次的部分错误状态:信息重复写入、工具被重复调用、上下文越滚越长。最终这个 Agent 的实际 token 消耗可能是理想状态的 1.6 倍到 2 倍。
更大的开销在人工。每次 Agent 跑完结果不对,你得去翻 trace、找原因。我见过一个团队,每天要花一个多小时处理一个老 Agent 的错误,一个月下来就是几十个小时。如果这些时间用在打磨稳定性上,早就能把失败率降下来了。所以我说,压缩 Prompt 省下来的那点 token,跟失败链路烧掉的钱完全不是一个量级。
1.2 为什么“压缩”本身也是一项高成本工程
再说一个反直觉的点:把 Prompt 从 2000 字压到 800 字,本身就要付出很高的维护成本。Prompt 压缩不是删几段话那么简单——你删掉了示例,模型对输出格式的跟随能力就会下降;你精简了背景说明,模型在多轮对话语境里就容易“失忆”。为了找回这些能力,你又得在代码里写一堆后处理逻辑,加一堆硬编码规则。这一步其实就是把 Prompt 的成本转移到了代码维护上。
而且 Prompt 压缩有边际递减效应。一个写得很满的 Prompt 可能已经包含了任务规则、输出格式、边界条件、示例。你压掉其中一部分,表面上看每次调用省了几百 token,但如果模型因此多犯了 2% 的错误,这 2% 的错误带来的重试开销,轻松超过你省下的 token。我在项目里试过:把一段 1500 token 的工具使用说明压缩到 700 token,结果模型开始混淆两个相似工具的边界,该调 A 工具的时候去调 B 工具。单次调用确实省了,整体成功率掉了近 8 个百分点,最后只能老实加回去。
所以我的结论是:压缩 Prompt 不是不能用,而是它只适合在失败率已经很低的情况下做“锦上添花”。在 Agent 还频繁出错的时候,真正该做的是系统性降低失败率。
1.3 换个思路:降失败率才是真省钱
把思路调转过来之后,我做了这么几件事:给 Agent 的输出加上强制校验;给工具调用包上超时和熔断;给 Agent 建立短期和长期记忆;每次失败都带回退预案。这四件事做完,这个项目里的 Agent 单任务平均 token 消耗其实没怎么降,但整体账单降了——因为不需要反复重跑了,也不需要每次出问题都人工检查了。
打个比方。压缩 Prompt 相当于让你每次开车少踩点油门,但路况还是那个路况,该堵还是堵。降低失败率相当于把路况本身修好,该通的都通了。省钱这件事,前者是省油,后者是少走冤枉路。对 Agent 来说,少走冤枉路的空间远大于省油的空间。
我后面讲的实操方法,都是围绕这个核心思路展开的:不要把宝押在“模型这次一定听话”上,而是把所有可能出问题的环节都兜住。
2. 让 Agent 少踩坑的三条核心设计原则
2.1 原则一:输出直接结构化,别让模型“自由发挥”
Agent 和普通单轮问答最大的区别在于:它的输出会被下一环节消费。你让模型返回一段自然语言,下一环节不管是写入数据库、调用工具还是展示给用户,都需要再解析一遍。解析这层就是最容易出错的环节:模型输出的格式稍微偏一点,比如少了一个逗号、多了一句解释,你的解析器就崩了。
我实践中比较推荐的做法是:从一开始就要求模型输出严格的 JSON 结构,并且在前端和后端同时约束字段。前端约束指的是在 Prompt 里给出明确的输出 JSON 示例;后端约束指的是在代码里用数据类或 JSON Schema 做校验。前后端一起上,才能把格式问题控制在可接受范围内。
这个原则的核心价值在于:把“模型输出”从“不可控的自由文本”变成“可控的结构化数据”。一旦输出是结构化的,你就可以在代码里对它做校验、做重试、做修正。如果模型输出一堆自然语言,这些工程手段全都用不上。
2.2 原则二:给 Agent 一套“安全网”,而不是赌它“这次一定能成功”
很多 Agent 的设计思路是线性的:拿到任务,调用模型,得到结果,继续下一步。中间任何一步失败,整个任务就断了。这种设计最大的问题在于,它把宝押在了“模型每一步都正确”这个假设上。但在真实业务里,模型就是会偶尔抽风,工具就是会偶尔超时。
安全网这个词听起来抽象,实际操作起来可以具体到这几件事。
第一,每步调用要有超时。不要让 Agent 在一个工具上无限等下去。第二,关键输出要有重试。模型返回不符合预期时,自动带错误信息重试一次。第三,失败要有降级路径。比如工具 A 拿不到数据时,是重试 B,还是直接标记为“需要人工介入”,必须提前定义。
我见过太多项目,Agent 一失败就整条链路挂掉,原因就是没设安全网。不是模型故意难为你,而是你不允许它有“喘口气”的机会。
2.3 原则三:用确定性代码来兜底不确定的模型
模型天然是不确定的——同样的输入,两次调用可能得到不一样的结果。你做工程化的目的,不是把模型变得确定,而是把确定性交给代码。凡是代码能判断的,就不要让模型判断。
举个例子:模型返回了一个日期字符串,可能是“2025-03-15”,也可能是“3月15号”,还可能是“March 15, 2025”。你不可能每次都让模型重新整理一遍,更不应该相信它一定能按你要求的格式返回。正确做法是用一段很小的代码去统一格式化。这就叫“确定性兜底”。
同理,模型返回的工具调用参数里,数字字段可能有空格、可能有单位,代码里可以清洗。列表字段可能被模型输出成字符串,代码里可以统一转换。这一层的兜底逻辑写好了,你会发现 Agent 的整体稳定性瞬间上一个台阶。
3. 实操思路一:用结构化输出和运行时校验拦住八成问题
3.1 先把输出协议定义清楚,再用代码校验
拿我最近在跑的一个“信息整理助手”来说,它的任务是读取一段活动通知文本,从中提取活动名称、时间、地点、主办方,然后写入内部表格。一开始我用自然语言 Prompt 让它“把信息列出来”,结果每次返回的格式都不同:有时是表格,有时是列表,有时是一段描述。后来我把输出格式改成严格 JSON,并且定义了字段类型和约束,才算把格式问题基本搞定。
这里我会用类似下面这种数据结构来定义输出协议。注意,这只是一个小例子,具体字段要根据你的业务场景来改。
from pydantic import BaseModel class ExtractResult(BaseModel): title: str # 活动名称 date: str | None # 日期,统一成 YYYY-MM-DD location: str | None organizer: str | None在运行时,我会先把模型返回的文本拿去做 JSON 解析。如果解析失败,我不会立刻给用户报错,而是做一次“修复式重试”:把模型原始输出、解析错误信息一起拼进重试的 Prompt,让模型自己修正。这样做的成功率其实不低,因为很多解析失败只是边缘小错误,比如多了一个尾逗号、少了一个大括号,模型一看错误提示就能改对。
但要注意一个度:重试最多二次,超过就了直接标记失败。不然模型会陷入“反复修,反复错”的循环,白白烧 token。
3.2 校验层的核心:从“信任模型”到“验证模型”
有些读者可能会想:我 Prompt 里明明写了“必须输出 JSON,字段必须包含 title,date 必须为字符串”,模型有时候还是会不听话。所以运行时校验这层不能省,它做的是“最后一公里”的把关。
校验这一层通常做三件事。
第一,类型检查。字段是否缺失,值是否为预期类型。拿“日期”字段来说,如果模型返回“下周三下午三点”,解析成字符串能通过类型检查——但这不是你想要的结构化日期,所以还得做格式检查。第二,约束检查。比如活动名称不能为空、日期必须匹配 ISO 格式、地点不能是“未知”。第三,业务规则检查。比如如果提取的日期早于今天,就提示重试,因为活动通知里的时间几乎不可能是过去的日期。
这三步走下来,基本能把模型输出的质量问题过滤掉一大半。剩下的极端情况,就交给重试和人工兜底。
这里有一个我踩过很多次的关键点:当你发现模型输出频繁不达标时,不要立刻去改 Prompt 里的措辞。先看日志,确定是缺字段、错格式还是业务语义不对,再有针对性地在 Prompt 里强调那一点,代码里补上对应校验。顺序反了,效果会很差——盲目改 Prompt,模型可能把正确的字段也改没了。
3.3 校验失败的兜底策略:降级、重试、人工干预
校验不是“非黑即白”的。当校验失败时,你有几种处理方式,按优先级排下来:
- 降级提取:如果模型没有输出全部字段,但你只需要其中一部分也能继续,那就只取有效字段,把缺失字段标记为“待确认”。2.定向重试:拿校验错误信息做提示,让模型只修复缺失或错误的字段,而不是整段重新生成。3.人工介入:前两步走不通,就把原始输出和校验错误一起推给人工处理。
这套兜底策略帮我解决了很多耗时问题。实际项目中我发现,只要设计好“哪一步能继续、哪一步必须停”,Agent 就不容易陷入死循环。你追求的不该是“100% 成功”,而是“该成功的能成功,不该浪费的时间不浪费”。
4. 实操思路二:函数调用的可靠性设计
4.1 超时和熔断:不让 Agent 无限等下去
Agent 调用外部工具函数,是踩坑高发区。最典型的问题是:工具函数内部调用了一个第三方 API,而 API 响应很慢。如果代码里没有超时控制,Agent 就会被一只“死掉”的请求卡住。更麻烦的是,卡住之后模型会反复重试同一个工具,形成一个慢请求队列,把整个链路拖死。
我的做法是给每个工具调用包一层统一的超时控制。Python 里用asyncio.wait_for或者concurrent.futures都能实现。超时时间不宜太长,一般根据工具类型设 8 到 15 秒。如果超时,工具函数返回固定的超时错误标记,Agent 收到之后走降级分支。
超过一定失败次数之后,还要熔断:同一工具连续失败三次,就不再调用它,直接标记为“工具不可用”。这个熔断状态可以维持 60 秒,然后再放开一次试探。这个逻辑跟服务降级是一个思路,但放在 Agent 场景里同样有效,防止模型在坏工具上反复“较劲”。
4.2 参数类型校验与自动修正
工具函数的第二个坑是参数类型。模型生成工具调用参数时,本质上是从文本里生成几个键值对,键值对的类型完全靠模型猜。模型可能把数字字段输出成带逗号的字符串,比如“1,234”,也可能把布尔值输出成 “是/否”。如果工具函数直接拿这些参数去执行,报错就来了。
处理方式分两步。第一,在工具函数入口做严格的类型清洗,统一处理常见脏数据。第二,如果参数实在无法转换,就返回一个带错误码的结果,而不是直接让工具抛异常。给模型和代码一个可处理的返回值。
def parse_worker_count(value): if isinstance(value, int): return value if isinstance(value, float): return int(value) if isinstance(value, str): # 去掉逗号、空格、单位后缀 cleaned = value.replace(",", "").replace("人", "").strip() return int(cleaned) raise ValueError(f"无法解析的参数: {value}")这段代码看起来简单,但它能拦住大量“模型说它有,但格式不对”的情况。很多时候工具调用失败,不是模型逻辑错了,而是它的参数“长得不规范”。一个自动清洗层,能让成功率升一大截。
4.3 幂等性设计:重试不等于重复干活
重试机制容易引入另一个问题:如果第一次调用工具其实已经成功执行了,只是响应超时或者没收到回包,你再次调用同一个工具,会重复干活。比如“发送邮件”这个工具,重试一次就多发出一封重复邮件,这在真实业务里是不可接受的。
解决思路是给工具调用加上幂等键。每次请求生成一个唯一的request_id,同一个request_id到达工具函数时,如果之前已经执行过,直接返回上次的结果。实现上可以用一个字典或者外部缓存做记录,按request_id存结果。
executed_results = {} def idempotent_call(request_id, func, *args, **kwargs): if request_id in executed_results: return executed_results[request_id] result = func(*args, **kwargs) executed_results[request_id] = result return result这个设计在“查询类”工具上可能用不上,但在“写入类”工具上非常关键。Agent 重试得越多,幂等设计带来的收益就越明显。很多 Agent 项目的脏数据,都是重试导致的重复执行。能把这层补上,数据质量会立刻改善。
5. 实操思路三:记忆与缓存,让 Agent 不重复“踩坑”
5.1 短期工作记忆:把已经确认的信息存起来
Agent 在多轮任务里经常犯一个低级错误:前面已经提取过的信息,后面对话里又让模型重新提取一遍。这既浪费 token,又增加出错概率。解决办法是在 Agent 内部维护一个短期工作记忆对象,保存任务执行过程中已经确认的中间结果。后续步骤如果需要这些信息,直接读记忆,而不是再问模型。
这个短期记忆可以用一个简单的数据类实现:字段名、值、来源、置信度。每次模型输出经过校验之后,就把有效字段写入记忆。后续步骤要用某个字段时,代码优先从记忆里取值。只有当记忆里没有时,才去调用模型。
这个做法的好处不只是省 token,更关键的是稳定。模型在多轮对话里可能会前后矛盾,但记忆是代码维护的,不会因为上下文漂移而丢失信息。相当于给 Agent 装了一块“不会忘事的记事板”。
5.2 长期经验记忆:沉淀失败教训
短期记忆解决单次任务的连贯性。长期记忆解决跨任务的教训沉淀。我在这个项目里做了一个很简单的失败模式记录:每次某个环节校验失败,我就把失败原因和最终处理方式存到一张表里。之后再做同类任务时,Agent 会先看一眼这张表,知道上次在哪类坑里栽过,就会避开。
比如有一段时间,模型经常把“主办方”字段提取成“联系人”,因为原文里主办方那段话里出现了人名的可能性很高。长期记忆记下这个失败模式后,在后续任务里,如果发现“主办方”字段疑似是“人名”,校验层会提示“该值疑似联系人而非主办方,请核对”。这个功能不是靠模型具备的,是靠工程手段沉淀的。
长期记忆需要注意“容量控制”:只记录高频失败模式,不要把所有小问题都记下来,否则 Agent 会被噪声干扰。我是按失败频率排序,只保留前 N 条作为提示词参考。
5.3 缓存策略:哪些调用值得缓存,哪些不值得
缓存不是银弹,用错了地方反而会带来脏数据。我在项目里只对两类调用做缓存:一类是只读、时效性不敏感的“查询类工具”,比如查某个城市的天气;另一类是 Prompt 里经常出现的长文本模板,比如工具说明。这两类缓存能显著降低 token 消耗,而且不会影响正确性。
不做缓存的是那些结果会变化的调用,比如“实时库存查询”。这类调用一旦缓存,Agent 拿到的是过期数据,后续决策全错。我见过不少团队在这上面吃过亏:为了省 token 给实时接口加了缓存,结果 Agent 基于旧数据做了一堆错误判断,后面再花好几轮去修正。
缓存的设计还有一个细节:要给缓存键加上业务维度,比如“城市 + 日期”,避免不同任务之间串数据。缓存命中率要能通过日志观测,太低就说明键设计得不对,该调整了。
6. 常见问题与排查技巧实录
6.1 模型输出 JSON 格式不稳定
这是最容易遇到的问题。模型在长上下文里,特别是前面已经有很多轮对话后,很容易忘记输出格式约束,开始自由发挥。我排查时先看是哪一轮开始走形的:如果是靠后的轮次,通常是因为 Prompt 里的格式要求被前面的内容“冲淡”了,解决办法是在关键轮次前重复强调输出格式,而不是只在系统 Prompt 里写一次。
另外可以改用一个更稳的技巧:让模型先输出一个短的 JSON 对象,再分步补全字段。这样每次输出的结构简单,模型跟随得更好,校验也更方便。对比下来,输出稳定性和 token 成本之间,其实可以找到一个不错的平衡点。
6.2 Agent 陷入重试死循环
表现是日志里同一个工具被连续调用多次、每次都失败,然后模型还在继续尝试。原因通常是重试逻辑设计得不合理——失败分支只是一个“换一种说法再试”,但没有判断错误的类型。当错误是可预见的(比如参数非法)时,重试多少次都没用。
我的排查方法是:先给错误分类。超时、参数错、业务规则错,三类错误的处理方式完全不一样。超时走降级,参数错走修正,业务规则错走人工。分类之后,重试循环自然会减少。另外我会给重试次数设置上限,比如最大二次,超过直接抛给人工流程。宁可人来看一眼,也不要让机器在一个坑里反复转。
6.3 工具参数类型错误导致的“隐性问题”
有时候工具函数本身会做类型转换,所以报错不明显,但结果却是错的。比如模型传了一个字符串 “2,400”,代码神器自动转成 2400,但如果模型指的是 2.4 万人,这个数字就错了。这种问题很难排查,因为代码没报错,只是数值不对。
我的经验是:不只是在工具层做清洗,同时要在 Prompt 里给字段加上明确的单位说明,并在校验层把单位也纳入检查。比如活动人数,模型必须返回“人数数值 + 单位”,代码里统一换算成标准单位后再存。这样能避免很多“类型对但含义错”的隐蔽问题。
6.4 环境不一致导致的“本地能跑,线上稳挂”
Agent 项目中本地测试通过、线上频繁失败的情况也很常见。差别通常在依赖版本和外部接口稳定性上。比如本地用的是某个第三方库的新版本,线上还是旧版本,解析行为不一样,导致输出校验失败。
我的建议是:把依赖版本锁定到精确版本号,并在部署时做一次“可复现性检查”。更关键的是,线上环境要打详细的调用日志——包括模型原始输出、工具函数入参出参、校验结果。没有日志,线上出了问题就只能靠猜。
6.5 排查问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 输出 JSON 解析失败 | Prompt 约束被上下文冲淡 | 在关键步骤前重复输出格式要求 |
| 工具重复调用 | 缺少幂等设计 | 增加 request_id 幂等键 |
| 工具调用超时 | 外部接口慢 | 加超时和熔断,失败走降级 |
| 数据数值错误 | 参数类型清洗不到位 | 在清洗层检查单位与含义 |
| 本地正常线上失败 | 依赖版本或环境不同 | 锁版本,远程日志对比 |
顺手分享一个我最近用的省钱小技巧
前面讲了很多大方向,最后说一个很实用的小技巧。如果 Agent 内部有多个工具,不要在每轮对话里把所有工具的说明书都塞进 Prompt。很多主流框架支持“仅把当前可用的工具描述发给模型”,这一步能省掉大量输入 token。配合前面做的工具超时和参数校验,即使模型只看到了少量工具描述,也不容易选错工具。
我在实际项目里测过:只发送当前步骤可用的工具描述,输入 token 直接降了 40% 左右,而且工具选择准确率没有下降。这说明省 token 和稳定性的矛盾,在大多数场景下是可以同时解决掉的——前提是你先把校验、重试和兜底做扎实。如果没有这些兜底,单纯省 token,最终总会从其他环节加倍花出去。