☰
Agent Harness工作流Token成本优化:降本50%的实战方案
2026/10/8 10:06:00 网站建设 项目流程

上个月对账的时候,我盯着账单愣了好一会儿。我们那套跑在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, window=10): 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账单,还有整套工作流的可维护性和稳定性。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询