先看图,这是一个平均每天十万+PV、产生过亿tokens消耗的产品,而这个产品的平均响应时间不到三秒,上亿的tokens费用每天只要二三十块,本文讲一下我们是如何做到的。
贵&慢
如果有朋友已经上线了AI产品的话,相信应该你们也碰到了这个两个问题:
- 流程设计完成之后,发现自己的AI产品流程执行太慢,领导不满意。
- tokens如流水一般,钱花的太快。
解决方案:
- 使用上下文缓存。
- 优化流程
- 选择适当的模型。
今天,我们先重点聊聊上下文缓存,优化流程和选择合适的模型是一个复杂且需要深入研究的话题,我们会在后续的文章中详细探讨。
上下文缓存的解决方案又分两部分:
- 利用云端厂商提供的上下文缓存能力。
- 业务方(也就是我们),通过自己的方案进行缓存控制。
云端上下文缓存(Context API)
先看效果:
| 指标 | 提升比例 | 案例(前 → 后) |
|---|---|---|
| 速度 | +27% | 4秒 → 2.9秒 |
| 费用 | -70% | 10万/年 → 3万/年 |
实测下来,基本上可以做到提升 27% 的响应速度和 减免70% 的费用消耗。
原本我们需要四秒响应,现在只需要不到三秒就可以,70%的费用减免可以从原本的十万每年一下降到三万每年。
这就是上文缓存带来的能力,接下来介绍一下我使用的缓存方案,来自火山方舟的上下文缓存(Context API)。
上下文缓存(Context API)是火山方舟提供的一个高效的缓存机制,旨在优化生成式AI在不同交互场景下的性能和成本。它通过缓存部分上下文数据,减少重复加载或处理,提高响应速度和一致性。
在方舟中目前存在session 缓存和前缀缓存两种缓存方式:
- session 缓存:保留初始信息,同时自动管理上下文,在动态对话尤其是超长轮次对话场景的可用性更强,搭配模型使用,可以获得高性能和低成本。
- 前缀缓存:保留初始信息,适合静态Prompt模板的反复使用场景。
应用场景如下:
| 功能 | 场景举例 | 适用缓存类型 |
|---|---|---|
| 标准化对话开场白 | 用于统一对话开场白,如"您好,请问有什么可以帮您?" | 前缀缓存 |
| 特定任务的指令 | 如翻译任务中的固定指令"请将以下内容翻译成英文:" | 前缀缓存 |
| 规则化模板 | FAQ系统的固定回答前缀,如"关于这个问题,我们的官方回答是:" | 前缀缓存 |
| 长期多轮对话 | 陪伴类AI需要记住用户长期偏好和过往对话内容 | Session 缓存 |
| 连续问题回答 | 客服机器人需要基于之前的问题上下文回答后续相关问题 | Session 缓存 |
| 长篇文本生成 | 生成小说或长文时需要回顾之前已生成的内容以保持连贯性 | Session 缓存 |
| 控制抖动时机 | 对延时敏感的场景,希望将网络抖动控制在非关键对话阶段 | Session 缓存 |
| 控制历史采纳量 | 对模型效果敏感的场景,需要精确控制模型采纳的历史对话轮数 | Session 缓存 |
我们需要按照如下步骤使用缓存:
- 在火山方舟开通对应模型的缓存能力
- 创建缓存
- 使用缓存
session缓存和前缀缓存支持的模型不一样,按照我们常用的模型来说:session缓存支持doubao-1.5,前缀缓存支持doubao-1.5和deepseek v3、deepseek r1。
选择缓存的时候,也需要先看下自己的业务使用了哪个模型。
示例代码
官方都没给node的示例代码,还得是贴心的我,给大家一份node版本的示例:
无论想要使用哪个缓存,区别就是创建缓存的时候传入的mode参数:common_prefix代表前缀缓存,session代表session 缓存。
创建缓存
// ! 创建缓存 -
async function createCache() {
const response = await fetch('https://ark.cn-beijing.volces.com/api/v3/context/create', {
method: 'POST',
headers: {
'Authorization': 'Bearer 自己的key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: model, // 放自己在火山上创建的推理点EP,建议接入doubao-1.5-pro-32k
messages: [
{
"role": "system", "content": `你的名字是小王` }
],
ttl: 3600, // 缓存存在时间
mode: "common_prefix", // 换成 session 就是使用session缓存
})
});
const data = await response.json();
console.log(data);
}
createCache()
使用缓存
上一步创建缓存最后输出的data中,有一个context_id字段,是ctx开头的。这个字段可以再我们调用大模型时传入,大模型就可以知道我们要使用某个制定的缓存内容了。
比如上面代码我们缓存的内容是:你得名字是小王, 那么我们调用大模型时,问你叫什么,大模型便会回复我们小王。
import OpenAI from 'openai';
const model = '' // 放自己在火山上创建的推理点EP,建议接入doubao-1.5-pro-32k
const openai = new OpenAI({
apiKey: '', // 换成自己的key
baseURL: 'https://ark.cn-beijing.volces.com/api/v3/context',
});
async function main() {
const res = await openai.chat.completions.create({
messages: [
{ role: 'user', content: `你叫什么?` },
],
temperature: 1,
context_id: "ctx-20250501105839-8qzbt",
top_p: 1,
model: model,
stream: true
});
for await (const part of res) {
process.stdout.write(part.choices[0]?.delta?.content || '');
}
process.stdout.write('\n');
}
main();
这里需要注意的是,如果我们使用缓存那么baseurl就不再是原来的https://ark.cn-beijing.volces.com/api/v3 而是https://ark.cn-beijing.volces.com/api/v3/context
此处做的是示例,在真实的线上环境中,利用缓存能力缓存了绝大部分的输入tokens,例如我们某个意图的判别提示词有3000+输入tokens的消耗,这还只是产品响应流程中的一个节点,这样的节点我们有十几个,响应流程每次执行会用到2-3个节点,使用了缓存之后,我们每次都缓存了大几千的tokens,这给我们省了很多费用。
计费
-
输入(元/千 token):新的请求中您无需重新发送历史对话,输入 token 仅代表添加到正在进行的对话中的新文本。在 rolling_tokens 模式下触发重新计算时,会将保存的历史对话重新计算和缓存,与新输入内容一样计费。
-
缓存命中(元/千 token):缓存中使用的内容。方舟自动处理历史记录,并输入给模型,这部分内容即命中缓存内容,他们也会产生费用,但是优化了计算和读入缓存的开销,计费费率会显著低于新输入内容。
-
存储(元/千 token/小时):历史对话存储在 Session 缓存中,会产生存储费用。计算方式根据每个自然小时使用缓存的最大量乘以单价进行累加。 举例说明(单价为虚拟单价,仅做举例使用):单价为 0.000017 元/千 token/小时,第 1 小时 Session 缓存最大的缓存用量 10k token,第 2 小时 Session 缓存最大的缓存用量 15k token,那么存储费用为:0.000017*10+0.000017*15 = 0.000425 元
限制
- session 缓存不支持并发调用。
- 前缀缓存支持并发调用。
- 前缀缓存过期时长可配置范围在1小时到7天,即ttl字段设置范围(单位:秒)为 [3600, 604800]。
- 都不支持 Partial 模式(又称 Prefill Response)。
Prefill Response 一个新概念,简单解释一下就是调用上下文缓存对话 API时,messages数组的最后一条消息的role字段取值可以是user或system,而不能为assistant。
这是个很好的技巧,不会的同学要记好:如果我们的messages中最后一条的role是assistant,对于大模型来说assistant的内容就是大模型自己前面输出,所以大模型会顺着这个内容开始输出,这就让我们很好的控制了大模型的输出!
例如, 我们传入了这样一个messages,user问1+1,assistant回答=,大模型接下来的输出就会被控制的很好,始终是2,而不是其他什么思考逻辑之类的废话:
const res = await openai.chat.completions.create({
messages: [
{ role: 'user', content: `1+1` },
{ role: 'assistant', content: `=` },
],
});
业务方的缓存方案
我们业务上的缓存,主要是缓存用户的query与对应的大模型回复。当不同的用户使用了相似的问题进行提问时,我们使用缓存的大模型回复内容进行回复。
在性能、费用要求都相对充裕的情况下,并不建议大家在业务侧自己做缓存,自己做缓存比较吃场景,一旦做不好带来的问题比带来的收益要大很多。
目前比较适合场景有:
- 用户的问题相对简单、单一的场景
- 对回复时效性要求高,且对于回复的精准性要求不高的场景
- 预算不够,又得上智能化产品的场景。
比较落地的方案有两部分,缓存知识库和缓存tools:
- 利用知识库
放弃知识库筛选多条内容让AI总结回复的用法,转而使用匹配答案的思路。
提前离线生产一批问题,投入知识库,然后将知识库的匹配数量调整为1,同时把匹配分数限制调高。
这样尽可能保证用户query和知识库的命中率,基本上做到每个从知识库匹配到的答案,都是正确答案。
但是知识库难免会出现匹配失误的情况,而我们场景又要求比较严格时,我们还可以再加入相关性验证的逻辑,即:使用AI对我们知识库中的信息和用户的query进行相关性验证。
例如:知识库中存有解释字段 解决XX问题,当AI拿用户的query和知识库中匹配到的信息进行相关性验证时,他就可以知道是否是解决用户问题的答案了。
- tools缓存
适用于Agent流程使用了大量function call 工具匹配 + 参数提取的产品。
本文不对function call做赘述,后面会出一期function call的能力和应用详解。不了解function call的能力的同学,可以先行百度一下。
当我们大量使用 function call的能力时,我们是需要调用API,然后交给大模型进行回复。
这里我们可以缓存对应的tools名称+参数和对应的大模型回复,下次直接进行回复。
当下一次出现对用的tools名称+参数时,直接拿出缓存的回复内容进行回复。
结语
当下各家厂商推出了不同优惠政策,我们只要善于利用就可以给自己给公司节约不少钱:
- deepseek的闲时半价。
- 火山的上下文缓存。
- 火山的批量推理半价优惠。
这些优惠对我们无论是线上还是线下、离线还是在线的应用,都具有非常大的优惠,例如本文讲的上下文缓存。
还有我们可以把任务做离线的批量推理,享受半价优惠。
说起来,有没有人好奇火山的批量推理 + 上下文缓存一起用,能便宜到什么程度??🤣🤣🤣
哈哈哈哈.....,想多了各位,火山的上下文缓存只支持在线推理,不支持和批量推理一起使用。
☺️你好,我是华洛,如果你对程序员转型AI产品负责人感兴趣,请给我点个赞。
你可以在这里联系我👉www.yuque.com/hualuo-fztn…
已入驻公众号【华洛AI转型纪实】,欢迎大家围观,后续会分享大量最近三年来的经验和踩过的坑。