☰
问答机器人转头就忘事?40行代码加个多轮记忆,Python实战第6期
2026/10/22 2:21:10 网站建设 项目流程

前五期下来,机器人已经像模像样了:50行检索引擎,第2期接上大模型做问答,第3期加了健康监控,第4期装上重试降级熔断,第5期搞定SSE流式输出。看着挺完整,真用起来你会撞上一个尴尬:它没有记性。

你问"我家猫三岁了,该打什么疫苗",它答得头头是道。紧接着问"那它平时吃啥好",它一脸懵:猫?什么猫?每一轮对话都是全新的开始,上一句说过的话,下一句全忘光。

病根不在模型,在我们。每次调API,我们只把当前这一句塞进prompt,历史对话根本没传过去。模型看不到历史,当然记不住。

修法也直接:维护一个对话历史列表,每次请求把历史拼进prompt一起发过去。听起来简单,真写起来有三个坎:token预算会爆,system提示词不能丢,历史太长得压缩。

今天把这三个坎一次踩平。代码40行,零依赖,复制就能跑。

先说环境。纯Python 3.9以上,不装任何包。大模型调用部分我用一个假函数代替,你换成自己的requests调用就行,记忆逻辑完全一样。

核心是一个ChatMemory类,干四件事:记消息、估token、超预算裁剪、拼prompt。

classChatMemory:"""多轮对话记忆:按token预算做滑动窗口裁剪"""def__init__(self,max_tokens=2000,reserve_ratio=0.5):self.messages=[]# 历史消息self.max_tokens=max_tokens self.reserve_ratio=reserve_ratio# 给新回答预留的token比例defestimate_tokens(self,text):# 粗估:中文约1.5字1个token,英文约4字符1个tokencn=sum(1forchintextif'\u4e00'<=ch<='\u9fff')returnint(cn/1.5+(len(text)-cn)/4)deftotal_tokens(self):returnsum(self.estimate_tokens(m["content"])forminself.messages)defadd(self,role,content):self.messages.append({"role":role,"content":content})self._trim()def_trim(self):budget=int(self.max_tokens*(1-self.reserve_ratio))whileself.messagesandself.total_tokens()>budget:# system提示词永远保留,删最旧的一条ifself.messages[0].get("role")=="system"andlen(self.messages)>1:delself.messages[1]else:delself.messages[0]defbuild_prompt(self):return"\n".join(f"{m['role']}:{m['content']}"forminself.messages)

token估算用的是粗算法。正式上线建议换tiktoken精确算,演示和自用这个够了,误差方向是往少了估还是往多了估,自己心里要有数。

裁剪逻辑注意两点。第一,system提示词永远不动,它是机器人的"人设说明书",裁掉就跑偏。第二,从最旧的开始删,保留最近的对话。人也一样,刚说的事记得最牢。

验证分两步。第一步测记性:

mem=ChatMemory(max_tokens=2000)mem.add("system","你是问答机器人助手。")mem.add("user","我叫小李。")mem.add("assistant","你好小李,有什么可以帮你?")mem.add("user","我叫什么名字?")print(mem.build_prompt())

prompt里带着"我叫小李",模型再答"我叫什么名字"就不会懵。第二步测裁剪:往里塞60轮闲聊,token总数被卡在预算线以下,system提示词还在第一位。我实测塞了60轮,裁完剩11条,token 96,system原地不动。

光裁剪还不够。有些场景历史很长,全删了可惜,比如用户聊了半小时需求。这时候上摘要兜底:把要删掉的旧对话压缩成一句话,塞回历史开头。

defsummarize_old(self,keep_last=4):"""把keep_last轮之前的旧对话压缩成一句话摘要"""iflen(self.messages)<=keep_last+1:returnhead=self.messages[:1]ifself.messages[0].get("role")=="system"else[]old=self.messages[len(head):-keep_last]importre keys=[]forminold:keys+=re.findall(r'\d+(?:\.\d+)?万?',m["content"])summary=f"之前聊过{len(old)//2}轮对话"ifkeys:summary+=f",提到数字:{'、'.join(keys[:5])}"self.messages=(head+[{"role":"system","content":"对话摘要:"+summary}]+self.messages[-keep_last:])

演示版用关键词拼接凑摘要。生产环境调一次模型做真正的摘要,效果天差地别,成本多一次调用,值。

踩坑时间,四个都是我真踩过的。

第一个坑:token估算偏差。中文按2字1token估,实际混着英文和标点,估少了15%。预算设2000,实际发出2300,API直接报超限。解决办法:预算打八折用,或者上tiktoken精确算。

第二个坑:多线程下历史串了。Web服务一个进程服务几十个用户,ChatMemory实例千万别做成全局单例。每个会话一个实例,用session_id做key存字典里,不然A用户的问题会出现在B用户的prompt里。

第三个坑:摘要把关键信息吃了。"用户说预算5万"这种数字,摘要模型觉得不重要给扔了,后面报价全错。我的做法:摘要前先正则捞出数字、日期、人名,拼在摘要前面,硬保。

第四个坑:system提示词被裁。早期版本裁剪没做保护,一轮长对话下来,人设说明书没了,机器人开始胡言乱语。记住:system永远豁免,这条写进_trim第一行。

收藏这篇,下次给机器人加记忆,照着这40行代码改,半小时搞定。

下期预告:Python实战第7期讲评测。你的问答机器人答得准不准,不能靠感觉,我教你搭一个自动打分流水线,关注我不错过。

讨论时间:你做的对话应用里,最长的一次对话有多少轮?token账单炸过吗?评论区聊聊。

—— Cyrus

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询