问答机器人上线一周,token账单吓我一跳:30行缓存省掉94.5%调用,Python实战第8期

发布时间:2026/11/4 2:22:13
问答机器人上线一周,token账单吓我一跳:30行缓存省掉94.5%调用,Python实战第8期

前七期下来,问答机器人已经是个完整产品了:50行检索引擎,第2期接大模型做问答,第3期健康监控,第4期重试降级熔断,第5期SSE流式输出,第6期多轮记忆,第7期令牌桶限流。上周我把它推上线,跑了一周,账单一出来我人傻了。

调用量比我预想的高出一大截。拉日志一看,气笑了:排前20的问题占了总请求的一大半,全是"退货运费谁承担""优惠券怎么叠加"这种固定答案的问题。每个都要调一次大模型,花钱等延迟,答案还一字不差。这钱花得冤。

解决办法就两个字:缓存。命中缓存的问题直接返回存好的答案,不调大模型。但有个细节:用户问法五花八门,"退货运费谁承担"和"请问退货运费谁承担?"是一个问题,精确匹配缓存认不出来。所以缓存之前先做一层归一化:转小写、去掉标点、去掉多余空格,归一化之后再算key。

环境很简单,Python 3标准库就够,hashlib和re,不用装任何第三方包。

核心代码30行,贴出来直接能跑:

importhashlibimportre calls=0# 统计真实调了几次大模型deffake_llm(q):"""模拟大模型调用,实际用的时候换成你的API调用"""globalcalls calls+=1return"答案:"+q[:20]defnormalize(q):q=q.lower()q=re.sub(r"[^\w\u4e00-\u9fa5]","",q)# 去标点、去空格returnq cache={}defanswer_cached(q):key=hashlib.md5(normalize(q).encode()).hexdigest()ifkeyincache:returncache[key],True# 命中,不调模型a=fake_llm(q)cache[key]=areturna,False

光写缓存不够,得验证它真省钱。我写了个模拟流量:20个高频问题,每个随机加"请问"前缀、加问号、“怎么"换"如何”,凑200个请求,跑一遍看实际调了几次模型:

importrandom base=["退货运费谁承担","优惠券怎么叠加使用","发票在哪里开","会员积分什么时候到账","支持哪些支付方式"]*4random.seed(7)queries=[]for_inrange(200):q=random.choice(base)v=random.random()ifv<0.3:q=q+"?"elifv<0.5:q="请问"+qelifv<0.65:q=q.replace("怎么","如何")queries.append(q)hits=sum(1forqinqueriesifanswer_cached(q)[1])print("总请求:",len(queries))print("实际调大模型:",calls)print("缓存命中:",hits)

我机器上真实跑出来的输出:

总请求: 200
实际调大模型: 11
缓存命中: 189

200个请求只调了11次模型,省掉94.5%的调用。注意这是模拟流量的结果,你的业务里重复率没这么高,但只要有三成重复问题,缓存就值回票价。

三个坑都是我真实踩过的。第一个坑:缓存key千万别直接用问题原文当key。大小写、标点、全半角,任何一个字符不同就是两次调用,缓存形同虚设。归一化这一步不能省。

第二个坑:不是所有答案都适合缓存。价格、政策、库存这种会变的东西,缓存久了就是在发过期答案。我的做法是问题分级:高频且答案稳定的进缓存,时效性强的走实时调用。简单起见也可以给缓存加TTL,存的时候记个过期时间,取的时候检查一下:

importtime store={}defcache_set(key,value,ttl=3600):store[key]=(value,time.time()+ttl)defcache_get(key):ifkeyinstore:value,expire=store[key]iftime.time()<expire:returnvalue,Truedelstore[key]# 过期了,删掉returnNone,False

TTL设多长看业务:客服话术这种一天变不了几次,设24小时;价格库存类,设5分钟。别拍脑袋,翻一下这类数据的历史变更频率再定。

第三个坑跟第6期的多轮记忆有关。带上下文的问题不能直接缓存,"它多少钱"里的"它"指什么,脱离上下文根本不知道。正确做法是把上下文拼进key:归一化(历史问题+当前问题)再算key。key拼错了,缓存会一本正经地返回牛头不对马嘴的答案。

收藏这篇,下次你的问答服务账单爆炸,直接照着这30行加缓存,半小时回本。

下期预告:第9期讲机器人上线后的可观测性——调用量、延迟、缓存命中率三个指标怎么埋点,用50行代码搭个极简监控面板。这个系列在专栏《Python实战》里连着更新,从检索引擎一路写到上线运维,关注后专栏里能连着看。

讨论:你线上服务的重复请求占比大概有多少?缓存key你是怎么拼的,踩过什么坑?

—— Cyrus