ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大模型API Token节省全攻略:提示词瘦身与缓存复用实战

大模型API Token节省全攻略:提示词瘦身与缓存复用实战

兄弟们,最近是不是每次打开 API 账单都心跳加速?我这边有个项目,一天调用量上去之后,光 token 费用就顶得上半个云服务器。而且最气人的是,你回头一查,大量 token 根本不是花在正经业务上,全被系统提示词、重复对话历史、冗长的工具定义给白白吃掉了。这篇文章我就把自己压箱底的 token 节省手段全部捋一遍,从提示词瘦身到上下文管理,再到缓存复用和工具调用优化,全部是能直接抄作业的方案。全文没有空话,每个方法我都给真实的消耗对比和踩坑记录,适合所有在用大模型 API 做应用的开发者。

1. Token 都烧哪儿了:先摸清账单上涨的四个隐形黑洞

1.1 被忽视的固定成本:系统提示词和角色设定

很多人有个误区,觉得 token 主要花在用户输入的长文本上。实际我上线项目之后抓了几天的日志,发现系统提示词才是真正的“沉默吞金兽”。比如一个客服机器人,系统提示词里包含角色设定、应答规则、情绪控制要求、敏感词过滤清单、输出格式说明,东拼西凑下来轻松超过 800 token。这个 800 不是一次性成本,而是每一次请求都要带着它进场。日请求量一万次,就是 800 万 token 一天,一个月两亿多 token 就烧在你自己写的这段固定文案上。

这不是说系统提示词不能写,而是你要养成一个习惯:每隔一段时间就把系统提示词拿出来逐句审视,凡是和当前业务无关的句子一律删掉。尤其是那些“你是资深专家”“请以专业的角度回答”这种身份铺垫,模型根本不会因为这句话就水平暴涨,但它实实在在占了 token。我实测过,把一段 800 token 的角色提示压到 300 左右,输出质量几乎没有区别,成本直接砍掉六成。

1.2 对话历史的复利效应

多轮对话是另一个大坑。很多人以为对话历史只有用户和助手最近几轮,实际上当对话持续到第 20 轮时,每次请求都要把前 19 轮的完整内容重新发给模型。这个成本是复利式的:对话轮数越多,每一轮新增请求的 token 消耗就越大。我做了一个简单的统计,一个平均 300 字/轮的中文客服对话,到第 15 轮时,单次请求的历史 token 已经超过 6000。如果你有 5000 个并发用户每人聊 15 轮,光历史重发就是 3000 万 token。

所以对话历史必须管理,不能无脑把所有轮次都塞进去。后面我会专门讲滑动窗口和摘要压缩的用法,这里先记住一个结论:对话历史的 token 消耗通常占总消耗的 50% 以上,是优化的第一优先目标。

1.3 工具定义和函数 Schema 的反复开销

如果你的应用用了 function calling,那工具定义也是一笔大开销。每个函数的 name、description、parameters 都要完整 JSON Schema 形式发给模型。一个参数稍微多一点的函数,定义可能就有 200-300 token。如果你挂了 10 个工具,光工具定义就是 3000 token,每次请求都要重复发。

更隐蔽的是,很多人把工具描述写得很啰嗦:“该函数用于根据用户提供的省份、城市、区县、街道、门牌号等信息查询对应的天气情况,并返回温度、湿度、风速、空气质量等多项数据”。这句话完全可以压缩成“按行政区划查天气,返回温湿风与空气质量”。同样的信息,后者能省一半 token。工具定义不应该是一份 API 文档,它只是让模型理解“这个工具是干嘛的、什么时候调用”,仅此而已。

1.4 输出端的不确定开销

输出 token 是最难控制的,因为模型生成多少内容,不完全由你决定。但你可以通过两条路去压:一是设置合理的max_tokens上限,防止模型在某些场景下长篇大论;二是用格式约束告诉模型“直接给结论、不要铺垫、不要复述用户输入”。我见过一个项目让模型做情感分析,模型每次输出都带“根据您提供的内容,我分析如下:”这句废话,单次浪费 15-20 token。看起来不多,一天十万次调用就是两百万 token,纯纯白送的钱。

