
这段时间后台经常有人问我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字段必须包含 titledate 必须为字符串”模型有时候还是会不听话。所以运行时校验这层不能省它做的是“最后一公里”的把关。校验这一层通常做三件事。第一类型检查。字段是否缺失值是否为预期类型。拿“日期”字段来说如果模型返回“下周三下午三点”解析成字符串能通过类型检查——但这不是你想要的结构化日期所以还得做格式检查。第二约束检查。比如活动名称不能为空、日期必须匹配 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最终总会从其他环节加倍花出去。