最近有个在某创业公司做 AI 应用的朋友找我,智能客服上线三个月,用户没涨多少,账单倒是涨得飞快。他把后台拉出来给我看,一个月光模型调用费就好几万,里面有接近一半的输入,是每次都在重复的那套系统提示词。我问他平时有没有关注过提示缓存,他愣了一下,问我那是什么。这大概就是问题所在:大多数团队在用大模型 API 做产品时,把注意力全放在提示词效果和用户体验上,完全忽略了成本结构里最容易优化的一环。
提示缓存,英文一般叫 Prompt Caching,简单说就是把请求里重复出现的那部分内容在服务端存下来,下次请求如果前缀一致,直接复用中间计算结果,而不是重新推理一遍。这件小事不改变你的产品逻辑,不改变提示词的输出效果,却能明显压低账单。这篇文章我会从成本账讲起,把提示缓存的工作原理、配置方法、提示词优化策略,以及我在真实项目里踩过的坑完整拆一遍。无论你做的是客服机器人、内容生成工具,还是企业内部知识库问答,只要你的应用在反复调用同一个模型接口,这篇文章都值得你花十分钟看完。
1. 先算一笔账:你的AI应用到底贵在哪
1.1 你每次调用到底在付什么钱
主流大模型 API 基本都按 token 计费。token 可以粗略理解成被切碎的字或词,英文一个单词可能被拆成几段,中文的几个字也可能合并成一个 token。你发送给模型的全部内容,包括系统提示词、历史聊天记录、用户当前输入,一般按输入 token 计费;模型生成返回的内容,按输出 token 计费。两者的单价不一样,输出 token 通常更贵,这也是为什么很多人一让模型生成大段文字就肉疼。
但真正容易被忽略的,是"输入"这一块的结构。很多人以为模型请求的输入就等于用户打的那句话,其实远不止。为了让 AI 像一个有角色、有规矩、有记忆的助手,你往往要在每次请求里塞入一整套系统提示词,里面包含角色设定、业务规则、工具说明、few-shot 示例、输出格式要求。做客服、知识库、自动化流程这类场景时,这部分内容动辄几千 token,非常常见。
问题在于,这些提示词在每次请求里几乎一字不改。你的用户换了、问题换了、会话换了,那套系统提示词还是原封不动地再发一遍。模型服务商对每个请求都是独立处理的,只要你把内容发过去,它就按照输入 token 收一次钱。哪怕这个提示词昨天已经被同一个账号处理过一万次,今天你发第一万零一次,它不会认识你,依然按全新输入收全价。
1.2 重复计算才是账单刺客
我经常用一句话概括这个问题:你相当于雇了一个员工,每天上班第一件事是把公司规章制度从头到尾朗读一遍,读完才能开始干活。这个朗读过程不产生任何新价值,但工资是照付的。模型推理也一样,固定的系统提示词反复进入模型,前向计算一遍遍重新执行,算力被消耗,费方买单。
这就是"重复计算"带来的账单刺客。很多 AI 应用的功能并不复杂,单次调用模型生成的内容也不长,但成本却居高不下,秘密就在于每次请求背负着一个巨大的、重复的输入包袱。在对话类应用里,系统提示词加上多轮历史记录,往往能达到几千甚至上万 token。用户问一句几十 token 的话,你却要为几千 token 的上下文买单。
换个角度算,如果请求次数上来了,比如一天几万次,那固定提示词部分的输入 token 就是天文数字。这部分钱本可以通过缓存省掉大头,却因为没做任何处理,老老实实全额计费,那账单自然就会吓人。所以我一直觉得,AI 应用成本高,很多时候不是模型定价的问题,而是调用姿势的问题。
1.3 一组数字直观感受
我们编一个尽可能贴近实际的模型账号来估算。某智能客服机器人,系统提示词 800 token,工具说明和业务规则 200 token,用户提问平均 100 token。每天被调用 1 万次。日输入 token 总数大概是:
(800 + 200 + 100) × 10000 = 1100 万 token/天
其中真正不一样的动态输入只有 100 万 token,剩下 1000 万 token 是每次重复发送的固定指示。假设这个模型平台的普通输入价格按 3 元每百万 token 估算,缓存输入按 0.6 元每百万 token 估算,账就很好算了。
| 场景 | 每天输入 token | 估算单价(元/百万token) | 每天输入费用 |
|---|---|---|---|
| 完全不启用缓存 | 1100 万 | 3 | 33 元 |
| 启用缓存,固定提示命中 | 1000 万按缓存价,100 万按普通价 | 0.6 / 3 | 9 元 |
| 理想情况(固定提示不收费,仅作示意) | 100 万 | 3 | 3 元 |
换算成月度,就是 990 元对 270 元的差别,一个月省下七百多块。这还只是一个中规中矩的客服机器人,固定提示词只有 1000 token。如果系统提示词加到 3000 token,日调用量到 5 万次,能省的就不是几百,而是每个月几万块。很多人没概念,是因为从没见过模型服务商后台的缓存命中指标,只知道"欠的账单没变少"。
2. 提示缓存到底干了什么
2.1 先从大模型的生成过程说起
要把缓存机制讲清楚,得简单看一下大模型是怎么回答问题的。现在主流的大模型基本都是 Transformer 架构,输入内容会先经过分词、向量化,然后进入数十层神经网络。在每一层里有一个关键操作叫自注意力机制,简单理解就是:每个词都要和其他所有词建立关联,计算彼此之间的相关性权重,才能理解整个句子和上下文。
这个计算过程里,每个位置的词都会产生一组键(Key)和值(Value),用来参与后续注意力分数的计算和加权。问题在于模型生成内容是一个词一个词蹦出来的,生成第 10 个词时,它需要回头结合前 9 个词的信息;生成第 100 个词时,又把前 99 个词的信息都调出来。如果不做缓存,每一次生成都要把整个输入序列的 Key 和 Value 从头算一遍。序列越长,重复计算的浪费就越严重。
提示缓存的核心,就是把这些 Key 和 Value 计算出来以后暂存在内存里。下次遇到完全相同的输入前缀,不用再一次次前向计算,直接把存好的结果取出来用。这就好比你是分析师,第一次算季度数据时把所有中间步骤写在草稿纸上,第二季度处理同一套底层数据时,不需要从零开始重新计算,直接抄草稿纸上的结果就行。
2.2 缓存的命中条件
提示缓存不是什么魔法,它要求缓存系统能够精确判断"这次请求和之前的某个请求有共同前缀"。绝大多数服务商的实现方式,都是按输入 token 序列的前缀去做匹配。也就是说,你的请求开头部分必须和之前的某次请求完全相同,完全相同到每一个 token、每一个空格、每一个标点都不能差。
只要前缀一致,缓存就能命中,后面的 Key 和 Value 直接复用。哪怕用户消息一直在变,只要变化的部分出现在请求尾部,前面的大段静态提示词和工具说明就能稳稳命中。反过来,如果你在提示词开头放了时间戳、随机数、用户 ID 这类每次都会变的内容,整个前缀就漂移了,缓存直接失效,后面写再多的固定规则也救不回来。
这里有一个很多人容易误解的点:提示缓存不是只缓存你手动标记的某一段内容,而是以整个请求前缀作为粒度去缓存。它的判断天然是"从头开始,能匹配多长就匹配多长"。
2.3 缓存省的不只是钱
首先最直接的是省钱。缓存命中的输入部分,计费价格远低于普通输入价格。有的服务商打折力度大,差距可能接近十倍。对重读型应用,输入占比高,缓存带来的降本效果非常明显。
其次是降延迟。第一次请求要完整计算整个模型前向过程,生成首字的延迟会比较明显。缓存命中后,重复前缀那部分计算直接跳过,模型只需要处理末尾新增的动态内容,首 token 延迟能明显下降,有时从一秒级降到几百毫秒甚至更低。用户体感就是"应用变快了",这对对话类产品的交互体验是个不小的加分项。
第三是提高后端吞吐稳定性。同样一台推理机器,处理 5000 token 的输入和 50 token 的输入,计算量完全不同。启用缓存后,固定提示词不再反复占用算力,你购买的并发套餐能支撑更多真实业务请求。换句话说,同样的基础设施成本,能扛住更大的调用量,这在系统容量规划上也很有价值。
3. 实操:打开缓存并验证收益
3.1 第一步:确认你的模型服务是否支持
不同模型服务商对提示缓存的开放程度不一样,有的默认开启,有的需要手动开启,有的只在特定模型、特定接口上支持。你做的第一件事,是去你对接的服务商控制台或者 API 文档里搜关键词:提示缓存、上下文缓存、Prompt Caching、Context Caching,都会有说明。
先确认你的账号套餐、模型版本是否在这一功能的支持清单里。有些付费套餐和免费套餐在缓存能力上有差别,有些模型的上下文窗口对缓存长度有限制。反正别想当然,先在文档里确认清楚再动手。确认支持之后,找到缓存开关的位置。一般在控制台的模型服务配置里,或者在 API 请求参数里。如果控制台能找到开关,优先打开;如果只能在代码里传参控制,看下一步。
3.2 第二步:在代码里正确传参
这里我写一个演示用的 HTTP 请求示例,具体字段名以你实际对接的服务商文档为准。实战中我见过很多开发者在框架里配了一个参数,结果根本没传进请求体,白忙活半天。正确做法是先抓包或者打日志确认请求体里真的带了缓存相关字段。
import requests API_URL = "https://api.your-ai-service.example/v1/chat/completions" HEADERS = {"Authorization": "Bearer YOUR_API_KEY"} SYSTEM_PROMPT = """ 你是一名智能客服,负责处理用户的售后问题。 规则: 1. 用户问价格相关问题,先引导查看订单详情。 2. 用户情绪激动时,需要先安抚情绪。 3. 所有回答控制在200字以内。 """ def call_with_cache(user_input): payload = { "model": "your-model-name", "messages": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_input}, ], "cache": { "enable": True, "prefix_id": "customer-service-v2" } } resp = requests.post(API_URL, headers=HEADERS, json=payload) data = resp.json() usage = data.get("usage", {}) print("输入token:", usage.get("prompt_tokens")) print("缓存命中token:", usage.get("prompt_cache_hit_tokens")) print("缓存未命中token:", usage.get("prompt_cache_miss_tokens")) return data看到prompt_cache_hit_tokens这个字段了吗?你只要打印请求返回的 usage 信息,就能直观看到每一次请求里有多少 token 是从缓存里读出来的。如果这个字段一直为 0,说明你的提示词前缀没有做到稳定,命中一直失败,需要回到提示词工程上去优化结构。
3.3 第三步:用日志和指标验证收益
把代码改完不要急着上线,先在测试环境发几条完全相同的请求,确认第二次请求的命中字段有值。然后连续发几条前缀相同、后缀不同的请求,观察命中情况。记录接口返回的首字响应时间,对比第一次请求和后续请求的差距。
上线之后,在模型服务商的成本分析页面看缓存命中率趋势。我建议你至少监控这几个指标:提示词 token 总量、缓存命中 token 数量、缓存未命中 token 数量、缓存节省金额。把这些值做成日报,观察一天内不同时段的命中率波动。如果命中率稳定在一个比较高的水平,账单曲线自然会往下掉。
这个验证过程不需要很复杂,但一定要做。我见过太多团队把缓存开关打开就以为万事大吉,结果过了两周发现命中率只有 5%,成本根本没什么变化。原因往往就藏在提示词结构里,不验证是发现不了的。
3.4 一个可以照抄的验证脚本
给你一个简单的验证脚本,把系统提示词固定一个常量,循环发送几个不同的用户问题,把每次返回的缓存命中字段打出来。这个脚本不一定适配你家服务商,但结构可以参考。
import requests import time SYSTEM_PROMPT = "你是一个负责解答产品使用问题的助手。回答请简洁,不超过两句话。" def send(user_input): payload = { "model": "your-model-name", "messages": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_input}, ], "cache": {"enable": True}, } resp = requests.post( "https://api.your-ai-service.example/v1/chat/completions", headers={"Authorization": "Bearer YOUR_API_KEY"}, json=payload, ) data = resp.json() usage = data.get("usage", {}) return usage.get("prompt_tokens"), usage.get("prompt_cache_hit_tokens") if __name__ == "__main__": for question in ["你好", "怎么退款", "可以开发票吗"]: hit = send(question)[1] print(f"问题: {question} | 缓存命中token: {hit}") time.sleep(1)如果你发现第一轮发送就出现大量命中,那可能是之前的请求已经建立了缓存,这是正常现象。如果三轮命中都很低,你就该检查系统提示词是不是真的原样发送了。
4. 提示词工程让缓存命中率翻倍
4.1 把静态前缀和动态内容彻底拆开
提示缓存对前缀的稳定性要求极高,所以你的提示词结构必须刻意设计。我把提示词分成两类:一类是静态内容,包括系统角色、业务规则、工具说明、few-shot 示例、输出格式要求;另一类是动态内容,包括用户当前问题、当前时间、用户 ID、日志快照等。
最佳实践是静态内容全部放在请求的最前面,动态内容一律往尾部塞。拿一个客服机器人举例,理想结构是:
messages = [ {"role": "system", "content": "你是客服助手,规则如下:..."}, {"role": "assistant", "content": "好的,我明白了。"}, # 固定示例 {"role": "user", "content": f"当前时间:{now}\n用户ID:{user_id}\n问题:{question}"} ]这里系统提示词和第一条助手示例都是静态的,用户问题和各种动态信息被放到最后一条消息里。只要 system 部分一字不变,缓存就能稳定命中前面那几百上千 token。如果你懒得区分,直接写了一个 f-string 把当前时间放在 system 提示词第一行,那缓存命中率基本归零。
4.2 多轮会话里的缓存复用
对话类应用会出现一个疑惑:会话越聊越长,历史消息不断追加,缓存还能用吗?答案是可以的。缓存匹配的是"前缀",只要你的消息列表前面部分不变,后面多几条历史消息没关系。系统提示词在最前面,前缀稳定,整个历史对话作为后续内容一起参与匹配,缓存命中会作用于那段已经稳定的部分。
所以多轮会话的正确做法是:保持消息列表按时间顺序稳定追加,不要做任何花式重排。我见过有些团队为了"优化上下文利用效率",每次请求前把历史消息按某个规则重新排序,或者把前面的 assistant 回复截断拼接,结果导致前缀发生了细微变化,缓存大规模失效。这属于典型的画蛇添足。
如果你用了某个现成的编排框架,要格外小心模板动态拼接。不要以为框架生成的提示词结构就是合理的,很多模板默认把时间、用户信息放在开头,你需要主动去改模板,把静态内容前移、动态字段后置。
4.3 一个改造示例:某客服项目的前后对比
我拿一个实际改过的模拟项目来演示。项目背景是某个内部客服问答系统,面向公司员工解答行政制度问题。一开始开发同学图省事,把所有信息揉进 system 提示词里:
messages = [ {"role": "system", "content": f"当前时间是{now},你是行政助手。员工姓名是{user_name}。" "你负责解答考勤、报销、休假等问题。规则如下:..."}, {"role": "user", "content": question} ]这个写法的致命伤是当前时间和员工姓名放在开头,每次请求的前缀都在变,缓存命中率基本为 0。而且如果员工姓名在 system 里出现,不同员工请求的开头还不一样,服务商即便想缓存也无能为力。改造之后是这样:
messages = [ {"role": "system", "content": "你是行政助手,负责解答考勤、报销、休假等问题。规则如下:..."}, {"role": "user", "content": f"员工姓名:{user_name}\n当前时间:{now}\n问题:{question}"} ]改动就几行,效果是系统提示词变成完全静态的常量,缓存命中率从接近 0 一路涨到 80% 以上。后台里那一大块重复输入的计费,直接按缓存打折价结算了。这个案例里省下的具体金额不说了,但你可以对照自己的输入 token 占比算一下,这个改动的性价比高得离谱。
4.4 提示词的版本管理也会影响缓存
还有一个实操中的隐藏坑:提示词迭代太频繁。你今天改一个字,明天调一句话,每次改动都会让缓存的旧前缀全部失效。改完之后,线上请求会经历一段"缓存冷启动"期,命中率突然掉下来,然后慢慢恢复。这个成本也许不大,但你要心里有数。
我一般建议团队把系统提示词当作正式代码来管理,放进版本控制,每次改动走审批流程,不要在线临时改。同时,如果希望减少提示词变更带来的缓存爆炸,尽量把"可能经常变"的规则放在靠后的位置,把最稳定的角色定义放在最前。这样当你调整某条业务规则时,影响到的只是缓存长度变短了一点点,而不是整段前缀全部失效。
5. 常见问题与避坑清单实录
5.1 命中率很高,为什么账单还是没降多少
我遇到过这种情况,后台显示缓存命中 token 数量不少,但最终账单里费用变化不明显。排查下来,原因可能是你的实际成本大头在输出 token。缓存只作用于输入侧,模型生成的输出 token 该花多少钱还是花多少钱。如果你的应用每轮让模型生成大段文章、长报告,输出成本远超输入成本,那么优化输入缓存的收益就会显得有限,这并不代表缓存没生效。
另一个可能更麻烦:你看到的"命中"指标是服务商某个统计维度,不是真正的计费维度。有的平台在接口返回里会标注缓存 token,但账单明细里对这部分是否打折、按什么折扣,逻辑不一定对得上。我建议你直接对比启用缓存前后两周的账单,而不是只看单一指标。如果输入成本曲线明显更平缓,说明方向对了。
5.2 前缀微小差异导致缓存全盘失效
缓存失效最常见的原因是提示词里有不可见字符差异,比如多一个空格、多一个换行、中文标点和英文标点混用。你眼睛看过去觉得"一模一样",但 token 化之后就是两个完全不同的序列。特别是从配置文件读取提示词时,换行符在不同系统下的写法不同,Windows 的 CRLF 和 Linux 的 LF 都会被当成不同内容。
解决办法是把系统提示词统一放到配置中心或者独立文件里,禁止在业务代码里用字符串拼接动态生成。如果实在要拼接,也建议写一个统一的提示词构建函数,内部集中处理格式。我在几个项目里都吃过这个亏,排查了半天,最后发现是某次部署时换行符被开发工具自动转了一遍,缓存直接全部失效。
5.3 系统提示词更新后,命中率骤降怎么办
如果你需要定期更新系统提示词,比如每天换一套新闻热点规则,那缓存命中率骤降几乎是必然的。但这不代表无解。一个做法是尽量把变化的部分压缩到最小粒度,把稳定的角色定义放在前面,变化的规则放在后面,让缓存依然能命中前面那一段。另一个做法是错峰发布,在业务低峰期更新提示词,让缓存重建发生在流量小的时候,降低对成本和延迟的影响。
还有一点要提醒:不要为了省成本而硬扛着不更新提示词。缓存省的只是重复计算的费用,业务效果始终是第一位的。该调整规则就调整,把缓存成本当成一次性的"提示词刷新代价",可控且值得。
5.4 缓存不是万能,监控和预算 buffer 必须有
提示缓存的命中并不保证百分之百稳定。服务端可能因为内存压力淘汰缓存,也可能因为模型版本升级导致缓存规则变化。业务高峰期大量请求同时打到模型侧,也可能出现缓存尚未完全建立的阶段。所以不要把系统完全押注在缓存命中率上,账单预算里留出 10%-20% 的缓冲余量比较稳妥。
我的习惯是给成本监控配几条告警:日消耗异常上涨、缓存命中率连续多日下跌、缓存命中 token 归零。这三条基本能覆盖掉绝大多数成本突变的情况。特别是命中率归零这种极端情况,很可能说明服务商侧的缓存策略变了,或者你的提示词模板被人改了一刀,早发现早处理,别等月底账单出来才傻眼。
5.5 速查表:常见问题与建议处理方式
| 现象 | 可能原因 | 建议处理方式 |
|---|---|---|
| 命中字段一直为 0 | 前缀漂移、提示词每次变化 | 把动态内容移到末尾,固定前缀设为常量 |
| 命中率很高但账单没降 | 输出 token 成本占比高 | 检查输出统计,优化生成长度 |
| 提示词一改命中率暴跌 | 修改导致前缀整体失效 | 规则后置,版本控制提示词 |
| 缓存命中忽高忽低 | 服务端淘汰缓存、负载波动 | 预留预算 buffer,监控告警 |
| 日志显示 token 数一样,费用对不上 | 计费逻辑与缓存折扣不透明 | 对比前后账单,核对文档 |
我个人到现在的体会是,提示缓存不是一个后期优化,而应该在系统设计的第一天就写进约定。它和代码怎么写、提示词怎么组织强相关。把静态提示词当成公共接口来维护,把动态内容永远放最后,这两条规矩立住了,缓存基本就是白拿的收益。尤其是现在很多模型平台的缓存价格低得惊人,同样的骨干功能,别人可能只是调整了一下提示词顺序,成本就比你低了几个身位。谁先把这个细节做好,谁就能在同等预算下跑更久的业务、支撑更多的用户。这不是什么高深的算法,就是一个很朴素的事实:别让模型每次都在重复劳动。