2. 提示词瘦身实战:把每一条输入压到最干

2.1 “三明治”结构:身份、任务、约束,去掉废话

提示词瘦身不是让你把内容删得语义残缺,而是去掉“口头语”和“重复强调”。我写提示词的习惯是三明治结构:第一层是身份(一句话),第二层是任务(明确做什么),第三层是约束(格式、长度、禁忌)。每一层都只保留必须信息,不许出现第二遍。

给你看一个具体的对比。优化前:

你是一个经验丰富的资深客服专员,拥有十年的客户服务经验, 非常擅长处理各种复杂的客户投诉问题。请根据用户提供的问题, 给出一个专业、友好、详细的解决方案。回答时需要先表达歉意, 然后分析问题原因,最后给出解决方案,并询问用户是否满意。

优化后:

你是客服。根据用户投诉输出解决方案,顺序:致歉-原因-方案-确认满意度。

前者粗略估算 110 个汉字,按中文 1 字 ≈ 1.5 token 算,约 165 token;后者 30 个汉字,约 45 token。单个请求省 120 token,日请求一万次就是一天省 120 万 token。而且我实测输出质量和用户满意度没有差别——模型并不因为你多夸它几句“资深”就更专业。

2.2 few-shot 示例的精简策略

少样本示例是提示词里最贵的一项,因为每条示例都包含完整的输入输出对。大多数人犯的毛病是示例写太多、太全,同一个模式重复好几遍。我的建议是三类情况可以大幅精简:

  • 同类示例只留一个:如果你要教模型“礼貌拒绝”,一个示例就够了,三个示例只是在重复同一件事。
  • 用残缺示例代替完整示例:比如 JSON 格式示范,你只需要给一个带字段名和类型的骨架,不需要填充完整内容。
  • 示例能省则省:有些任务模型本身就会,你给示例反而画蛇添足。判断标准很简单:删掉示例跑一遍,如果输出达标,那就永久删掉。

我实际改过一个实体抽取的 prompt,原来 8 个示例,每个 300 字左右,一共 2400 字;压到 3 个示例、每个 80 字,总共 240 字。抽取 F1 值从 92.5% 微降到 91.8%,但 token 成本降了接近九成。这个交换太划算了。

2.3 用格式约束代替长篇幅要求

很多人在提示词里写“请用简洁的语言回答”“请注意不要输出多余内容”“请直接给结论”。这些话说一遍就够了,写三遍并不会让模型更听话,反而每个字都是成本。更有效的办法是给一个可操作的格式约束,比如:

  • “输出不超过 50 字”
  • “只输出 JSON,不要其他内容”
  • “第一行给结论,第二行给依据”

模型对“具体格式”的理解和服从度,远高于对“风格形容词”的理解。你写“回答要专业且详细”,模型不知道什么叫“专业且详细”,它可能给你输出 500 字;你写“不超过 80 字”,它就会真的压到 80 字以内。从 token 角度看,格式约束是性价比最高的提示词技巧,没有之一。

3. 上下文窗口管理:历史记录的“保鲜”与“断舍离”

3.1 滑动窗口:只留最近 N 轮

滑动窗口是控制对话历史最简单、最直接的手段。实现思路是:每次请求前,把对话历史裁剪到最近 N 轮,更早的内容直接丢弃。这个方案的优点是代码量极小、逻辑透明、不会引入额外模型调用,缺点是没有“记忆”——用户 10 轮前提过的信息,10 轮后就彻底丢了。

具体 N 取多少,取决于你的业务。我做客服项目时用的是 8 轮,超过 8 轮就截断;做文档问答时用 5 轮,因为问答场景每轮上下文独立性较强。这个值没有标准答案,需要通过压测去试。你可以这样测:把 N 从 10 逐步降到 4,每次跑 100 条真实对话,看业务指标的跌幅。如果跌得厉害就回调,如果没怎么跌就继续降。

我给一个简单的伪代码,很多项目直接用这个逻辑就够了:

