1. 先把Token账单算明白,再谈省钱
很多人一听到“节约Token”就急着找Prompt压缩技巧,但我觉得第一步不是想技巧,而是先把计费逻辑摸清楚。Token就是模型处理文本的最小单位,中文一个汉字可能对应1到2个Token,英文则按词根切分。调用LLM时,任何输入进来的系统提示、历史对话、用户问题,以及模型吐出来的每一个字,全部都要折算成Token,按量计费。
这个成本结构跟平时买东西不一样,它是“你还没拿到结果就开始扣钱”的。哪怕模型一句话都没回复,只要请求发出去,输入的Token就已产生费用;而且绝大多数平台对输出Token的单价要比输入更贵,有些甚至是几倍的差距。也就是说,光盯着“让回复变短”还不够,输入那边才是大头,尤其多轮对话场景里,历史消息会反复累计。
我接触过不少项目,一开始觉得调用量不大,结果月末账单出来直接傻眼。后来帮某团队做过一次成本盘点,发现一个非常典型的浪费模式:系统提示词写了上千字,每轮都带上;历史对话从来不裁剪,聊到十几轮直接变成几万Token的上下文;工具函数的说明书比代码本身还要长。这一套组合拳下来,一次调用消耗的Token量,几乎是业务原来预估的五倍以上。
所以如果你问我节约Token从哪里开始,我会先回答:先拿出一天的真实日志,统计每一类请求的平均输入Token、输出Token、单次成本。没有这个数据,后面所有的优化都是拍脑袋。常用平台的控制台一般都能查token用量,没有的话就自己埋点,在请求前后分别记录usage字段并入库。等数据攒上几天,哪些业务流程是吞金兽,一眼就能看出来。
有一个基本原则我建议刻在脑子里:调用LLM之前,先问自己三个问题——这个问题是否真的需要LLM参与?哪些上下文模型必须要知道,哪些只是我顺手塞进去的?输出内容需要多精细,能不能接受更短的回复?这三个问题想清楚,节流的思路会自然浮现出来。
2. 输入侧先动手:Prompt瘦身与结构化改造
2.1 一句一句删,把Prompt当成欠了债的文案
Prompt占的是输入Token,而且每调用一次就花一次,比输出Token更容易失控。你可以做一个“减重练习”:把现有Prompt逐句过一遍,凡是不影响语义的字全部删掉。我见过很多Prompt开头写“你好,你是一个智能助手,你的任务是帮助用户解决各种问题,你拥有以下能力……”,这一串其实对模型理解任务几乎没有额外帮助,属于典型的冗余。
真正有意义的Prompt,只需要包含三部分:角色约束(可选)、任务说明、输入数据与输出格式。注意这里的“角色约束”也不是越长越好。你完全没必要给模型打造一个几百字的小传,只需一两句话说明“你是客服助手,回答要简洁、直接、使用简体中文”,效果和长篇大论基本一致。
另一类冗余是客套语。比如“请您根据以下内容进行回答,非常感谢”这类话,模型既不会因此更卖力,也不会因此更友善,删掉之后输出质量不会有任何变化。还有“如果可以的话”“如果你知道的话”这种模糊填充词,纯粹是在给Token账单加码。
我习惯做一个最小化测试:先把Prompt砍掉50%,用同样的测试集跑一遍,看效果是否可接受;如果效果还行,再继续砍到30%-40%。实际测下来,很多业务Prompt在压缩一半之后,模型回答质量不降反升,因为干扰信息少了,注意力更集中。
2.2 固定模板加变量插槽,比临时拼字符串强得多
有些团队让业务方通过接口直接传一段自由文本,后台再拼上系统提示就发给模型,看起来方便,实际上埋了好几个坑。自由文本里很容易混入格式混乱的描述、重复信息,甚至会让Prompt的公共部分无法复用。更好的做法是做一个固定的模板,只有必要的位置留出变量插槽。
举个例子,我在某个客服项目里用的是这种模板:
prompt_template = ( "你是{bot_role}。\n" "用户提问:{user_question}\n" "已知订单状态:{order_status}\n" "历史处理结论:{history_conclusion}\n" "请直接输出处理建议,不要复述问题,控制在80字内。" )这样做的意义不只是省事。固定模板意味着每次请求的“公共前缀”是稳定的,很多模型服务商对相同前缀的请求有自动缓存机制,命中之后前面的Token费用会大幅降低甚至免费重算。如果你用字典动态拼接,每次开头都不同,缓存很难生效,白白多花钱。
另外,模板中能不放的信息坚决不放。比如“当前系统时间”如果业务用不上就别加;“用户所在地区”除非用于定制回答否则也别塞进去。总之原则就一句话:只有对生成结果有影响的变量,才值得随请求一起发出去。
2.3 少样本示例要精挑,能用一个就不用两个
少样本提示(few-shot)确实能提升输出稳定性,但代价是每个示例都会变成输入Token,永远存在。很多人在Prompt里塞五六个示例,生怕模型看不懂,结果五六个示例就是大几百Token,比任务本身还贵。
这里有一个经验:如果任务语义足够明确,比如“把用户输入转成JSON格式”,零示例就够了;如果任务需要特定输出风格,给一个典型示例即可;只有当模型的输出经常走偏、且规则难以用文字描述时,才给两个示例,但每个示例要刻意选择边界情况,让信息密度最大化。比如你想让模型学会“对用户情绪进行三级分类”,与其给三个普通例子,不如给一个“平静”“一个愤怒”“一个夹杂幽默的讽刺”,让模型从两个例子中掌握更多边界信号。
2.4 动词标准化:让Prompt句式一致、减少歧义
这个细节容易被忽略。同一个意图,你这次写“请提取”,下次写“帮我解析”,再下次写“分析一下”,模型会花费额外的“理解成本”,表现在输出上就是偶尔多出来的一句反问或补全。最佳实践是针对固定任务,使用固定祈使句:提取、判断、生成、改写、翻译。让Prompt呈现出“指令+对象+约束”的统一格式,模型每轮要处理的内容更少,输出也更稳。
3. 输出侧别浪费:响应裁剪与输出约束
3.1 max_tokens并不是设置得越大越好
使用API时,max_tokens是控制输出上限的参数,但它不是“最好留很大,防止截断”。只要你设置了较大的值,模型就会“默认”你允许它写这么多,于是很多模型会尽量把内容填充到这个容量的六七成;你如果不设置,服务商又可能按平台默认值放得特别宽。所以节省Token的第一步,就是给每个任务设置合理的输出上限。
比如一个“判断用户意图”的任务,输出只需要一个词:下单、咨询、投诉。那么max_tokens设10就够了,模型再啰嗦也会在10个Token内被截断。一个“文案改写”的任务,如果要求50字以内,max_tokens设200已经非常富裕,因为50字的中文最多占100多Token。
注意一点:max_tokens不是想设多小就多小。太小会导致内容被硬截断,尤其生成JSON时很容易截出残缺结构,下游解析直接报错。所以这个参数需要通过实际测试找到“安全余量”:先设一个小值,看是否满足业务,再往下压,直到接近极限。通常我会取预期输出Token数乘以1.5倍作为安全值,既能防截断,又不会给模型“长篇大论”的空间。
3.2 用结构化输出管住格式,模型就没机会说废话
很多时候模型输出的“废话”,根源在于你只给了文字要求,没强制结构。现在的API基本都支持JSON模式或者结构化输出,只要在请求参数中开启,并给定一个JSON Schema,模型就会严格按结构返回,不会再出现“好的,我来帮你分析一下:第一……”这种自白段落。
我在项目中更推荐直接给Schema的方案。比如要做信息抽取,我先定义好字段:
{ "type": "object", "properties": { "customer_name": {"type": "string"}, "order_id": {"type": "string"}, "issue_type": {"type": "string", "enum": ["物流", "退换货", "发票", "其他"]}, "sentiment": {"type": "string", "enum": ["负面", "中性", "正面"]} }, "required": ["customer_name", "order_id", "issue_type", "sentiment"] }模型输出就被约束成紧紧巴巴的一段JSON,几乎零废话。这个方案唯一的代价是Schema本身也会占输入Token,所以Schema里的字段注释能省则省,字段名本身足够表意就行。
3.3 重试越少,省得越多
遇到模型输出格式异常,很多人的第一反应是重新生成一次,代价直接翻倍。更划算的做法是“先解析、后重试”。比如要求JSON输出,先用解析器看看是不是标准JSON;如果只是多了个尾逗号或说明文字,我可能直接用后处理修正,而不是重新调用。如果确实需要重试,也要用截断后的错误信息去引导,而不是把上一轮完整输出原样丢回去。
还有一个小技巧:但凡希望输出稳定,temperature不要拉太高。默认值或偏低的值能让模型输出更容易落在预期分布里,减少因“创造力”产生的重复修正。控制输出质量,其实是控制重试成本,这才是最容易被忽略的省钱方式。
4. 多轮会话中,上下文就是隐形吞金兽
4.1 历史消息是怎么一步步吃钱的
先算一笔账。假设系统提示词固定500 Token,用户每轮提问平均80 Token,模型每轮回答平均300 Token。第一轮请求的总输入是“系统提示+用户第一问”,也就是580 Token左右,输出约300 Token。但到了第十轮,如果你把全部历史都带上,输入会变成“系统提示+前九个回合的所有内容和第十轮提问”,也就是500加9乘380再加80,约4000 Token,这还只是输入一次。如果每个问题都要带着这些历史重新推理,成本就是你看到的线性甚至更陡的增长。
更隐蔽的是,历史消息里包含了模型之前的输出。模型输出是按更贵的价格计费的,你再把它塞回下一次请求当输入,等于同一份内容付了两次钱。所以在多轮场景里,历史消息不是“免费的记忆”,而是“带利息的复利”。
4.2 滑动窗口只保留真正有用的回合
我建议第一档优化直接上滑动窗口,只保留最近N轮对话。具体保留多少,取决于业务。简单咨询场景保留3到5轮就足够;复杂分析场景适当放宽,但不要超过10轮。超出窗口的历史不再进入模型,而是进入“摘要层”或干脆丢弃。
代码实现不复杂,每次构造消息列表时先截断:
def build_messages(system_prompt, history, max_rounds=5): recent = history[-max_rounds * 2:] # 每轮含一条user和一条assistant messages = [{"role": "system", "content": system_prompt}] messages.extend(recent) return messages如果业务真的需要长期记忆,我建议把历史事件抽取成结构化的“状态摘要”,而不是原样保存聊天记录。比如客服场景里,把“用户订单号、问题类型、客户的诉求、当前处理状态”整理成一段200字以内的摘要,和聊天原文相比,Token占用往往能减少80%以上。
4.3 让模型定期帮你压缩笔记
“摘要式代偿”是我特别推荐的一种方式:每聊完一段,就另起一个“快速请求”,让模型把这段对话压成一句结论,再把结论作为下一轮历史。这个额外的压缩请求也会消耗Token,但和保留全文相比,基本是九牛一毛。压一次的成本,换来后面几十轮的持续省钱。
举个例子,一段800 Token的对话原文,压缩后可能只剩100 Token,如果这段对话要被后续五个问题反复引用,省下的Token就是700乘以五次,也就是3500 Token。这样的小投入非常划算。实际执行时还可以把压缩请求的控制参数调低,模型不需要追求文采,只要准确转述关键事实即可。
4.4 能用微服务状态替代的,就别让模型记
很多项目在架构上有一个误区:本该由数据库记录的信息,让模型以对话文本形式“记住”。比如用户填了一个金额,系统不存数据库,而是依赖模型从上一轮输出里提取。下一次使用时又把整段对话带上,让模型再次提炼。这在原型阶段可以理解,但正式上线就是双重烧钱。
正确做法是:业务状态全部落到结构化存储里,模型只负责“理解与生成”,不要让它承担“记忆”的职责。只有模型真正需要的少数关键字段,才作为变量写进Prompt。这个改动省下的Token,往往比任何Prompt技巧都多。
5. 工具调用与函数定义:别把说明书写得像论文
5.1 函数描述是Token黑洞
凡是让模型能够调用工具的API,都需要把函数列表、参数定义、描述信息一起发给模型。很多人给每个函数写一长串说明,还附带“历史背景、设计原因、注意事项”,全部折算成输入Token。我这段时间专门看过几个项目的工具定义,发现有些函数的description长达三四百字,而真正影响模型决策的只有“什么条件下调用它”“参数怎么填”,最多一百字就能说清。
精简函数描述有一个方向:从“解释这个函数是什么”改成“告诉模型什么时候用、怎么用”。模型不需要理解你的代码实现,只需要知道触发条件和传参规则。越接近“决策说明书”,描述越短,效果反而越好。
5.2 动态装工具,不要一次全塞
在复杂Agent场景里,一次请求带上二十个工具定义,Token随随便便就上2000。这其实很容易优化:先根据用户问题做一轮预判,只装与该任务相关的三到五个工具,其余不放入工具列表。预判可以用规则匹配,也可以用一个极小的分类模型,成本远低于大模型调用。
比如某客服系统有“查订单”“改地址”“申请退款”“转人工”“查物流”五个工具,用户问题里带关键词“物流”,那就只装载“查物流”工具。另外还预留一个“其他”兜底分类。这样工具描述总量从几百行压缩到几十行,单次请求省下的Token非常可观。
5.3 合并小函数,精简返厂次数
还有一个思路:把细粒度的函数“攒”成合并接口。比如系统原来有“获取用户名”“获取订单号”“获取商品列表”三个独立函数,每次调用前模型都要选一次,选错又重试。合并成一个“获取订单上下文”函数,一次性吐出所有信息,虽然这一次输出可能会长一点,但省去了整数次工具调用的开销,总成本往往更低。
合并函数还有附带好处:模型少做几轮“决策与拼接”,出错的概率下降,重试也随之减少。只从Token角度看,也是稳赚不赔。
6. 模型按任务分层:省钱的关键其实是模型路由
6.1 小任务用轻量模型,大任务才上旗舰模型
不是所有请求都需要最强模型。一个“判断用户消息是咨询还是投诉”的任务,用轻量模型就够了;“根据用户描述生成一份包含退款金额和操作步骤的解决方案”才值得请出旗舰模型。任务分级之后,成本结构会发生质的改变。
我的建议是建一个简单的路由层,按任务类型和输入长度分流:
def route_task(task_type, input_len): if task_type == "classification" and input_len < 500: return "cheap-model" if task_type == "extraction" and input_len < 1000: return "mid-model" return "flagship-model"这只是示意,具体阈值要按模型性价比、延迟要求来调。但核心思路是成立的:把高频、简单、输出短的请求统统扔给廉价模型,把低频、复杂、输出长的请求留给强模型。很多平台的小模型成本只有旗舰模型的十分之一,同样的业务量,模型分层之后账单能降一个数量级。
6.2 非LLM能解决的事,别硬叫LLM
有一类浪费最冤:本来用正则、字典、字符串替换就能完成的活儿,非要用Prompt调模型。比如从订单号中提取订单日期,正则三行搞定;判断用户输入是否匹配某个商品名称,精确匹配加别名表即可;统计一条消息里的数字个数,更不该让模型出手。这些任务放到LLM上,单一请求成本不算高,但如果是日请求量几十万的高频场景,纯属烧钱。
养成一个习惯:接到需求先问“有没有非模型方案”。规则方法维护成本高?那就用轻量分类模型而不是大语言模型。总之,LLM应该只处理真正需要语义理解、开放生成的任务,机械性任务一律走传统代码路径。
6.3 给路由层配一个“置信度回落”
模型分层之后,另一个问题是如何保证效果不降级。我会给轻量模型的输出做一个置信度判断,不确定时回落给旗舰模型。比如分类任务,如果轻量模型给出了概率很低的答案,或者解析结果落在未知类别,就自动升级到强模型。这个策略既照顾了成本,又不牺牲关键场景的效果。
7. 让缓存和RAG替你挡掉重复请求
7.1 完全相同的请求,直接复用上一次结果
业务里存在大量“相同或相似问题反复进来”的情况,比如同一个用户连续问三次“我的订单到哪了”,不同用户问“怎么退货”。如果每次都发一次完整Prompt,就是重复付三遍钱。最简单的方式是做一个请求级别缓存:以“任务类型+用户输入+关键变量”作为Key,查询结果直接返回给用户,压根不触发模型调用。这样既能降本,又能显著降低延迟,对用户体验也是正向提升。
需要做的只是设置一个合理的缓存失效时间,避免过期信息被复用。比如订单状态五分钟内可能变化,TTL设六十秒,过期后重新查询。这种动态业务场景下,缓存命中率会打折,但仍然是划算的。
7.2 语义缓存:相似问题不重复生成
请求级别缓存只对完全相同的问题有效。进一步可以上语义缓存:先把用户输入向量化,和已有请求池做相似度检索,相似度超过阈值就复用历史答案,用一套新的文案模板做微调(其实也可以完全复用)。这在客服FAQ类业务里非常有效,可能命中率能到百分之三四十。
唯一要留意的是语义缓存的“度”。如果两个问题字面不同但答案确实一致,缓存就赚了;如果字面相近但实际业务状态不同,就要小心误复用。为此可以把“业务状态快照”一并纳入缓存键,比如订单号、用户等级、当前阶段等,避免张冠李戴。
7.3 长文档别整本塞进Prompt
很多人的RAG流程是“检索到一堆相关片段,然后全部拼到Prompt里”。一个片段500字,拼上8个片段就是4000字,而且还包含了重复信息。正确的做法是对检索结果做重排序,过滤掉重复或低相关片段,每个Top切片控制在模型真正需要的范围内。
另外,RAG场景有个比较常见的坑:明明只需要某文档的第二章节内容,却把整个文档摘要都带上了。应该做切片级别的精细定位,把“与当前答案直接相关”的内容作为输入,其余一概不给模型看。
7.4 留意平台的自动缓存机制
现在不少模型平台支持对相同前缀的请求做自动缓存,命中部分不再重复计费或价格大幅折扣。这要求你在系统设计时尽量保持“前缀稳定”,比如把所有系统提示、固定指令放在消息队列前面,把变化的部分放在后面。这样一来,即使对话每轮不同,稳定前缀也更容易命中缓存。
与之配套的是尽量复用同一个Prompt模板,不要每一轮动态重写系统提示。哪怕只是系统提示里有毫秒级时间戳,都会打断缓存前缀,让省钱机制完全失效。这些细节不做日志比对,根本发现不了问题。
8. 我真实踩过的Token坑与省钱清单
8.1 最容易忽略的三类浪费
第一类是调试时随手打印大段上下文,排完bug没改回来,结果线上服务一直把完整调试内容发送给模型。这个我踩过一次,账单整整翻了一倍。建议给请求封装统一入口,在入口处做“必填字段过滤”,只放白名单变量进入Prompt。
第二类是“为了安全加限制”,比如系统提示里反复写“不要泄露隐私”“不要编造信息”“不要在回答中出现违法内容”,每一条都占据Token,而且重复出现并不会显著提升安全性。合规约束应当在产品层面通过参数与后置过滤实现,Prompt里只保留必要的一条。
第三类是日志系统本身。很多团队会把完整Prompt和完整输出原样打日志,日志服务按量计费,而且大数据平台还要对文本做清洗。这些成本不体现在API账单里,但同样属于运营成本。日志应只记录Token计数、请求时长、命中缓存与否等元数据,必要时才保留快照。
8.2 Token计数要按真实tokenizer来算
很多人拿中文字符数当Token数,误差极大。同一个模型家族内也有不同分词规则,最准确的办法是直接使用模型服务商提供的tokenizer包来做预计算。在设计Prompt模板时,可以顺手写一个小脚本自动统计模板Token数,提交到代码库之前顺手看一下变化,省到就是赚到。
8.3 上线前的Token压测比想象中重要
我建议在项目准备上线前,跑一套“黄金问题集”,把每个问题的Prompt Tokens、Completion Tokens、总费用全部打印出来,算一次单用户完整对话的平均成本,再乘上预期DAU和平均轮数,得到月预估账单。如果这个金额超过预期,就在上线前回头调整Prompt、窗口和模型路由。等真上线再发现成本爆炸,改起来就非常被动。
8.4 一个省钱效果的对比实验
我们在某个模拟项目X里做过一组对比,效果非常直观。优化前的流程是:完整系统提示加全量历史消息加全套工具定义,一次普通问答平均消耗2600 Token。优化后:精简系统提示到100字、对话历史只留最近三轮、工具按需动态装载、输出开JSON模式并设max_tokens,同样的问答平均只消耗600 Token左右。业务效果没有明显下降,单次成本降低约七成。
这组数据说明一件事:Token优化不是“抠唆”式的极限压缩,而是把正确的信息放在正确的位置,把无关信息挡在请求之外。做到这个程度,成本和效果是可以兼得的。
最后再分享一个小技巧:定期把线上请求抽样出来做“Token审计”,重点关注那些Token消耗量异常大的Top会话,看看是哪一层的设计在吃钱。很多优化点不是一开始就能发现的,只有持续盯数据、持续调整,Token账单才会越来越贴近你的业务真实需求。