
在做 AI 客服助手这件事上很多开发者第一步就是把大模型接口接上输入问题、输出答案感觉相当简单。真正开始做之后才会发现问题远没那么轻松模型是“失忆”的同一个用户第二次来它完全不记得第一次说过什么知识库文档更新了它回答的还是旧内容多轮追问一多上下文塞满提示词成本蹭蹭往上涨。这个项目要解决的就是这几件事——用 LLM 负责生成回复用 RAG 从文档里实时检索业务知识再用向量数据库统一承载知识片段和用户记忆最终构建一个真正“带记忆”的 AI 客服助手。想入门 RAG、做私有知识库问答或者正在被客服机器人“答非所问”折磨的开发者这篇内容应该能帮你把整条链路跑通。1. 先搞清楚一件事客服助手为什么会“失忆”1.1 一次典型的“失忆现场”想象一下用户这样投诉“我五分钟前刚问过退款政策你当时说可以退现在怎么又说不可以了”普通大模型客服根本没有办法自己处理前后矛盾。原因是大模型接口是一个无状态函数你给它一段输入它生成一段输出它不维护任何“刚才发生了什么”的状态。用户说的上一句话对外层系统来说只是历史日志里的字符串模型本身并没有自动记住。解决这个问题的第一反应通常是把历史对话硬拼到提示词里再发给大模型。这样做能解决一部分问题但撑不了多久——对话轮数一多Token 数量就会爆炸既要塞历史又要塞系统指令还要塞用户当前的问题上下文长度很快见顶。而且历史记录里真正有用的信息没多少大段废话反而会干扰模型理解回答质量越来越差。1.2 客服助手真正需要的三类“记忆”我习惯把客服助手的记忆拆成三层分别对应不同的存储和处理方案。刚开始搭建的时候很多人混为一谈代码写到最后发现哪边都没管好。记忆类型内容例子特点推荐存储方式知识记忆产品手册、退换货政策、常见问题相对稳定更新频率低文档切块后转向量放向量数据库会话记忆当前这轮对话的用户说了什么、助手答了什么短时、强时效性内存缓存或缓存服务中的滑动窗口用户画像记忆用户偏好、历史订单信息、身份标签需跨会话持久化长期复用向量数据库按用户ID隔离存储知识记忆是“客服懂业务”的基础。RAG 解决的正是知识记忆的实时检索问题它不需要微调模型只需要把最新文档做切分、向量化、入库回答的时候检索相关片段送进提示词就行。会话记忆解决“上下文连贯”。一个简单的做法是在缓存服务里为每个会话保留最近 N 轮消息组装提示词时把最近几轮拼进去而不是把全部历史都塞进去。用户画像记忆解决“跨会话识别”。很多客服场景里用户隔几天再次咨询如果助手还记得上次的偏好和关键信息体验会完全不一样。这种记忆必须结构化地存入向量数据库并且要能按用户维度精确召回。这也是这个项目叫“带记忆的客服助手”的核心所在。三层记忆各归其位再配合一个统一的向量检索入口整体架构才会清晰。2. 架构设计LLM、RAG、向量数据库各管哪一块2.1 三个组件的边界与职责很多项目做毁了就是把三个组件的职责搞混了。明确边界是我做这类系统时的第一步。LLM大模型负责语言理解和生成。它做的是“读问题、看材料、写回答”但不负责记忆也不负责检索。它看到什么材料就基于什么材料回答。RAG检索增强生成负责从外部知识源中找到与当前问题最相关的内容。它解决的是大模型知识盲区问题让模型能基于你提供的最新文档回答而不是凭训练时的旧记忆瞎编。向量数据库负责把文本变成向量并提供相似度检索。知识片段、用户长期记忆、历史对话摘要都可以抽象成向量存入其中。它是整个系统的“记忆中枢”。用生活化的比喻来说LLM 像一个很聪明但容易忘事的员工RAG 像一个随叫随到的资料员向量数据库则是资料员身后的一大排档案柜。档案柜里的资料需要定期更新资料员负责按需取用员工只负责最后把资料整理成客户能听懂的话。2.2 为什么选 RAG 而不是微调模型不少朋友一上来就问“要不要微调模型微调过的模型是不是更懂我们业务”我的观点很直接优先做 RAG别一上来就微调。原因有三个。第一微调的投入产出比太低。微调需要准备大量标注好的业务问答对训练过程要调参还要反复验证效果而且每次知识更新都要重新训练一轮。相比之下RAG 只需要把新文档做切分、向量化、入库五分钟内知识就能生效。第二RAG 天然可追溯。客服场景里用户和合规部门经常会问“你这个回答的依据是什么”RAG 可以直接把命中的文档片段拿出来作为依据。微调模型的推理过程像一个黑盒很难向用户证明信息来源。第三RAG 更容易定位问题。回答错了能很快判断是“没检索到”还是“模型理解错了”。微调模型如果错了排查方向很模糊很难说清楚是哪一步出了问题。2.3 为什么向量数据库是“记忆中枢”知识片段是向量用户偏好记忆也是向量。把它们放在同一个向量数据库里好处非常明显。统一管理不需要为知识库和用户记忆各搭一套存储系统。向量数据库里可以用集合或命名空间把不同类型的向量隔开。灵活过滤向量数据库通常支持在相似度检索的同时加上标量过滤条件。比如召回用户偏好时可以只查某个用户ID下的记忆向量避免检索结果串到别的用户身上。这是做带记忆客服的关键能力。快速上线大部分向量数据库部署并不复杂本地跑一个轻量实例代码里封装好连接客户端后续换更重的集群方案也只是改配置的问题。在这个项目中我会把知识库和用户长期记忆分别放进两个不同的集合里。一个叫“知识库”一个叫“用户记忆”两者共用同一套向量检索逻辑但在写入和召回时通过集合名和元数据区分。3. 核心原理从文档切分到向量召回3.1 文档切分与 Embedding知识的存储单位不是整篇文章而是“块”。为什么因为向量检索的效率和精度都取决于检索单元的大小。把整篇几万字的文档转成一个向量召回时等于把整本书扔给模型既浪费 Token又容易抓不住重点。切分文档时我通常会按字符数或自然段落设置一个块大小范围。块太短信息不完整块太长噪声太大向量相似度会被稀释。比较稳妥的起点是每块 300 到 500 字块与块之间留 20 到 50 字的重叠尽量保证同一段语义内容不会被切断。切分完成之后每一块文本要通过文本向量化模型转成向量。这个过程专业说法叫 Embedding。文本向量化模型会把一段文字映射成一个几百维的浮点数数组。语义相近的文本对应的向量在空间里距离更近。后续检索时把用户的问题也转成向量然后去向量数据库里找最接近的向量就能找到语义匹配的内容。3.2 向量检索到底在做什么向量检索的核心是计算相似度。常见的是余弦相似度公式不复杂把两个向量归一化之后做点积结果越接近 1 表示方向越一致、语义越相近。举个例子。用户问“怎么退货”这个问题的向量大概率会落在“退款”“售后”“七天无理由”这些文本向量的附近。向量数据库会返回一个 Top N 的候选列表每个结果附带一个相似度分数。这里有一个特别容易被忽略的问题相似度分数是相对的不是绝对可靠的。同一个向量在不同批次数据里的相似度分布可能有差异所以不能只凭一个固定阈值判断结果可用。我一般会先看相似度最高的是否明显高于其他候选如果前几名分数差距不大说明问题比较泛可能需要换种问法或扩大召回数量。3.3 召回结果如何送进 Prompt检索到相关片段之后下一步是组装提示词。这一环直接影响回答质量。我先给一套基础模板实际项目里可以调整顺序和措辞。你是本平台的客服助手请根据提供的资料用简洁、友好的语气回答用户问题。 【业务资料】 - 售后政策... - 退货流程... 【用户画像记忆】 - 用户上次咨询过冰箱故障偏好文字沟通。 【近期对话】 - 用户我下单的冰箱可以退吗 - 客服请提供订单号和故障描述。 【当前问题】 请问退货需要哪些凭证 要求 1. 如果资料中没有明确说明不要编造可以说“稍等我帮您查询”。 2. 回答控制在200字以内。注意这里的关键是把“业务资料”、“用户画像记忆”、“近期对话”分块放在提示词里而不是一股脑拼接。大模型的注意力机制对结构清晰的内容更敏感明确的分隔符能帮助模型区分不同来源的信息。如果检索到的资料为空我通常会加一行“资料中没有相关信息”让模型宁可拒绝回答也不要胡说。这一步对生产环境来说非常重要。4. 实战搭建从零到能跑的最小系统4.1 环境准备与基础依赖这个最小系统用 Python 编写语言版本建议 3.10 以上。依赖方面包括三块大模型接口客户端、文本向量化客户端、向量数据库客户端。我把这些依赖抽象成几个通用模块在实际项目中你替换成自己选用的库即可。# 示例依赖安装命令请换成对应库的实际名称 pip install llm-client-sdk pip install embedding-model-sdk pip install vector-db-sdk环境准备的核心是确认三件事。第一大模型接口能够访问本地跑则确认显存或内存足够。第二向量化模型能够加载如果没有 GPU 小尺寸模型跑 CPU 也能接受。第三向量数据库启动成功并能通过客户端连接上。4.2 知识库入库流程知识库入库是整个系统最先要做的事。我一般会准备一个简单的文本目录每个文件对应一份产品文档或常见问题。入库脚本的核心逻辑如下。import os import uuid def load_docs(doc_dir): for filename in os.listdir(doc_dir): if filename.endswith(.txt): with open(os.path.join(doc_dir, filename), r, encodingutf-8) as f: yield filename, f.read() def split_text(text, chunk_size400, overlap50): chunks [] start 0 while start len(text): end start chunk_size chunks.append(text[start:end]) start end - overlap return chunks def build_knowledge_base(doc_dir): for filename, content in load_docs(doc_dir): chunks split_text(content) for index, chunk in enumerate(chunks): vector embedding_client.encode(chunk) vector_store.insert( collectionknowledge, vectorvector, metadata{ source: filename, chunk_index: index, text: chunk, }, )入库完成后建议做一个抽样验证。随便挑几个问题跑一遍检索看返回的片段是不是预期内容。这一步能尽早发现切分不合理、向量化模型不合适等问题。4.3 会话记忆与用户长期记忆的设计知识库建好后就要处理记忆部分。这里我把记忆拆成两段实现。短期记忆直接用进程内的字典或缓存服务保存最近几轮对话。逻辑很简单每个用户会话保留最近 6 条消息超过就丢弃最旧的。短期记忆不需要向量化因为它的作用是当前对话的上下文衔接。short_term_buffer {} def append_short_term(user_id, role, content): if user_id not in short_term_buffer: short_term_buffer[user_id] [] short_term_buffer[user_id].append({ role: role, content: content, }) # 只保留最近 6 条 short_term_buffer[user_id] short_term_buffer[user_id][-6:] def get_short_term(user_id): return short_term_buffer.get(user_id, [])长期记忆需要从对话里抽取关键信息并向量化。每次客服回答完调用一次提取逻辑。提取的粒度不能太粗。def extract_long_term_memory(user_id, conversation_text): summary_prompt f 请从下面的客服对话中抽取需要长期记住的用户信息。 只输出简短条目比如“用户偏好文字沟通”“用户家里养猫”。 不要输出对话过程不要推断没有出现的信息。 对话内容 {conversation_text} memory_text llm_client.chat(summary_prompt) memory_vector embedding_client.encode(memory_text) vector_store.insert( collectionuser_memory, vectormemory_vector, metadata{ user_id: user_id, memory_text: memory_text, created_at: get_current_time(), }, )这里有个细节长期记忆不能直接把用户原话原封不动存进去最好通过大模型做一次提炼。原因是原话里有大量噪声“我上次说了什么来着”这类句子存进向量库没有意义。提炼后的记忆条目更干净召回时效果也更好。4.4 客服主流程代码骨架整个系统的核心调用流程我整理成一个大致的伪代码骨架方便复现。def chat(user_id, user_message): # 1. 对用户问题向量化 query_vec embedding_client.encode(user_message) # 2. 从知识库检索相关业务资料 knowledge_hits vector_store.search( collectionknowledge, vectorquery_vec, top_k4, ) # 3. 从长期记忆中检索该用户的相关画像 memory_hits vector_store.search( collectionuser_memory, vectorquery_vec, top_k3, filter{user_id: user_id}, ) # 4. 拼装提示词 knowledge_context \n.join([hit[text] for hit in knowledge_hits]) memory_context \n.join([hit[memory_text] for hit in memory_hits]) short_context format_short_term(get_short_term(user_id)) prompt build_prompt( knowledge_contextknowledge_context, memory_contextmemory_context, short_contextshort_context, user_messageuser_message, ) # 5. 调用大模型生成回复 answer llm_client.chat(prompt) # 6. 写入短期记忆 append_short_term(user_id, user, user_message) append_short_term(user_id, assistant, answer) # 7. 异步抽取长期记忆也可以放到消息队列里处理 extract_long_term_memory(user_id, f用户{user_message}\n客服{answer}) return answer步骤顺序很重要。先检索再拼提示词先短期记忆再长期记忆每一步的输入输出都有清晰边界。我早期写代码时把短期记忆和长期记忆都写在一起结果调试时很难分辨问题出在哪个环节。拆开之后代码维护难度明显下降。5. 常见问题与排查技巧实录5.1 检索结果不相关这是 RAG 项目里最常遇到的坑。表现是用户问题很明确但向量数据库返回的内容和问题完全对不上。我通常按以下顺序排查。检查项可能原因处理办法文档切分文本被切得太碎语义断裂增加块大小或按段落切分向量化模型通用向量模型不擅长垂直领域术语换用领域语料微调过的向量模型召回数量TopK 太小正确答案被截掉先调大到 8 到 10 个观察结果相似度阈值阈值过低返回大量噪声提高阈值前先看召回结果的分数分布另外一个经常被忽略的点是切分时如果文件本身有标题、表格需要做特殊处理。表格内容被切成文字后语义会完全改变最好是先把表格转成一句一句可读的说明文字再参与切分。5.2 记忆串号用户A看到用户B的信息这种问题非常严重不只是技术错误还可能是合规事故。出现串号绝大多数原因是检索长期记忆时没有按用户ID做过滤。我在前面代码里特别加了filter{user_id: user_id}这行代码不能省。向量数据库的相似度检索是全局检索如果不加过滤条件查出来的必然是所有用户里最相近的记忆这和用户A的用户偏好可能毫无关系。上生产环境前一定要专门做一次“用户隔离测试”——用两个不同用户的偏好数据确认召回结果互不可见。我自己的习惯是长期记忆的元数据里永远带上user_id并且在封装检索函数时强制要求传入用户ID。这样即使以后代码重构也不容易漏掉隔离条件。5.3 上下文过长Token 超限客服助手跑一段时间后短期记忆虽然只保留 6 条但如果每轮回答都很长拼接起来还是会超限。解决办法有几个。第一限制每次大模型回答的长度比如在提示词里要求 200 字以内。第二短期记忆保留的不是原始消息而是当天对话的滚动摘要。每积累三五轮就让大模型把之前的对话浓缩成摘要再在需要时拼接摘要和最近一轮。第三超出上下文窗口时提供降级方案比如直接提示用户重新描述问题。5.4 延迟高、成本高客服场景对延迟很敏感。实测下来大模型调用往往是整个链路中最耗时的部分。流式输出是一个有效的优化手段用户不需要等完整回答首字返回后就能开始阅读。另外知识检索如果经常命中相同内容可以加一层结果缓存。比如相同问题在一段时间内直接复用上一次的回答。但这种缓存只适合标准问题不适合个性化问题使用时要根据业务场景平衡。5.5 安全与隐私做带记忆的客服助手记忆数据本身就是敏感资产。用户画像记忆里可能包含个人偏好、联系方式、订单信息。设计时至少要满足几个基本要求。按用户ID隔离存储和查询保证数据归属清晰。对用户记忆设置有效期过期后自动清理。提供用户主动删除记忆的接口。不要在记忆里存储身份证号、银行卡号、密码等高危信息如有需要走其他加密存储方案。这个话题在搜索引擎和社区里讨论很多但真正落地时大多数人还是在做系统时才发现自己漏了隔离和删除功能。6. 后续扩展和我的个人体会这个最小系统跑通之后可以往好几个方向扩展。我给知识库加过自动更新任务每天检测文档目录变化自动增量入库新文件也给短期记忆加过摘要压缩机制保证长会话不会爆上下文。如果客服场景更复杂还可以把“记忆”再细分比如增加订单记忆、商品浏览历史记忆用不同的集合或标签区分。我个人在实际搭建中的体会是与其一开始追求完美的架构不如先跑通一条最简单的链路再逐步加记忆、加规则、加优化。这个项目的难点不在单个技术而在把 LLM、RAG、向量数据库三个组件合理地串起来并想清楚每一层记忆该放在哪里。只要架构清晰后续换模型、换向量库、加功能都会顺畅很多。带记忆的客服助手本质上不是做一个“更聪明的机器人”而是把数据、知识和用户连接成一个可持续迭代的系统。