十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

3行代码给客服AI装上长期记忆:从原理到工程落地

3行代码给客服AI装上长期记忆:从原理到工程落地 先说个真实场景。用户上午来投诉过“订单延迟发货补偿一张20元优惠券”客服机器人确实回复了“补偿已到账24小时内可用”。结果下午用户再来问“上次说的优惠券怎么还没到”AI一脸茫然“很抱歉我没有查到相关优惠记录您可以重新描述一下问题吗”就这一下用户基本就炸了。做客服系统的人应该都懂这种“AI总忘事”的体验跟人工客服完全没法比。本期内容就专门解决这个问题给客服AI加上一套记忆系统让它记得用户说过什么、解决到什么程度、有哪些承诺和偏好。关键是核心能力的接入只需要3行代码——你没看错我把记忆的“提取、检索、注入”三个动作压缩到3个调用里跑通整套流程。适合正在做客服机器人、AI Agent、CRM系统的开发者也适合自己搭AI应用但被“上下文割裂”折磨的独立开发者。1. 客服系统为什么需要“记忆”以及它到底记什么先说清楚一个底层事实大模型本身就是无状态的。你发一句“我想退货”它回一句“好的请提供订单号”这是独立的两个时间点模型不会自动记住上一句。你现在用的所有客服AI如果每次会话都从零开始那你体验到的就是“机器人反复确认订单号”的经典智障场景。这不是模型能力差而是工程架构上没做“状态管理”。聊天机器人不聊天的时候它什么都不记得而你引入的所谓“记忆”本质上是把对话历史、用户画像、业务事实在模型“失忆”之前额外保存到外部存储里下次提问前再捞出来塞进上下文。理解这个你就理解市面上所有AI记忆方案的核心逻辑了。1.1 无状态AI与“伪记忆”之间的差距有人可能觉得我每次把最近20轮对话都拼进prompt这不就是记忆吗对这叫“伪记忆”或者叫“窗口记忆”。它的确能保证当前会话上下连贯但跨会话就抓瞎。用户第二天再来你昨天拼接的20轮对话全部失效AI照样不认得他。伪记忆在客服场景里还有两个硬伤。一是Token成本20轮对话拼进prompt每来一个新请求就重复计费一次用户聊得越久成本越离谱。二是噪声污染20轮里可能15轮是客套话和无效信息真正有用的“用户要开发票”、“上次退款金额是88元”被淹没在里面大模型有时反而抓不住重点。所以真正的记忆系统要做的是“提炼事实而不是保存流水账”。就好比人工客服有个小本子和用户聊完以后记的不是每个字而是“处理了退款、用户偏好顺丰、下次优先用优惠券安抚”。本子记的是结论和要点不是原始录音。这个思路贯穿全文后边的代码也是按这个思路写的。1.2 按场景拆解记忆到底要记哪几类信息在设计客服记忆之前建议先把“记忆对象”分清楚。我通常分成三层每一层的存储策略完全不一样记忆类型生命周期存储粒度典型内容示例遗忘策略会话记忆当前会话内原始消息或摘要用户刚发的“我要查物流”会话结束即失效用户长期记忆跨会话持久结构化事实/向量用户偏好夜间送货、会员等级、历史投诉原因按时间衰减/人工删除业务事实记忆与用户无关但客服必需知识库/商品库退货规则、物流时效、优惠叠加限制运营方主动更新你会发现会话记忆解决的是“当前聊天不串行”用户长期记忆解决的是“下次来不用重新自我介绍”业务事实记忆解决的是“客服别瞎答规则”。多数团队只做第一层所以AI一直显得“很健忘”。本期重点讲第二层也就是跨会话的用户记忆因为这是“3行代码”能解决的也是提升体验最明显的。1.3 记忆系统的四个关键动作不管用什么技术栈记忆系统本质上就四个动作提取、存储、检索、注入。这四个词建议你贴在工位上排查问题的时候按这个顺序捋基本都能定位到环节。提取Extraction就是把“用户说了一堆话”里的长期价值信息抽出来。比如用户说“我孩子下周生日帮我看看有没有适合周末到的礼物。”这里值得长期记住的是“孩子生日在下周”而不是“用户在看礼物”这个瞬时意图。第二步存储Storage把提出来的事实写成结构化条目或向量落到数据库里。第三步检索Retrieval用户下次提问时拿当前问题去库里找回相关的历史事实。最后注入Injection把召回的记忆拼到系统提示词或上下文里发给大模型。举个例子。用户第一次说“每次发货都放错楼层你们快递员是不是不看备注”你提取出的长期记忆就是“用户要求发货前确认楼层备注曾因放错楼层投诉”。存好后过几天用户只说一句“还是老问题”你检索到这条历史事实注入到系统提示词里AI就能答“很抱歉楼层又放错了我这次帮您优先联系配送员确认位置并同步申请一张无门槛券。”这个效果就是记性好的客服而不是一台复读机。2. 技术选型为什么我在生产里选记忆库而不是全手工拼说实话给AI加记忆这件事我第一次做的时候也是纯手工方案把所有聊天记录倒进Redis来新请求时按时间滚动取最近50条塞给模型。刚开始觉得挺简单上线两天就被打脸。用户聊过的事情太多50条窗口根本装不下关键信息而且无关内容把有效信息挤得干干净净。自那以后我就明确了记忆这件事不能靠“堆原始文本”必须有一个“先提炼、再召回”的中间层。而这个中间层自己从零写一套成本很高。后来社区里出现了几个现成的记忆库方案把提取、存储、检索、注入封装成了很简洁的API核心调用确实可以压缩到几行这也是本期标题里“3行代码”说法的来源。2.1 自己写记忆的人后来都后悔了什么自己写记忆的早期版本流程通常是这样的定义一张消息表字段就是user_id、content、created_at每次请求前把该用户最近N条消息查出来拼成“历史记录”字符串。问题出在哪第一最近的消息不一定是最重要的消息。用户十句话里可能只有一句透露了“我在外地出差周五回来”而剩下九句全是“嗯、好的、谢谢”。时间窗口一长关键信息被稀释窗口一短重要信息直接丢失。第二所有历史记录都是明文原文再塞给模型模型还要花时间去理解哪句重要不但浪费Token回答还容易跑偏。更麻烦的是冲突处理。用户第一次说“我住在上海市浦东新区”第二次说“我搬家到杭州了”你如果只拼原文模型可能会得出“用户住在上海和杭州”的矛盾结论。这种问题靠“流水账拼接”是解决不了的必须有一个“按用户维度做事实更新”的层。自己做这层就得写版本控制、去重、合并、优先级判定工程量一点都不小。2.2 mem0/Zep这类记忆库到底做了什么事现在社区里比较流行的一类库是把“事实提炼向量检索”打包成一套服务的方案典型的就是mem0、Zep这类开源项目。它们做的事情通俗讲就两步一是拿到一段对话文本先让大模型从中抽取关键事实并把同类事实合并、覆盖旧值二是把这些事实做向量化存储用户提问时用语义相似度召回最相关的条目。你打开这类库的文档会发现核心API确实很精简往往就是一个add方法存记忆、一个search方法查记忆。像mem0安装后调用大概就是下面这种写法from mem0 import Memory memory Memory() # 写入把当前对话内容里值得长期记住的部分抽出来存储 memory.add(用户要求发货前必须电话确认且偏好顺丰速运, user_iduser_10001) # 检索根据用户当前问题召回相关历史记忆 related memory.search(这个消费者怎么联系最稳妥, user_iduser_10001)你仔细琢磨一下add背后是把一句自然语言拆成“用户偏好顺丰”、“需要电话确认”等事实条目search背后是语义匹配而不是简单的关键词匹配。这套东西如果自己拿向量数据库加提示词工程去写没有个几百行下不来所以我说“3行代码”解决的是接入成本不是底层原理的复杂度。不过这类库也不是银弹。它对底层模型有依赖——抽取事实和生成向量都需要调用模型接口调用量大就会产生费用。而且不同版本的API差异不小有的用Memory类有的叫MemoryManager接入前务必先看本地安装版本的docstring别照抄网络上的老代码。后面章节我会给一个不依赖任何记忆库的轻量版实现原理是一样的适合学习和低成本场景。2.3 记忆库背后那套通用原理自己也能实现很多人一听到“记忆库”就觉得高深其实把黑盒打开核心就是两个部分事实库和检索器。事实库存的是“用户张三偏好夜间送货”检索器负责在用户提问时找出最相关的事实。自己动手做最简版本可以用一张SQLite表字段是user_id、fact、created_at。写入逻辑用一次大模型调用提示词就一句话“从对话中提炼出值得长期记住的用户信息只输出JSON数组。”检索逻辑最简单的是先按用户筛再用关键词或向量相似度排序取前Top N。向量相似度如果你不想引入额外服务可以用sentence-transformers这类本地模型生成句子向量再算余弦相似度。这一套做下来也就两三百行但你已经把记忆系统的骨架搭起来了。后期发现不够用了再把存储换成FAISS或专门的向量数据库把抽取和检索换成记忆库API业务层代码几乎不用动。也就是说理解原理比追捧工具重要得多。3. 实操演示3行代码把记忆装进客服机器人接下来是重点。我会完整演示一个“客服机器人长期记忆”的最小实现。为了贴合标题里“3行代码”的说法我会先把核心逻辑压缩成3个调用然后补充一份能跑起来的完整示例以及一个不依赖外部记忆库的轻量方案。先说清楚我这里用的mem0是截止写作时社区常见版本API在不同版本间有调整。你安装后如果发现导入方式不一样就用help()看文档逻辑是通用的。3.1 环境准备与安装先准备一个干净的Python环境要求Python 3.9以上。安装依赖pip install mem0ai langchain-openai我建议你同时装好你平时用的大模型SDK比如OpenAI、DashScope或者其他国内厂商的SDK因为mem0只负责记忆真正回复用户还是靠大模型本身。如果你是在国内环境调用记得把base_url换成对应服务商的网关地址这里不展开各家文档写得很清楚。安装完之后第一件事是检查版本和可用方法python -c import mem0; print(mem0.__version__)然后根据你的记忆库版本初始化配置。mem0早期版本用默认配置就能跑它会自动在本地建一个向量索引目录新版本可能要指定embedding模型和LLM配置。无论如何先跑通默认配置再按需改成生产环境参数。3.2 核心3行代码提取、检索、注入我把“给AI加记忆”的核心过程压缩成三行每一行对应一个关键动作from mem0 import Memory memory Memory() memory.add(user_iduid, textuser_message) memory_text memory.search(user_iduid, queryuser_message) prompt base_prompt 已知用户信息 memory_text第一行初始化记忆库相当于打开小本子。第二行add方法把用户刚说的内容交给底层大模型让它抽取值得长期保存的事实并写入存储。这里要注意add传入的是“完整对话文本”或“一条完整发言”不是整个会话流水账你可以把多轮对话拼成一个字符串再传入让抽取更准确。第三行的search是根据用户当前问题从已有记忆中召回最相关的一批条目结果是一个字符串列表或JSON数组。最后把召回结果注入到系统提示词里再发给对话模型。这三行就是记忆系统的灵魂。很多教程还要写一堆初始化配置其实核心API调用就在这。当然工程化落地不会只有这三行后面我会展开讲。3.3 接入真实客服流程完整的FastAPI示例光有核心三行不够我把它放进一个真实的客服接口里。下面是一个FastAPI写的简单客服后端流程是接收用户消息→查询历史记忆→生成回复→写入新的记忆。from fastapi import FastAPI from pydantic import BaseModel from mem0 import Memory from langchain_openai import ChatOpenAI app FastAPI() memory Memory() llm ChatOpenAI(modelgpt-4o-mini, temperature0.3) BASE_PROMPT 你是某旗舰店的客服助手。 请优先参考已知用户信息来回答如果信息不足就正常引导用户补充。 回答要简洁、专业、有温度。 class ChatRequest(BaseModel): user_id: str message: str app.post(/chat) async def chat(req: ChatRequest): # 第一步检索该用户已有的长期记忆 related memory.search(queryreq.message, user_idreq.user_id) # 第二步把记忆注入系统提示词 system_prompt BASE_PROMPT \n已知用户信息\n .join(related) # 第三步调用大模型生成回答 response llm.invoke([ {role: system, content: system_prompt}, {role: user, content: req.message}, ]) # 第四步把本次对话内容写入记忆库 memory.add(textreq.message, user_idreq.user_id) return {reply: response.content, recalled_memory: related}这里有几个细节值得强调。user_id必须由业务层唯一标识客服系统里通常用用户ID或手机号脱敏后的哈希值不能是整个系统共用一个ID那等于所有用户共享记忆会串隐私。search出来的结果可能为空字符串这时候不要往prompt里硬拼我已经在示例代码里做了基础处理思路展开成生产代码时建议加个空值判断。写入动作放在回复之后而不是回复之前目的是确保记忆库里是我已经处理完的一段内容避免把正在回答的内容再抽取一遍重复存储。3.4 如果不想引入重库一个轻量本地记忆Demomem0解决得很好但不是所有项目都适合引第三方依赖。如果你的客服机器人并发不高、只需要“跨会话记住用户偏好”这一条核心能力我建议你直接自己写一个轻量记忆类。下面这个Demo只用Python标准库加SQLite逻辑非常简单。import sqlite3 from datetime import datetime class TinyMemory: def __init__(self, db_pathmemory.db): self.conn sqlite3.connect(db_path) self.conn.execute( CREATE TABLE IF NOT EXISTS memory ( user_id TEXT, fact TEXT, created_at TEXT ) ) self.conn.commit() def save_fact(self, user_id: str, fact: str): self.conn.execute( INSERT INTO memory (user_id, fact, created_at) VALUES (?, ?, ?), (user_id, fact, datetime.now().isoformat()) ) self.conn.commit() def recall(self, user_id: str, limit: int 10): cur self.conn.execute( SELECT fact FROM memory WHERE user_id ? ORDER BY created_at DESC LIMIT ?, (user_id, limit) ) return [row[0] for row in cur.fetchall()]这个版本没有语义检索只是按时间取最近N条事实。你可以在save_fact之前先调用一次大模型“从用户发言中抽取10字以内的事实列表只输出JSON”再把JSON里每个fact逐条save。召回的时候直接按时间倒序取简单粗暴。实测下来对于重复问题较多的客服场景这个轻量方案在准确率上并不比复杂的向量检索差太多因为用户事实类信息往往表达比较固定时间倒序基本能覆盖大多数需求。真正需要向量检索的场景是用户问法变化很大比如今天说“退款进度”明天说“钱怎么还没退回来”这时候语义匹配才有明显优势。这个方案适合个人项目或MVP阶段零外部依赖部署时一个文件打包带走。4. 上线后必须处理的5个坑与排查实录代码写出来只是第一步真正折磨人的是上线之后。我根据自己踩过的坑整理了几个高发问题每个都附排查思路和解决建议建议直接收藏。4.1 坑1记忆召回了但全是废话我遇到过最典型的情况记忆库确实写入了大量条目search也顺利返回了但返回的是“用户说想要一台手机”“用户询问了发货时间”这种废话对于AI生成回答一点价值都没有。排查下来发现问题出在提取环节。add的时候如果直接把整段对话原封不动丢进去底层模型会把“用户当前正在问什么”也当成长期事实存下来。比如用户问“今天是否发货”就被存成“用户在问今天是否发货”下一次用户问“我的东西到哪里了”这条记忆完全没有参考价值。解决思路是调整存记忆的策略。第一个原则是只存“不受时间影响的偏好、状态、承诺、身份信息”不存“当次提问的内容”。第二个原则是可以自己写提取提示词让模型输出结构化结果比如请从对话中提取需要长期记住的用户信息关注 - 用户身份信息如所在地、会员等级 - 明确偏好如联系时间、配送偏好 - 对服务的不满或投诉 - 客服给出的承诺或补偿 输出JSON数组没有则输出空数组。如果你用的记忆库不支持自定义提取提示词那就退一步把系统统一要求转换文本格式再存。总之记忆质量取决于提取策略你喂给记忆库的东西决定它能给你什么。4.2 坑2上下文窗口被记忆塞爆另一个高发问题是Token失控。记忆库存了几个月后某个高活跃用户的记忆条目可能上百条search如果不限制数量全塞进prompt直接把模型上下文窗口撑爆或者把真正重要的内容挤到注意力之外。这里我建议做一个三层预算控制。第一层search的时候用limit参数限制召回条数比如最多5条超出部分不注入。第二层每条记忆在库里可以加一个优先级字段比如投诉和承诺类记2分普通偏好记1分排序时先按优先级再按相关性。第三层定一个上线前就会用到的“记忆长度预算”把注入记忆的总字符数限制在系统提示词的20%以内宁缺毋滥。我实测过很多用户其实不需要超过5条历史记忆就能回答好大部分问题真正关键的往往就是最近一两条。所以没必要贪多少而精才是对的。4.3 坑3用户隐私与敏感信息进了记忆库这是最容易被忽略但最严重的问题。如果用户说了一句“我电话是138xxxx地址是XX小区”你的记忆库很可能原样把这串手机号和地址存下来。一旦数据库泄露或者内部人员能直接查库这妥妥是重大事故。我的做法是在写入记忆之前加一道脱敏过滤。最简单的方案是维护一个正则列表把手机号、身份证号、银行卡号等模式替换成占位符。再严格一点可以在记忆库前面套一层模型调用用提示词让模型不抽取任何个人隐私信息只保留服务相关的偏好和承诺。注意脱敏要在写入端做而不是在读取端做。读取端脱敏只是显示层面隐藏数据库里仍然是明文等到发现的时候往往已经晚了。4.4 坑4记忆冲突用户说“我现在改地址了”用户记忆不是一成不变的今天说喜欢顺丰明天可能就因为快递点太远改成中通。如果记忆系统只会追加不会覆盖AI就会同时看到“偏好顺丰”和“偏好中通”两条矛盾记忆回答时容易摇摆。解决冲突有三个思路。一是为每条记忆加时间戳检索排序时让新记忆优先。二是在写入时做“同主题合并”比如记忆库判断新事实和旧事实相似度超过阈值就用新内容覆盖旧内容。三是给用户提供主动纠正入口比如客服界面加一个“用户要求修改备注”的按钮后台强制覆盖旧记忆。日常场景先把时间戳加上这基本零成本。覆盖策略等记忆量大了再上不要一上来就做复杂合并容易误伤。4.5 坑5调用成本悄悄涨上去记忆功能本质上多了一次存储调用和一次检索调用而且这些调用背后都可能是大模型在跑。如果每次客服对话都要调两三次模型一个月下来账单会明显上升。控制成本有个实用技巧没必要每条消息都做长期记忆提取只在关键节点存。比如检测到用户消息里包含“地址、电话、投诉、退款、偏好、承诺”等关键词就触发记忆写入普通闲聊直接跳过。检索也可以做缓存同一个用户在同一天内相同问法可以直接复用上次的检索结果不必每次都向量检索。我在生产里实测过加上“触发式写入”之后记忆相关的模型调用量下降了一半以上客服体验基本没变。这个优化强烈建议做。5. 从3行代码到一个能赚钱的客服系统记忆的工程化如果你只是做个Demo看到这里已经够了。但如果是上线迭代我建议一开始就把记忆模块当成一个独立服务来设计而不是散落在客服代码里的几个函数。这样后面换存储、加功能、做运营后台都方便。5.1 把记忆模块拆成独立服务记忆模块对外暴露三个接口就够了write写入、read读取、delete删除。具体是HTTP接口还是函数调用取决于你的架构。单体应用可以先抽成一个类微服务阶段再升级成独立API。接口设计上有个经验读接口一定要做两层过滤第一层按用户ID隔离第二层才做相关性排序。这意味着后端要先校验身份再查询记忆库不能把“所有用户记忆”当成一个大池子让模型随便捞。5.2 用户身份的统一与记忆共享客服系统里最麻烦的其实是“怎么识别同一个用户”。用户可能先以访客身份咨询再登录账号又绑定手机号这些都是不同ID。如果每个ID都单独开一份记忆用户登录之后历史记忆就全丢了。我建议业务层维护一份统一身份映射表把访客ID、登录ID、手机号、第三方OpenID关联到同一个内部用户主键上。记忆系统只认这个主键不做任何身份识别逻辑。这样无论用户从小程序、APP、网页还是电话进来都拿到同一份记忆。5.3 后续可以扩展的三种形态如果这期内容你消化得不错后面可以沿着三个方向扩展。第一是摘要型记忆定期把用户多轮对话压缩成一份“用户画像摘要”适合长期未活跃用户冷启动。第二是运营可视化给客服管理人员一个后台能看到“某个用户的记忆时间线”方便人工介入修正。第三是基于记忆的主动服务比如用户上次抱怨过“发票从来没开对”下次订单完成后系统主动询问“发票信息需要沿用上次备注吗”。这三个方向不需要推翻现有架构都是在当前记忆服务之上加一层业务逻辑而已。从一个“3行代码的记忆功能”起步慢慢进化成客服系统里最有价值的数据资产这条路我走了不止一遍每一步都有实实在在的收益。最后再说点个人体会。我一开始也觉得“记忆不就是把聊天记录存下来嘛”真上手才发现难点全在后半截什么该记、什么不该记、记了之后怎么在恰当时机把它想起来。这跟人脑一模一样——记性好的人很多懂得在什么场合想起来什么的人才是真正靠谱的。你现在用3行代码跑通的是“记得住”后面花心思打磨的是“想得起”后者才是用户体验的胜负手。希望这篇能帮你把第一步迈过去后面遇到具体问题欢迎随时沿着这四个动作去排查提取、存储、检索、注入。
返回列表