AI 应用的缓存、并发与成本优化:从单次调用到可控吞吐
前面的文章已经介绍了模型选择、错误处理和成本估算。原型能跑起来之后,新的问题通常是:请求一多,响应变慢,账单也跟着上涨。很多时候,优化并不始于更换模型,而是先减少不必要的调用,再让剩余调用以可控的并发运行。本篇只聚焦一个核心知识点:如何用缓存和并发控制改善 AI 应用的吞吐与成本。示例使用 Python 标准库模拟模型调用,不需要密钥,读者可以直接运行并观察缓存命中和并发限制。
为什么缓存和并发要一起考虑
一次模型调用通常包含网络等待、排队和生成时间。若相同输入被重复提交,例如用户刷新页面、定时任务重复执行,直接再次调用既浪费钱,也增加延迟。缓存可以把“输入相同、允许复用”的结果保存起来,命中时不再访问模型。
但缓存不能解决所有问题。面对大量不同请求时,同时发出太多调用可能触发服务商限流,甚至让本机连接、内存和下游系统过载。因此需要并发上限:允许多个请求同时进行以提高吞吐,但最多只运行固定数量。可以把它理解成一个有容量的服务窗口,而不是无限扩张的线程数。
一个实际的调用层至少要回答四个问题:缓存键是什么、结果能保存多久、最多允许多少并发、失败结果是否缓存。缓存键必须包含会影响结果的因素,例如模型名、系统提示词版本和用户输入;否则不同配置可能错误地复用同一个结果。失败结果通常不应该缓存,否则一次临时故障会被保留成长期错误。
最小可运行示例
下面的代码用 asyncio 模拟一个耗时的模型调用。fake_model_call 代表真实 SDK 的异步请求;实际接入时,只需要替换这个函数,并保留缓存和信号量的边界。程序还记录了模拟成本:缓存命中不产生新的模型调用,未命中则按一次输入和输出 token 估算。
1 | import asyncio |
运行方式:
1 | python cache_demo.py |
这段程序有三个关键点。第一,cache_key 使用 SHA-256 生成稳定键,但哈希并不会自动让缓存正确:真正重要的是把模型和提示词版本也纳入键。修改系统提示词后,应升级 prompt-v1,避免旧答案污染新逻辑。第二,asyncio.gather 同时提交多个任务,Semaphore(2) 则把实际模型调用限制为最多两个并行任务。第三,信号量内部的第二次缓存检查是必要的:两个相同请求可能同时发现缓存为空,排队后必须再次检查,才能避免重复调用。
从内存缓存走向真实系统
示例中的字典只适合演示。进程重启后数据会消失,多进程部署时每个进程也各自维护一份缓存。生产环境通常会使用带过期时间的缓存服务或数据库,并为每条记录设置 TTL(生存时间)。TTL 太短,命中率低;太长,答案可能过时。适合缓存的往往是确定性较高、重复率高且允许短暂陈旧的任务。
缓存还要控制容量和隐私。不要把完整的敏感原文、访问令牌或用户个人信息无期限保存;可以只保存必要结果,并设置最大容量、过期时间和访问权限。多租户应用必须把租户标识纳入缓存键,否则可能把一个用户的结果返回给另一个用户。对于包含实时数据、权限信息或随机生成要求的请求,应明确禁止缓存,或者让这些因素参与键计算。
并发上限也不能凭感觉设定。先记录请求数、平均延迟、P95 延迟、限流错误和缓存命中率,再逐步调整。并发数增大不一定更快:当服务商限流或网络成为瓶颈时,继续增加只会带来更多重试和更高失败率。实际调用还应结合前文的超时与指数退避,并区分可重试错误和业务错误;不要对所有失败无限重试。
常见问题
缓存命中后还要计费吗? 如果确实没有发起新的模型请求,通常不会产生这一请求的模型调用费用;但缓存基础设施本身可能有存储和网络成本,计费规则仍应以所用服务商为准。
是不是并发越大越好? 不是。并发只是把等待时间重叠起来,不能突破服务商配额。应从较小值开始,结合限流、错误率和尾延迟压测。
为什么代码要在信号量内再次检查缓存? 多个相同请求可能同时通过第一次检查。第一个任务写入结果后,后续任务如果不复查,仍会重复调用模型。这个模式常称为“缓存击穿保护”的最小版本。
什么时候不应该缓存? 当结果必须实时、与用户权限强相关、包含敏感数据,或每次都要求模型重新随机生成时,不应直接复用旧结果。即使能缓存,也要先完成脱敏、隔离和过期设计。
小结
- 缓存通过复用相同请求的结果,减少重复调用、延迟和成本。
- 缓存键必须包含模型、提示词版本以及所有影响结果的输入。
- 异步并发可以提高吞吐,但信号量能把并发控制在服务商和系统可承受的范围内。
- 在并发控制区域内再次检查缓存,可以避免相同请求同时穿透缓存。
- 真实系统还要补充 TTL、容量、隐私隔离、超时、限流监控和成本记录。
到这里,AI 应用的基础工程化路线已经覆盖了成本、并发和性能。下一篇可在已有知识上综合实现一个小项目,但应先明确数据边界、失败策略和可验证指标,再把多个组件组合起来。