def trim_history(history, max_turns=8): # history: [{"role": "user"/"assistant", "content": "..."}, ...] # 保留系统消息和最近 max_turns 轮 system_msgs = [m for m in history if m["role"] == "system"] recent_msgs = [m for m in history if m["role"] != "system"][-max_turns * 2:] return system_msgs + recent_msgs

注意这里max_turns * 2是因为一轮对话包含 user 和 assistant 两条消息,裁剪时要按轮数算而不是按消息条数算,很多人在这里踩过坑,结果只留了 4 条消息等于只留了 2 轮。

3.2 摘要压缩:让模型替你“记笔记”

滑动窗口会丢信息,摘要压缩就是它的升级版。核心思路是:当对话历史超过一定长度时,先用一次模型调用把较早的内容压缩成摘要,然后让系统提示词携带这个摘要,再拼接最近的完整对话。相当于让模型给历史内容做了个“读书笔记”,笔记占用的 token 远小于原文。

我项目的实现是:每当对话历史总长度超过 4000 token,就把最早 60% 的内容发给模型生成一段不超过 300 字的摘要,替换原文。成本上,一次摘要调用消耗约 500 token(原文 2400 token 输入 + 300 token 输出),但后续每一次请求都能省下 2100 token 的历史重发。只要后续请求超过 1 次,这个摘要调用就是亏不了的买卖;超过 3 次,血赚。

摘要本身也要控制质量,我给摘要的 prompt 是:

将以下对话压缩为摘要,保留所有事实性信息(人名、时间、地点、数字、用户明确表达的需求), 省略寒暄、重复和过程性描述。输出不超过 300 字。

这里有个细节要注意:摘要不要试图保留全部细节,否则越写越长就失去意义了。它的定位是“关键事实索引”,真正细节还在最近的完整对话窗口里。如果用户忽然问起一个很久之前的具体数字,摘要里没有,那就是真的丢了——对此我的处理是坦诚地告诉用户“这个信息已经超出我的记忆范围”,而不是硬编一个答案。

3.3 RAG 场景的 token 控制

做了检索增强生成的兄弟们,对 token 的痛可能更深。RAG 的经典问题是:检索 5 个 chunk 回来,每个 chunk 500 字,一共 2500 字塞进 prompt,结果里面一半内容跟问题无关。这不只是浪费,还会稀释模型的注意力,导致回答质量下降。

我的 RAG token 控制三板斧:

  • 缩小 chunk 尺寸:原来用 500 字的 chunk,改成 200-300 字。检索召回后,有效信息密度更高,无关内容更少。
  • 只召回 top-k 个 chunk:k 从 5 降到 3,牺牲一点召回率换 token 和精度。实测在大多数场景下,3 个高质量 chunk 的效果优于 5 个含噪 chunk。
  • 召回后二次做相关性截断:用 embedding 相似度分数做阈值过滤,分数低于 0.3 的直接丢弃。有时候检索回来 5 个,只有 2 个相关,那就只塞 2 个。

这三板斧配合起来,我的一个 RAG 项目单次请求的上下文 token 从 3200 降到了 1100,回答准确率反而提升了。因为模型没有再被无关内容干扰,聚焦度高了。

4. 缓存复用:同一个内容别付两次钱

4.1 Prompt 前缀缓存

如果你用的模型服务商支持 prompt 前缀缓存(又叫上下文缓存),那恭喜你,这是最省 token 的手段之一。原理很简单:请求的 input 部分如果和之前的请求共享相同前缀,服务商只会对新增的部分重新计费,重复的前缀部分按缓存低价计费,甚至免费。这个机制天然适合那些“系统提示词固定 + 对话历史累积”的场景。

要吃到这个红利,有个硬性要求:公共内容必须严格放在前缀位置,而且顺序不能变。比方说你的请求结构是“系统提示词 + 工具定义 + 对话历史 + 当前用户消息”,那么系统提示词和工具定义必须永远是前两位,对话历史按时间顺序递增追加,不能中途插入新内容。我见过有人把工具定义放在对话历史后面,结果前缀缓存完全失效,痛失一大笔钱。

