ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent Harness工作流Token成本优化:降本50%的实战方案

Agent Harness工作流Token成本优化:降本50%的实战方案 上个月对账的时候我盯着账单愣了好一会儿。我们那套跑在Agent Harness上的工作流Token消耗比三个月前翻了一倍但业务量只涨了不到两成。更扎心的是翻代码才发现大家几个月的迭代全在加新功能、调Prompt没有一个人认真算过每次请求到底烧了多少Token。于是我从账单入手做了一轮以成本为主题的专项改造最终把单次工作流的Token开销压低了接近50%而下游业务指标不仅没掉某些场景的准确率反而还升了一点。这篇把这次实践的方法、代码思路、以及踩过的坑一次性讲清楚给正在跑Agent工作流、被Token账单搞到头疼的工程同学一份可以直接照抄的优化清单。先说明一下我所说的Harness工作流是什么。在AI Agent工程里Harness可以理解为驾驭Agent的一层框架负责把LLM调用、工具调用、上下文管理和任务编排串在一起让Agent不至于漫无目的地发散。Prompt决定模型的人设Harness决定系统该怎么调度模型、该喂给模型什么内容、模型输出的东西怎么落到业务里。Token成本优化本质上就是在这层工作流里做三件事让输入更少、让输出更短、让调用更少。整个优化周期我花了大概两周没有改任何模型参数也不是靠换便宜模型实现的纯粹是工作流层面的工程优化。下面按思路一步步拆。1. Token账单为何失控先算清钱到底花在哪做优化之前我习惯先问一个问题钱到底去哪了没有消耗明细的优化都是靠感觉靠感觉的优化大概率踩空。我当时把Harness工作流的每次请求从入口到出口全部打点把Token消耗拆成四块输入上下文、LLM输出、工具返回内容、重试产生的二次计费。打点之后三个典型的吞金兽浮出水面而且它们都在一个非常容易被忽略的地方工作流的上下文管理模块。1.1 第一个吞金兽会话历史的全量回传我们最初的设计是典型的聊天机器人思路把整段会话历史塞给模型让它记住前面的对话。业务刚上线时这个方案确实简单但工作流越做越深节点越来越多之后问题就暴露了。一次完整的业务流转可能要经过5个节点每个节点下面的工具调用又会回传历史消息。假设每轮对话平均800个Token十轮之后光是历史消息就是8000 Token而真正与当前请求相关的有效内容可能只有几百Token。更夸张的是有些节点根本不需要看历史聊天记录它只需要一个订单号或是一个客户ID。但当时的代码把所有上下文原封不动往下传造成大量重复计费。我在打点日志里见过单次请求输入上下文超过3万Token的情况其中将近七成是历史消息的重复搬运。模型就像一个听了十分钟废话才听到重点的客服既慢又贵。1.2 第二个吞金兽工具返回内容不做剪枝第二个问题出在工具调用环节。当时我们的搜索引擎工具会返回完整的结果列表默认10条摘要每条摘要又是一整段网页文字。如果模型为了判断一个简单问题需要读取5万字的工具返回那输入成本直接飙升。更麻烦的是一些内部系统接口会在返回体里带上完整的JSON对象里面有一堆模型根本用不到的调试字段、时间戳和空值字段。我当时用了一个很笨的办法验证把一次真实请求的输入内容打印出来逐段标注哪些信息模型在当前任务里真正用到了。结果发现工具返回里大约60%的字段对模型完成当前步骤毫无帮助。这些字段没有人会在意但模型每次都要一视同仁地处理Token照付。1.3 第三个吞金兽失败重试导致的重复计费第三个吞金兽藏得更深是重试机制。工作流里某个环节依赖第三方接口而第三方接口偶尔超时或返回5xx。我们的第一版重试逻辑很粗暴无论哪个环节失败直接从工作流起点重跑每重跑一次前面所有步骤的Input和Output Token都要再付一遍钱。有一次上游服务抖动了大约20分钟我们的Token消耗直接把当天预算干穿了一半。这三类问题的共性很明显模型本身没有变贵是工作流把模型喂得太饱了。优化思路也因此确定下来让Harness在调度时更聪明而不是指望模型在同样的输入下自动省钱。所以我没动Prompt、没换模型而是把注意力集中在工作流的输入压缩、输出约束和调用次数收敛上。2. 降本的底层逻辑三层漏斗式优化把一个具体的成本问题抽象成模型事情就简单了。单次工作流的Token总成本可以化为一个公式总成本约等于每次调用的输入Token加输出Token再乘以调用次数。所以降本只有三条路减少调用次数、压缩每次调用的输入、控制每次调用的输出。我把它们整理成一个三层漏斗从收益最大的上层开始逐层做。2.1 第一层少调用让规则和路由先挡一道少调用听起来最直接但落地最考验业务理解。我的做法是给工作流入口加了一个路由层能靠规则判断的绝不调用LLM。比如判断一个请求是否属于已知业务类型、是否命中黑名单、是否需要走人工审核这些完全可以用几行代码完成。再比如某些批量任务以前是每条记录都启动一个完整的工作流后来改成先做一次轻量预分类把相似任务合并成一批共用一次上下文初始化。这一层操作起来比重构上下文难因为要深入理解业务流转逻辑但收益非常稳。我们当时用了一个很简单有效的判断标准这条LLM调用如果删掉业务结果会变吗不会变的直接改成规则。第一个版本删掉了大约15%的无效调用Token成本同步下降。2.2 第二层省输入让模型每次只看该看的东西省输入是性价比最高的一层也是我花时间最多的一层。核心动作是打破一个上下文走天下的思维惯性。Harness工作流里的每个节点应该只拿到它完成当前任务所必需的那部分上下文而不是把所有信息一股脑塞进去。具体来说我做了三类事情一是把会话历史改成滚动窗口加摘要的组合方式老消息不再全量回传二是给工具返回加上字段白名单只保留下游真正需要的字段三是对长文本做结构化提取把模型需要的信息沉淀成键值对或短句下次直接读结构化的状态而不必再读原始文本。这三件事会在后面两章详细展开。2.3 第三层省输出让模型用最短方式回答输出Token的控制经常被忽略因为大家默认模型回答得越详细越好。但在内部工作流里详细往往是浪费。我们的做法很简单直接凡是只需要机器读取结果的地方一律使用结构化返回。比如模型要判断客户是否同意续约不需要生成一段分析文字只要返回一个JSON对象里面包含decision字段和reason字段就够了。同时把max_tokens设置成与任务匹配的长度从根本上杜绝模型画蛇添足。一个实用的量化经验对于同样的判断任务自由文本输出平均每次消耗约350个Token改成结构化JSON输出后平均降到60到80个Token单次调用输出侧直接省了四分之三。而且结构化输出对下游解析更友好可靠性反而上升。2.4 先砍哪一刀不要平均用力我实际执行时的顺序是先做省输入因为它在我们的账单里占比最大改动也相对独立再做省输出因为改动集中在工作流的Prompt模板和模型参数上最后做少调用因为它涉及业务流程改造周期长需要多方配合。这个顺序可以理解为先捡最大块的肉对大模型API账单来说输入Token往往占据大头尤其是带工具调用的Agent工作流上下文会因为工具返回和中间结果迅速膨胀。先把输入压下来是整个漏斗里收益最明显的。3. 实战改造一上下文窗口瘦身把聊天记录变成工作摘要这一章是本次优化里贡献最大的一部分单独拿出来讲。前面提过我们的工作流在早期犯了一个典型错误把LLM当数据库靠堆上下文让它记住所有东西。为了把这个习惯改掉我引入了Harness的记忆管理层核心是三件套滚动窗口、摘要压缩、原子状态存储。3.1 问题复现上下文是怎么越滚越大的拿一个真实业务场景举例客户提交续约申请工作流需要依次完成资质核验、历史订单分析、续约方案生成、邮件发送确认四个节点。在优化之前每个节点收到的上下文都包含完整的会话历史也就是客户从一开始说的每一句话包括你好你们周末上班吗这样的寒暄。模型在第三个节点生成续约方案时不仅看到了前面几个节点的结论还看到了所有原始对话和中间报告。一次请求输入Token轻松破万而且随着对话轮次增加这个数字线性上涨。我在打点日志里给这种请求起了个外号负重跑。模型每走一个节点都要背着整个对话历史再读一遍明明是同一批信息却被反复计费。而真正影响决策的信息比如订单金额、续约周期、客户偏好其实只有几个字段。3.2 核心方案滚动窗口加摘要加原子状态记忆管理层的设计思路是这样的把上下文分成三段组装。第一段是工作日志摘要由系统对较早的对话轮次做一次轻量压缩生成一段200字以内的进展总结第二段是最近N轮对话保留最近十轮以内的原始消息保证模型能理解当下的语境第三段是原子状态字段把业务上关键的变量从对话里抽出来单独存成结构化数据比如订单编号、客户等级、当前阶段、待处理事项。每次调用LLM时Harness只组装这三段旧的原始消息不再回传。举个例子客户在第1到第20轮对话里逐步说明了公司规模、预算、期望续约时间这些信息在第一次被模型读到后工作流会把它抽取成company_size: 200人、budget_range: 30万到50万、expected_renewal: 明年3月这样的字段。等到第21轮需要生成方案时模型只需要看这三个字段加最近几轮对话不再需要重新阅读第3轮那段关于公司规模的原始描述。这套设计的本质是让工作流具备记忆而不是复读。3.3 代码实现思路消息结构加上裁剪触发器具体的实现我用伪代码来表达方便你直接套到自己项目里。第一步给消息对象加上元信息标记它是否可以被裁剪以及它属于哪个业务阶段。第二步在组装上下文前判断当前消息总量是否超过预设阈值。第三步如果超了就调用一次摘要LLM把最早的一部分消息压成摘要块。第四步把摘要块、最近N轮消息、原子状态字段按顺序组装成最终的Prompt。class Message: content: str source_node: str # 来源节点如 intake, analysis can_summarize: bool # 是否允许被摘要 created_at: datetime def build_context(messages, atomic_state, window10): recent [m for m in messages if not m.can_summarize or m.is_recent] if len(recent) window: old recent[:-window] recent recent[-window:] summary summarize_messages(old) # 调用摘要LLM else: summary get_summary_from_cache() # 已有摘要块则复用 return { summary_block: summary, recent_block: recent, atomic_state: atomic_state, }这里有一个细节摘要并不是每次都重新生成。我给摘要块加了一个缓存和版本号只在旧消息累积到一定量时才触发一次新摘要。否则每次组装上下文都去调摘要LLM压缩省下的Token又会被摘要过程吃掉得不偿失。实际操作中我按消息距离当前时间超过30分钟且条数超过20条来触发摘要这样同一个会话里摘要生成的频率很低。3.4 单这一项就省了约30%改造完成后我用同一批测试请求跑了效果对比。同样是十轮会话的续约场景优化前单次工作流输入Token约1.8万优化后降到约6000输入侧减少约65%。如果按整条工作流的平均消耗来算由于还有少部分请求本身很短、不需要触发摘要整体Token量下降了大约三成。最让我意外的是模型在关键任务上的表现没有退化因为它在每一步看到的信息更聚焦反而减少了被无关历史干扰的情况。副作用也要说清楚摘要压缩存在信息损失风险尤其是那些早期对话里的细节条件。为了对冲这个风险我把原子状态字段作为硬信息通道凡是业务校验需要用的关键变量都必须结构化抽取并存入状态库不允许只存在于摘要文本里。摘要只负责维护叙事连贯性关键事实走结构化存储两条通道互不干扰。4. 实战改造二结构化输出、工具剪枝与缓存复用上下文瘦身解决的是喂给模型太多的问题这一章解决模型产出太多和工具带回太多的问题。这三项改造叠加之后整体成本就开始往50%的方向走了。4.1 结构化输出省掉模型自言自语的空间内部工作流和面向用户聊天最大的区别是大多数节点不关心模型的文采只关心它能不能返回一个可以被程序解析、校验、落库的结果。所以我把所有内部节点的Prompt都改成了只输出JSON的格式并给出严格的字段约束。举个例子判断客户续约风险的节点以前会生成一段自然语言分析现在改成只输出一个JSON对象包含risk_level、key_risks、suggest_action三个字段。为了让模型不跑偏我会在系统提示里写明不要输出任何JSON以外的内容不要解释不要客套。同时把max_tokens从900调低到200。这个改动对短文本场景的效果立竿见影。需要注意结构化输出不是万能的。如果任务本身要求生成营销文案或总结报告强行压缩输出会牺牲效果。我的判断标准很简单输出的消费方是程序还是人。程序消费的输出一律结构化人消费的输出才保留自然语言。4.2 工具返回的字段白名单别让模型看它用不到的东西工具调用是Agent工作流另一个隐性成本重灾区。我之前在打点分析里发现大约60%的返回字段对决策没用所以这次直接给工具层加了一个返回映射器。每个工具在被定义时必须声明两个东西哪些字段必须返回给模型哪些字段只需要写入状态库但不回传。字段级白名单的配置让工具返回量降下去同时保持了必要信息完整。还有一类工具是搜索引擎或内容检索类。这类工具返回的是长文本列表白名单思路不太够用。我的处理办法是加一个聚合层先把原始结果做一个轻量级的提取只返回最相关的三条摘要每条摘要限制在100字以内。如果模型觉得信息不够它可以通过一个深度检索工具再去拿全文。这样设计之后搜索类节点的输入从平均8000 Token降到了1500 Token而且因为信息密度更集中模型选对答案的概率反而更高了。4.3 缓存复用同一件事别让模型算两遍就算输入输出都做了压缩如果同一个问题被反复问、同一个结果被反复生成成本依然压不住。我在工作流里加了一个语义结果缓存层它的逻辑很简单在业务层面定义缓存键比如客户ID方案类型版本号第一次生成结果时写入Redis后续相同键的请求直接命中缓存绕过LLM调用。初始版本踩了一个坑总是想把缓存键做得太通用比如直接用Prompt的字符串哈希。结果微小的Prompt措辞变化都会导致未命中命中率不到10%。后来我把缓存键收敛到业务维度并且额外记录atomic_state的版本号一旦关键字段发生变化就自动失效。命中率提升到45%左右有一部分高频业务的请求直接走了缓存连一次模型调用都没有。缓存命中时一定要记得打点节省了多少Token。我当时给缓存系统加了一个计数器每次命中都按估算的调用成本记账。这组数据非常有用后来向业务方汇报优化成果时光缓存帮我们省了X万Token这一条就足够有说服力。4.4 收益测算第二层改造又砍掉15%到20%所有改造上线后我做了整整一周的AB对比。优化前工作流单次请求的平均Token消耗基线是43000优化后平均值降到23000左右整体降幅约46.5%。如果拆开看上下文压缩贡献了约30%工具剪枝和结构化输出贡献了约12%缓存和路由贡献了约5%。这个比例在不同业务上会不一样但基本符合输入压缩是大头的规律。需要说明的是这些优化有一个前提业务目标不能被牺牲。我在整个改造期间维护了一条自动化测试集覆盖20个典型业务场景每周跑一遍对比回答质量和完成率。结果显示优化后测试集综合完成率从之前的91%小幅涨到了92.5%没有出现回退。原因还是那句话信息聚焦之后模型犯错的概率反而降低了。5. 踩坑实录优化过头之后质量回退与重试风暴任何优化做太狠都会翻车这一章记几个我们真实踩过的坑。如果你正在做类似的Harness工作流成本优化大概率也会遇到。5.1 坑一工具剪枝太狠导致模型信息不足还要二次补查第一次做工具剪枝时我为了让Token压到最低把搜索工具的返回结果裁剪成标题加链接正文全部去掉。结果跑了一天后发现很多请求的Token不仅没降反而涨了。原因很简单模型看到的信息太薄不足以做出判断于是触发了信息不足分支额外调用了一到两次深度检索工具。每一次补查都是新的完整上下文加完整输出整体成本反而更高。这个教训让我彻底明白了一个道理Token优化不能只盯着单次调用的数字要看整条工作流的全局成本。后来我给剪枝策略加了一个冗余系数核心信息字段不剪辅助信息字段才剪。模型本来就不是只看一个标题就能做判断的适当的正文摘要反而能让它一次说对避免后续连环调用。成本最低不是最健康的状态成本和质量平衡才是。5.2 坑二结构化输出的Schema写太大命中也跟着变低结构化输出有一个隐蔽的成本点就是Schema本身也要作为Prompt的一部分喂给模型。我第一次给一个复杂节点定义了很详细的JSON Schema每个字段都写了大段的描述、枚举值和示例结果发现模型并没有更听话反而因为字段解释太多而理解了偏差导致JSON解析失败引发了一次重试。输入侧多花了Schema的Token输出侧又多付了一轮重试的钱。调整方法很直接Schema里只保留字段名和简短描述枚举值尽量给具体的示例去掉所有装饰性文字。字段名的命名也要直白让模型看一眼就懂而不是靠解释硬凑。压缩后的Schema从约900 Token降到约280 Token且模型返回格式的稳定率明显上升。记住一句话模型不是读不懂复杂Schema而是复杂Schema给了它太多自由发挥的空间。5.3 坑三缓存TTL设置不当命中率和新鲜度的博弈缓存看起来很简单但TTL是门学问。第一次我把结果缓存设为5分钟因为担心数据过期结果大量相同请求依然在打模型命中率只有不到10%缓存形同虚设。后来我改成24小时命中率一下冲到70%但问题又来了客户的某些动态数据出现滞后比如客户已经修改了预算模型还在拿旧结果回复。用户马上察觉到了反馈说系统记性太好。最终方案是把数据按新鲜度分级存储。客户基本信息、产品目录这类弱变化数据TTL可以设到24小时甚至更长价格、库存、客户状态这类强变化数据TTL只能设5到10分钟或者干脆不缓存。同一套缓存框架按数据特性分级配置命中率稳定在45%到55%之间同时业务方反馈的过期问题基本消失。这个分级思路不只适用于Token优化任何Agent工作流里的缓存设计都该这么考量。5.4 坑四摘要叠加导致信息蒸发关键的细节找不回来了滚动窗口加摘要的做法虽然有效但我还遇到一个隐患越是早期的对话经过多轮摘要叠加后细节丢失得越厉害。有一次一个客户在首轮对话里提到了必须在下周之前完成部署这个条件在前两轮摘要里还在到了第三次摘要压缩时摘要模型觉得部署时间不如客户规模重要就把时间信息省略了。结果后续节点在生成方案时完全忽略了时间约束差点造成交付延误。修复方案就是之前提到的原子状态字段所有业务校验必须依赖的硬信息都不允许只存在于摘要文本中必须在首次出现时就被抽取成结构化字段存入状态库。我专门加了一个校验规则凡是出现在业务规则配置里的关键词比如金额、日期、名称、数量都必须走结构化存储。摘要块即使把信息丢了工作流也不会出问题因为它永远优先读原子状态字段。这套双通道设计最终让我们敢放心把原始消息丢掉。6. 降本50%后的数据复盘哪些手段最值钱整个优化项目告一段落后我整理了一张成本优化手段的复盘表这张表对后续新项目的预算评估很有帮助也方便你对比自己的场景。优化手段Token收益占比实施难度关键注意点上下文滚动窗口加摘要约30%中摘要不要频繁生成需配合原子状态防信息丢失工具返回字段白名单约8%低核心字段不能一刀切剪枝太狠会造成二次补查结构化输出约束约4%低Schema别写太大程序消费的输出才适合结构化缓存复用约5%中缓存键按业务维度定义TTL按数据新鲜度分级路由与规则前置拦截约2%高需要业务理解判断哪些调用可以改成规则6.1 效果评估质量没有降级反而更稳了我强调一下Token降了一半并不等于效果打对折。我们在优化前后各跑了一周线上数据用自动化测试集测算关键业务完成率。优化前的完成率基线是91%优化后是92.5%。具体到每个场景差异不大但趋势是正向的。这背后的原因比较朴素工作流的上下文更聚焦模型被无关信息带偏的概率降低工具返回的信息密度更高模型决策时的信噪比提升结构化输出减少了格式解析环节的稳定性问题下游程序消费更顺滑。6.2 后续还能继续挖的优化点降本50%之后我并没有认为这件事做完了还有几个方向可以继续挖。一是API侧的Prompt缓存把高频通用的系统提示词做服务端缓存进一步降低输入成本二是模型分层路由简单的分类任务交给更小、更便宜的模型只有复杂推理才走大模型三是批量请求合并多个短请求在业务允许的情况下合并成一次调用减少重复的头部开销。这些方向有的厂商已经支持有的需要基于业务约束自己设计但核心思路一致就是在Harness层做更聪明的调度。6.3 最后说点个人体会这次实践给我最大的启发是Token成本优化和传统后端性能优化很像。遇到账单暴涨第一反应不是急着压缩Prompt或换模型而是先打点、先拆解搞清楚每次调用的Token构成。数据会告诉你最大的浪费在哪里。第二是要建立每一次调用都要有明确业务产出的思维方式。模型很强但不需要它每次都展示全部能力Harness工作流存在的意义就是只让模型做它最擅长、又最有必要的那一步。如果让我给正在做类似工作的朋友一句建议那就是不要追求把某个单点压到极限而是追求整个工作流在成本和效果之间的平衡。先砍最大的头每改一步都跑一遍回归用数据和业务反馈做裁判。这样一轮下来省下的不只是50%的Token账单还有整套工作流的可维护性和稳定性。
返回列表