这个标题不是我起的夸张噱头,是我调 GLM-5.3-Flash 接口账单时真实发生的数字。上个月我接了一个知识库问答类的 Agent 服务,每天固定调用 GLM-5.3-Flash 大概 12 万次,月末拉账单一看,输入 token 花了 11400 元,占总费用九成。后来我翻了一遍 API 文档,在请求体里加了一个参数,第二个月同样量级的调用,费用从 12600 元掉到了不到 2000 元,换算过来正好是 6.3 倍的成本降幅。今天把整个排查、原理、实操过程和踩坑记录整理出来,给正在被大模型调用成本折磨的朋友一份可以直接照抄的作业。
这篇文章适合谁?如果你在跑 Agent 服务、知识库问答、批量文本分析,或者任何"固定提示词 + 动态用户输入"的大模型调用场景,这篇文章能帮你省下一大笔钱。就算你现在用的是别的模型,只要平台支持提示词缓存,思路完全可以平移。
1. 先算一笔账:GLM-5.3-Flash 的钱到底花在哪
1.1 一次调用账单是怎么构成的
大模型 API 的计费逻辑和传统云计算产品不一样,它不看调用时长,也不看请求次数,而是按 token 计费。一次完整的请求,账单上通常拆成三块:输入 token、缓存命中的输入 token、输出 token。
GLM-5.3-Flash 的定价结构也是这个套路:总费用等于未命中缓存的输入 token 乘以普通输入单价,加上命中缓存的输入 token 乘以命中单价,再加上输出 token 乘以输出单价。关键点在于,命中缓存的输入 token 单价,比普通输入 token 便宜了不止一个数量级。这个价格差,就是整篇文章所有优化动作的基础。
我最初对这件事没太上心,总觉得"输入 token 才几个钱,大头在输出"。实际跑了几天发现完全不是这样。我的服务是 24 小时全天候跑的,系统提示词固定有 1800 个 token 左右,里面塞了角色设定、知识库检索说明、工具调用的 JSON 结构定义,用户真正传过来的问题平均只有 100 个 token。这类应用有个共同特点:每次请求里输入内容的绝对量很大,但真正变化的部分极少。
1.2 我的项目里输入 token 为什么占了九成
把上个月的账单拉出来看,一天的总 token 消耗大约 2.52 亿,其中输入 token 有 2.28 亿,输出 token 只有 0.24 亿。按照 GLM-5.3-Flash 当时的价格计算,输入费用 11400 元,输出费用 1200 元,输入部分占总费用的 90% 以上。
这里面的逻辑很简单,输出 token 是一次性的,生成完就结束;而输入 token 里相当大一部分是恒定不变的系统提示词、few-shot 示例、工具定义。这些内容每次请求都会被完整发送给模型做处理,注意是"每一次",不管你问的是"今天天气怎么样"还是"明天呢",前面那一大段 1800 个 token 的通知提示词都会跟着重新计算一遍。
很多人看到热词里"同时访问一个页面传递参数不同,结果一样"这个描述会觉得眼熟,我在项目里也遇到过类似的困惑——请求参数明明在变,模型返回却总是模模糊糊地相似。后来排查才发现问题不在模型,而在 prompt 结构本身:静态部分太多,动态部分太少,模型被固定模板带偏了。这也侧面证明,固定前缀加动态后缀这种结构,在大模型应用里有多普遍。
1.3 很多人会把优化重点放错位置
我见过不少开发者优化成本时,第一反应是压缩输出长度,给模型加"尽量简短回答"的提示词,或者调低 max_tokens。这个方向没错,但对高频调用场景来说,往往是在小头上下功夫。
输出 token 确实单价高,但它总量小。真实账单里最肥的一块肉是输入 token,尤其是重复的输入 token。如果服务端的输入单价是 5 元/百万 token,你每天跑 2.28 亿输入 token,那就是 11400 元。这时候只要让其中九成以上走缓存命中通道,哪怕命中单价便宜到可以忽略,省下来的也是几千元一天的量级。
所以遇到成本问题,第一步绝对不是急着改业务逻辑,而是先看账单结构:输入占多少,输出占多少,有没有命中缓存的记录。账单会告诉你钱到底漏在哪里。
提示:如果你的请求结构也是"很长的固定前缀 + 很短的动态输入",那下面这个优化几乎一定适合你。
2. 加一个参数省下 6.3 倍,原理到底是什么
2.1 这不是超参数调优,是请求里的一个字段
先说清楚,标题里"加了 1 个参数",不是模型训练阶段的超参数,不是提示工程里让模型"多说一点"的系统指令,也不是什么复杂的参数优化算法。它就是一个普通的 API 请求参数,在调用 GLM-5.3-Flash 时加进请求体里,告诉服务端"请为我启用提示词缓存"。
我看到热词里有一堆"超参数""位置参数 python""main 函数参数"这类词,很容易让人误以为这是什么高级调参技巧。实际上,真正起作用的机制叫 Prompt Caching,中文叫提示词缓存。它不需要你改代码逻辑,不需要你重新训练模型,也不需要你调整任何模型内部权重,只改一个请求字段。
这个机制的工作原理,通俗点说就是服务端把你请求里的前缀内容做哈希存储,下一次请求到来时,如果前缀和之前完全一致,就直接复用之前算好的中间结果,不再把整段前缀重新算一遍。这就像浏览器缓存:你不会每次打开网页都重新从服务器下载所有图片和脚本,命中缓存的资源直接读本地就行,又快又省。
2.2 提示词缓存机制:一张表讲清楚
大模型推理时,Transformer 架构会对每个 token 计算 KV(Key-Value)中间状态,这个状态非常占显存和算力。如果每次请求都从零开始计算那段 1800 token 的系统提示词,哪怕用户问题只有几十个 token,计算量也一点不会少。
开启提示词缓存后,服务端会把固定前缀的 KV 结构保存一段时间。同一个用户或同一批请求再次使用相同前缀时,直接读取缓存,省掉这段计算。这个过程对上层调用方完全透明,你只需要在请求里开启对应参数。
GLM-5.3-Flash 开启缓存后的计费结构大概是这样,我用一个表格来展示我项目的真实数据:
| 计费项 | 优化前价格(元/百万 token) | 优化后价格(元/百万 token) |
|---|---|---|
| 普通输入 token(未命中) | 5 | 5 |
| 缓存命中输入 token | 不支持 | 0.025 |
| 输出 token | 5 | 5 |
注意这个 0.025 元/百万 token 的命中价格,是普通输入价格的 1/200。平台敢开这么低的价格,不是做慈善,而是因为命中缓存的 token 不再重复计算注意力机制,服务端的边际成本本来就极低。这个逻辑对所有做提示词缓存的大模型平台都成立,只是各家命中的折扣力度略有不同。
2.3 6.3 倍是怎么算出来的
这里必须把计算过程拆开,否则很多人会以为 6.3 倍是"缓存单价是普通单价的 6.3 倍便宜",那就理解偏了。实际是叠加了命中率之后,整体账单的降幅。
我项目的数据是这样的:
- 优化前:每天总费用 = 2.28 亿输入 token × 5 元/百万 + 0.24 亿输出 token × 5 元/百万 = 11400 + 1200 = 12600 元
- 优化后:假设整体输入命中率 93.5%,即 2.1318 亿 token 走命中通道,费用 = 2.1318 亿 × 0.025 元/百万 = 53.3 元;剩下 0.1482 亿 token 未命中,费用 = 0.1482 亿 × 5 元/百万 = 741 元;输出费用还是 1200 元
- 优化后合计 = 53.3 + 741 + 1200 = 1994.3 元
然后拿优化前费用除以优化后费用:12600 ÷ 1994.3 ≈ 6.3。
这就是 6.3 倍的全部来源。本质上是"命中单价拉到极低 + 固定前缀命中率足够高"两个条件叠加的结果。其实就算命中率只有 80%,总费用也会从 12600 元降到 3867 元左右,也有 3 倍出头的降幅,已经非常可观了。
3. 实操:给我的 GLM-5.3-Flash 调用加上这个参数
3.1 改动前后的代码对比
先说结论,代码改动量小到不可思议。我用的是智谱官方 Python SDK,改动前长这样:
from zhipuai import ZhipuAI client = ZhipuAI(api_key="your-api-key") resp = client.chat.completions.create( model="glm-5.3-flash", messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_query} ] )改动后就加了一行,具体是把参数放进extra_body里:
resp = client.chat.completions.create( model="glm-5.3-flash", messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_query} ], extra_body={"cache_prompt": True} # 就加了这一行 )如果你用的是 OpenAI 兼容接口,逻辑完全一样,把参数放在请求体顶层:
resp = client.chat.completions.create( model="glm-5.3-flash", messages=messages, extra_body={"cache_prompt": True} )用 curl 直接调的话就是:
curl https://open.bigmodel.cn/api/paas/v4/chat/completions \ -H "Authorization: Bearer $ZHIPU_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "glm-5.3-flash", "messages": [...], "cache_prompt": true }'我第一版上线时直接把cache_prompt: true写死,跑了 24 小时,账单效果立竿见影。这里有个容易踩的坑:不同 SDK 版本对扩展参数的处理方式不一样,有的把参数直接放在请求体顶层,有的必须用extra_body包一层。如果加的位置不对,服务端根本收不到这个参数,但也不会报错,账单自然没有变化。
3.2 参数值选型和触发边界
cache_prompt这个参数的值不是随便填的,不同平台给出的语义会有差异,使用前一定要确认你接的版本支持哪些取值。
我实测下来,GLM-5.3-Flash 的cache_prompt支持布尔值,true表示开启提示词缓存,false表示关闭。有些平台还支持字符串形式的自动模式,比如"auto",让服务端根据请求长度和内容自行判断是否走缓存。如果你不确定,建议先显式传true,观察响应里的usage字段。
另外,提示词缓存通常不是无条件的。我了解到的常见触发规则包括:
- 固定前缀 token 数需要达到一定长度。如果前缀太短,缓存意义不大,服务端可能直接忽略
- 缓存的 KV 结构有有效期,超过一定时间没复用会被清理。具体 TTL 各家不同,文档里一般会写
- 命中是按"前缀完全一致"来判定的,中间任何一个 token 变了,后面的缓存就断了
所以在设计系统提示词时,尽量把动态内容放在固定部分的后面。比如时间戳、随机数这类每次变化的字符串,如果插在 system prompt 中间,会导致整条前缀无法命中。
3.3 上线前必须验证的三件事
第一件事,确认请求体里参数真的生效。最快的办法是看响应里的usage字段。开启缓存后,响应里会多出类似prompt_tokens_details.cached_tokens的信息,里面会明确写本次请求命中了多少个 token。如果加了参数之后这个字段看不到,说明参数没传对。
第二件事,跑小流量对比。不要一次性把全部生产流量切过去,先挑 10% 的请求验证 2 到 3 天,看命中率和单次调用延时。缓存命中后,首 token 延迟一般会下降,因为省了前缀的计算时间,这个指标也能侧面验证缓存是否生效。
第三件事,把前后端两侧的数据都留下。我习惯把每次调用的usage完整打到日志里,后续做成本分析时非常有用。没有 usage 明细,你根本不知道命中率是多少,也没法算清楚省了多少。
4. 哪些场景能吃红利,哪些场景别乱加
4.1 能吃到红利的三类典型场景
不是所有项目都能靠这个参数省钱,我整理了最适合开启提示词缓存的几类场景。
第一类是知识库问答。这类服务通常有一个很长的系统提示词,里面写了 RAG 检索规则、回答风格、引用格式要求。用户在对话框里输入的问题反而是整段 prompt 里最短的部分。固定前缀占比越高,命中率越漂亮,省的钱越夸张。
第二类是 Agent 工具调用。现在很多 Agent 应用会在系统提示词里塞大量的工具定义 JSON Schema,一个工具定义可能就要几百 token,五个工具就是一两千 token。用户每换一个任务,工具定义却完全不变,这就是天然的缓存命中场景。
第三类是批量离线分析。我有个批处理任务,每天用同一套分析规则处理几千条文本,规则写在 system prompt 里,只有待分析文本在变化。这类任务开启缓存后,成本下降比例比在线服务还明显,因为它的固定前缀占比可以到 95% 以上。
还有一类容易被忽略的是客服导购场景。话术模板、商品规格说明、售后政策这些内容通常固定不变,用户问的问题五花八门,但问候语和规则说明始终是同一段。这类场景我在实测中命中率也能稳定到 90% 左右。
4.2 加了反而没意义的两种场景
第一种是 prompt 前缀过短。如果你的系统提示词总共就几十个 token,用户输入反而占了大头,缓存命中的意义就不大。平台通常还有最小 token 阈值要求,前缀太短可能根本不会被缓存。这种情况加参数也不会带来明显收益,还可能因为额外的缓存判断逻辑增加轻微延迟。
第二种是完全随机的请求结构。我见过一些项目把用户历史聊天记录全部拼在 system prompt 里,而且每次请求都拼入最新内容,导致前缀一直在变。这种场景下,前一次请求和后一次请求的前缀几乎没有重叠,缓存命中率会非常难看,有时候甚至降到 10% 以下,加了等于没加。
所以我在设计 prompt 时一直坚持一个原则:固定内容尽量集中放在前面,动态内容尽量靠后。这个习惯不仅有利于缓存命中,对模型理解稳定性也有帮助。
5. 我踩过的坑和排查技巧
5.1 请求加了参数,账单却纹丝不动
这是我第一次上手就遇到的坑。我在代码里加了extra_body={"cache_prompt": True},跑了半天拉账单发现费用和昨天一模一样,一点变化都没有。第一反应是平台不支持这个特性,翻文档确认支持之后,开始怀疑参数没传进去。
排查过程是这样的:先看响应里的usage字段,发现prompt_tokens_details完全是空的,压根没有cached_tokens这个子字段。然后我在代码里打印了实际发送的请求体,发现 SDK 把extra_body合并到了请求体里。但接口返回依然没有缓存标记。
后来我换了种方式,直接手动构造 HTTP 请求,把cache_prompt参数放在请求体顶层,一切就正常了。问题出在我用的 SDK 版本较老,extra_body的合并逻辑有差异。这里给大家一个排查顺序:先看响应字段,再看实际请求体,最后换一种调用方式交叉验证。
注意:一个比较隐蔽的问题是,有些平台对"无效参数"是静默忽略的,不会报错。你以为加了参数,实际请求里参数根本没到服务端。所以一定要以响应里的
usage为准,不要以请求代码为准。
5.2 命中率忽高忽低,是哪里出了问题
参数正常生效后,命中率也不是一下子稳定到 90% 以上的。我观察了一段时间,发现命中率在 60% 到 95% 之间波动,一开始很不理解:系统提示词明明一个字没改,为什么有些请求命中,有些请求不命中?
排查后发现,问题出在会话历史的拼接方式上。我原来的 prompt 结构是"系统提示词 + 最近 5 轮对话记录 + 当前用户问题",这导致前缀每次都不同。用户每多问一轮,前面 5 轮的内容就整体平移一次,跟上一轮请求的前缀对不上了。
改法也简单:把系统提示词和工具定义放在最前面,会话历史放在后面,并且固定只拼最近 N 轮。这样只要系统和工具定义不变,前缀就始终是稳定的,命中率很快就稳定到 93% 以上。
另外要注意的一点是,系统提示词里如果放了时间戳或者日期这类动态信息,也会让前缀失效。我有个任务在 system prompt 里放了"今天是星期几",结果每天第一次请求都会重新计算缓存,当天后续请求才能命中。后来把这类动态信息移到了用户消息里,问题才解决。
5.3 排查流程和最终建议
我把整个排查思路整理成一个固定流程,后面每次接入新项目都按这个来:
- 第一步:看账单结构,确认输入 token 占比是否超过六成。占比不够,优化效果有限
- 第二步:看 prompt 结构,确认固定前缀是否足够长且放在最前
- 第三步:开启参数后,检查响应里的
cached_tokens值,确认处于命中状态 - 第四步:连续观察两三天,统计平均命中率和每万次调用成本
- 第五步:如果命中率低于 80%,回头检查 prompt 里是否混入了动态内容
按这个流程走下来,基本不会有漏网之鱼。从我个人的经验看,提示词缓存是目前大模型应用成本优化里投入产出比最高的一项优化。它不是那种需要你重构架构、换模型、改训练方式的大工程,只需要一个参数加对位置,再加上一点 prompt 结构上的配合,就能拿到几倍的成本下降。
这个经验最关键的一点在于,优化成本之前先搞清楚每个词的分布,不要凭感觉动手。我踩过的最大的坑,就是把精力全放在压缩输出 token 上,结果发现那边怎么压都省不出大钱,真正的成本大头一直静静地躺在输入侧。后来改了 prompt 结构,开了缓存,一切才回到正轨。
如果你也在跑类似的服务,建议下周就做一次小范围的参数验证,拿真实账单数据算一算,看看你项目的固定前缀占比能不能撑起一个漂亮的命中率。这个测试成本很低,收益却可能超出你的预期。