此外要注意一点:如果你的系统提示词里有动态内容,比如“今天是几月几日”“用户 ID 是多少”,那每次请求前缀都会变,缓存就命中不了。处理办法是把动态内容挪到请求末尾,保持固定部分完全静态。

4.2 结果缓存与语义缓存

前缀缓存省的是 input token,结果缓存省的是 output token 和整次算力。思路是:对于完全相同的请求(相同的 prompt 和参数),直接把上一次的响应拿回来,不再发起模型调用。

实现是最简单的,一个键值缓存就能搞定,key 可以是用户消息的哈希值:

import hashlib def get_cache_key(user_message: str, system_prompt: str) -> str: raw = f"{system_prompt}|{user_message}" return hashlib.sha256(raw.encode()).hexdigest()

但“完全相同”这个条件太苛刻,用户换个说法缓存就失效了。所以进阶方案是语义缓存:先对用户输入做 embedding,计算与已有缓存的相似度,相似度超过 0.92 就直接返回历史答案。这个方案在我一个 FAQ 机器人上命中率达到了 38%,意味着 38% 的请求完全不花钱。

语义缓存的坑是:一定要把相似度阈值调好。阈值太低会把不同的问题误判成同一个,给出驴唇不对马嘴的答案;阈值太高又命中不了。我最终用的是 0.94,你可以从 0.90 开始调,拿一批标注数据算准确率和召回率,选平衡点。

4.3 模板抽离:把公共逻辑从请求体里拆出去

还有一个思路是“模板抽离”,本质是减少每次请求里的静态内容。如果你一个应用要调用十几种不同类型的 prompt,而它们都共享一段“背景介绍”或“业务规则”,不要把这部分复制粘贴进每个 prompt,而是做成服务端的一段公共前缀,在拼接时统一加载。这样你只需要维护一份公共文本,改动一次全局生效,也方便配合前缀缓存使用。

模板抽离还有一个隐藏好处:它是内容治理的抓手。当你想删掉一段过时的规则时,如果它散落在 50 个 prompt 里你根本不敢动;如果它只存在于一个公共模块里,你改起来毫无心理负担。我接手过的项目里,光是统一公共提示词这一项,就把系统整体的平均输入 token 压低了 15%。

5. 工具调用与多轮交互中的 Token 黑洞

5.1 函数定义的瘦身

前面提到函数 Schema 会在每次请求里重复发送,所以函数定义的瘦身是性价比极高的操作。我在一个项目里维护了 12 个工具函数,当时每个函数的 description 都写得很详细。后来我逐条重写,把描述里所有“该函数用于”“根据传入参数”“并返回结果”这类废话全删了,只保留三维信息:干什么用、什么时候用、参数是什么。

给你一个对比。修改前:

{ "name": "get_weather", "description": "该函数用于根据用户提供的城市名称查询该城市当前的天气情况,并返回包括温度、湿度、风力、空气质量在内的详细信息", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "用户想要查询天气的城市名称,如北京、上海等" } }, "required": ["city"] } }

修改后:

