上周财务在群里甩了张截图:本月大模型API账单5368元,环比涨4倍。"你们技术部门不花钱就难受?"那一刻,我突然理解了所有被砍过预算的算法团队。

我们做的是智能客服系统,对接三四家大客户。日活不算高,但对话极长。有些用户能跟机器人聊上百轮,每次请求都得把整段对话历史塞进上下文。模型每生成一个token,都要重新读一遍那座聊天记录大山。token像水一样流走。

打开网易新闻 查看精彩图片

必须上缓存了。不是Redis,不是CDN——是上下文缓存。核心思路是在语义层面对模型输入做去重:如果一段完整上下文已经算过一次,第二次就别傻乎乎重新算。上线后日耗token从约100万降到约10万,成本砍90%。API中位延迟从3.2秒降到0.4秒。以下是完整方案、代码,以及两个差点把我们炸飞的坑。

打开网易新闻 查看精彩图片

token到底浪费在哪

先交代背景。我们用Chat Completions API。每轮对话都要拼一个很长的messages列表发给模型。假设用户已经聊了30轮,当前请求长这样:

messages = [
{"role": "system", "content": "你是客服,请友好回答..."},
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "您好,请问有什么可以帮您的?"},
{"role": "user", "content": "我的订单没收到"},
{"role": "assistant", "content": "请提供订单号..."},
...
{"role": "user", "content": "还是没收到,已经三天了"}
]

每个新请求90%内容和上一轮一模一样,模型却从头处理所有token,账单把每个都算作输入token。典型的"Redis缓存响应"套路在这里没用,因为messages列表每次都变(末尾多一轮),缓存key永远对不上。

根因很清楚:我们没有把"已经算过的前缀"从计费和计算里剥离。如果能识别前缀已被缓存,复用上次模型的中间状态,就能省大量token。但OpenAI的API不像Anthropic那样原生暴露Prompt Caching功能(2024年底部分模型才上线),只能自己模拟。

方案设计:为什么不用向量检索,为什么要自研KV缓存

面前三条路:

1. 全消息响应缓存:只有整个messages列表完全一致才返回缓存答案。命中率几乎为零,因为每个新请求都多一轮。

2. 向量数据库语义匹配:把历史消息向量化,找"语义相似"的问题复用答案。但这会引入语义漂移,且快速演变的对话里上下文微妙变化会被忽略。更致命的是,它缓存的是"答案"而非"计算过程"——用户换种问法,向量匹配上了,但对话历史完全不同,直接复用答案会穿帮。

3. 自研KV缓存(Prefix Caching):在transformer的KV cache层面做文章。模型处理长序列时,每层的key和value矩阵可以缓存。如果新输入的前缀和缓存里的前缀完全一致,直接复用KV矩阵,只算新增的后缀部分。

第三条路才是正解。但实现上有两个硬骨头:

• 怎么快速判断"前缀是否命中缓存"——不能每次把整段文本哈希,太长
• 怎么存储和加载KV cache——OpenAI API不暴露内部状态

落地:用本地vLLM+前缀树实现

我们最终方案:弃用OpenAI API,切到本地部署的vLLM,利用其内置的Prefix Caching功能。

打开网易新闻 查看精彩图片

vLLM的PagedAttention把KV cache切成块管理。当新请求进来时,它用前缀树(radix tree)快速匹配已缓存的块序列。匹配上的块直接复用,没匹配上的部分才走模型计算。

关键代码结构:

• 对话按session_id隔离缓存
• 每轮对话的messages列表生成增量哈希(Rabin-Karp滚动哈希),O(1)更新
• vLLM的scheduler自动处理块级复用,无需业务层干预

上线效果:日token消耗从~1M降到~100K,延迟从3.2s降到0.4s。成本结构彻底改写。

两个差点炸飞的坑

坑一:缓存污染

初期测试时发现,用户A的session偶尔能命中用户B的缓存。排查发现是滚动哈希冲突——不同长文本可能撞出相同哈希值。修复:哈希值+前8个token文本做双重校验,冲突率降到可忽略。

坑二:缓存失效策略

vLLM的默认LRU淘汰在客服场景有问题。一个用户聊了两小时、上百轮,前缀树深度极大,但LRU可能把根节点淘汰掉,导致整棵树失效。改成按session_id的独立LRU池,长对话用户的缓存不被短对话挤掉。

复盘:为什么这事值得自己干

2024年底OpenAI给部分模型加了Prompt Caching beta,按缓存命中部分收25%费用。但我们的场景是:长对话、高重复、对延迟敏感。自研方案不仅更便宜(省90% vs 省75%),延迟也更低(本地推理 vs 网络往返)。

一个反直觉的发现:很多团队把"优化大模型成本"等同于"换更便宜的模型"或"压缩prompt"。但真正的大头在长对话的重复计算。上下文缓存是架构层面的重构,不是调参能解决的。

财务这个月没再@我。账单截图变成了延迟监控图——那条从3.2秒跌到0.4秒的曲线,比任何解释都有说服力。