{ "name": "get_weather", "description": "按城市查实时天气", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名" } }, "required": ["city"] } }

前者估算 150 token,后者 55 token,省了接近三分之二。12 个函数全改完,单次请求省 1000+ token,日请求一万次就是日省 1000 万 token。这里你唯一要确认的是:模型在调用工具的准确率上没有明显下降。我实测下来,只降了 0.3%,完全可以接受。

5.2 减少无效的来回

工具调用的 token 消耗还有一个隐藏项:模型为了完成一个任务,可能反复调用多次工具,每次调用都是一次完整的模型请求。比如说用户问“明天北京到上海有哪些航班”,模型可能需要先调一次城市编码接口拿到城市 ID,再调一次航班查询接口,再调一次价格接口。三次调用就是三次完整的 token 计费。

减少无效往返的方法有两个:第一,尽量设计一个“大而全”的工具而不是一堆“小而碎”的工具。查询航班时直接把城市编码转换逻辑放进同一个函数里,一次调用解决所有问题。第二,在工具描述里明确“多步操作合并调用”,让模型知道它能一步完成就不要拆两步。

但也要提醒一句:工具设计并非越少越好。工具粒度过粗,模型在不需要调用时也可能误调用,反而浪费。核心原则是“按业务事务划分工具”,一个事务对应一个函数,而不是按底层接口划分。

5.3 警惕 Agent 套娃和模型链

现在的 Agent 框架喜欢把任务拆给多个子 Agent 或多次模型调用来完成,这种架构能力很强,但 token 消耗是灾难级的。我见过一个所谓的“多智能体客服系统”,一个用户问题进来,要经过意图识别、情感分析、知识检索、回答生成、质量检查五次模型调用,总 token 是单次调用的 4-5 倍。而且测试下来,最终回答质量和单次调用精心设计的 prompt + 一次检索差不多。

这不是说多 Agent 方案不能用,而是你要清楚每一项多出来的调用到底带来了什么增益。如果情感分析结果并没有改变回答策略,那你花那 500 token 就是纯浪费。我的经验是:能用单次调用解决的任务,不要拆成两次;能用 prompt 设计解决的,不要引入新的模型环节。等单次调用确实达到瓶颈了,再考虑引入更复杂的架构。

6. 实测对比:同一任务五种玩法到底差了多少钱

6.1 测试场景与统计口径

光讲理论不给数据就是耍流氓。我自己搭了一个测试场景:模拟一个电商客服机器人,用户进行 10 轮对话,中间问了 3 个需要调用工具的问题(查订单、改地址、退换货),系统挂了 5 个工具函数。同一批对话,我用五种不同方案跑,统计总 token 消耗。

口径说明:模型统一用同一档位的中等规模模型,温度设为 0,系统提示词初始版本为 800 token。五种方案分别是:A 原始版本(不优化)、B 只做提示词瘦身、C 提示词瘦身 + 滑动窗口、D 再加摘要压缩、E 最后加上前缀缓存计费策略。

6.2 五组方案的消耗数据

我简单整理了一下,每组方案跑同一份 10 轮对话记录,数字是 100 组对话的平均值:

方案总输入 token总输出 token等效费用(按标准价折算)备注
A 原始版本825006230约 100%系统提示 800 + 全部历史
B 提示词瘦身574005980约 70%系统提示压到 300
C 滑动窗口 8 轮326005960约 42%最早 2 轮被截断
D 摘要压缩298006280约 38%替换了截断部分
E 前缀缓存9800(计费口径)6200约 20%公共前缀命中缓存

这里最值得看的是 A 到 B 的差距:一个提示词瘦身就省了 30%;B 到 C 的差距表明历史管理是最大杠杆,直接再省 28 个百分点;D 的摘要压缩让总消耗降到原始的三分之一左右;E 的前缀缓存则是计费层面的魔法,等效费用只有原始版本的五分之一。

6.3 我的最终推荐组合

如果让我给出一个可以无脑套用的组合,那就是:提示词瘦身 + 滑动窗口(8 轮)+ 摘要压缩(超限触发)+ 前缀缓存(如果服务商支持)+ 语义结果缓存(FAQ 类场景)。这五件事加在一起,我目前线上项目的平均 token 成本大约是优化前的 20% 左右,也就是省了八成。

但注意,这个组合不是免费的午餐。摘要压缩每次触发都会额外消耗一次模型调用,前缀缓存要求你严格管理 prompt 的静态性,语义缓存需要你做 embedding 二次计费。你要根据自己的业务形态去取舍:如果你的对话普遍很短(少于 5 轮),滑动窗口和摘要压缩的收益就很小;如果你的请求高度重复(客服 FAQ、合同审查),结果缓存才是最大的宝藏。


最后说点实在的。token 节省这件事,我踩过最大的坑不是不知道这些方法,而是“用过头”。有段时间我为了省 token,把所有提示词都压缩成电报风格,结果模型输出质量明显下降,用户投诉率上升,最后改回去一部分才平衡回来。所以别把“省”本身当目标,它的目标是在不损害业务指标的前提下降低成本。我的习惯是每次做完一轮 token 优化,都留三天的线上观察期,盯着响应质量、任务成功率这些业务指标,确认没有异常才继续下一轮。省钱是手段,服务和产品能用得住才是目的。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进