为LLM构建外部记忆系统:原理、实现与RAG应用实践
1. 项目概述为LLM装上“记忆”的探索最近在折腾大语言模型应用开发的朋友估计都绕不开一个核心痛点上下文长度限制。无论是OpenAI的GPT系列还是开源的Llama、Qwen它们的“工作记忆”窗口都是有限的。这意味着当你和一个LLM进行长对话或者让它处理一份很长的文档时它很容易“忘记”开头说过的话导致前后矛盾、信息丢失体验大打折扣。这就像让一个只有7秒记忆的人来跟你讨论一本长篇小说结果可想而知。我关注的这个项目shohey1226/llm_memory其核心目标就是解决这个问题。它不是一个全新的模型而是一个精巧的“外挂”系统旨在为现有的LLM应用注入长期记忆能力。简单来说它试图为LLM构建一个外部的、可持久化、可检索的“记忆库”让模型在需要时能够回顾之前的对话历史或相关知识从而做出更连贯、更准确的回应。这对于构建复杂的对话机器人、个人知识助手、乃至游戏NPC等场景都有着至关重要的意义。这个项目的价值在于它提供了一种相对通用且可插拔的解决方案思路。开发者不必等待模型厂商推出上下文超长的昂贵版本而是可以通过集成这样的记忆模块在现有模型基础上显著提升应用在长上下文任务上的表现。接下来我们就深入拆解一下这样一个“记忆系统”究竟是如何被设计和实现的。2. 核心架构与设计哲学2.1 记忆系统的核心组件拆解一个有效的LLM记忆系统绝非简单地将所有历史对话文本存储起来那么简单。它需要解决几个关键问题记忆什么如何存储如何检索llm_memory项目的设计通常围绕以下几个核心组件展开记忆存储器这是系统的基石负责持久化保存记忆单元。常见的选择包括向量数据库如Chroma, Pinecone, Weaviate、关系型数据库如SQLite, PostgreSQL或简单的文件系统。向量数据库因其强大的语义检索能力而成为首选它允许系统根据当前对话的“语义”去查找相关的历史记忆而不仅仅是关键词匹配。记忆编码器这是将原始对话文本或信息转化为“记忆”的关键环节。通常这里会使用一个嵌入模型Embedding Model例如OpenAI的text-embedding-ada-002或开源的BGE、SentenceTransformers等。编码器将一段文本转化为一个高维向量即嵌入向量这个向量捕获了文本的语义信息并存入记忆存储器。记忆检索器当LLM需要回忆时检索器开始工作。它接收当前的对话上下文或用户问题同样通过编码器将其转化为查询向量然后在记忆存储器中进行相似度搜索如余弦相似度找出与当前语境最相关的若干条历史记忆。记忆处理器/聚合器检索到的记忆可能是多条零散的片段。直接将这些片段全部塞给LLM可能会再次耗尽上下文窗口。因此需要一个处理器来对检索结果进行筛选、去重、排序和摘要。例如只保留相似度最高的前K条或者使用另一个LLM对多条相关记忆进行概括总结生成一个精炼的“记忆摘要”后再提供给主模型。记忆更新策略这是决定系统“智能”程度的重要部分。记忆不是只增不减的需要有一套策略来决定何时创建新记忆、何时更新旧记忆、以及何时遗忘删除旧记忆。策略可以很简单比如每轮对话都保存也可以很复杂基于信息的重要性、时效性或相关性进行动态管理。llm_memory项目的实现本质上就是在用代码搭建和串联这些组件并定义它们之间的交互协议。2.2 两种主流记忆模式解析在实际应用中记忆系统通常会实现两种基础模式以适应不同场景对话记忆这是最直接的模式专注于记住同一会话中用户与AI的交互历史。其核心是维护一个不断增长的对话列表。但全量存储会快速消耗上下文窗口因此需要做“窗口化”或“摘要化”处理。滑动窗口只保留最近N轮对话。实现简单但会彻底遗忘窗口外的内容。摘要压缩定期如每5轮对话后使用LLM对之前的对话历史生成一个简短的摘要然后用这个摘要替代原始的多轮历史作为一条新的“概要记忆”存入长期存储。后续对话可以基于这个摘要和新的窗口历史进行。这种方式能在有限空间内保留更多历史信息的精髓。实体记忆这种模式更侧重于记住对话中出现的具体“事物”的属性与关系比如人物、地点、概念等。例如用户说“我喜欢吃芒果”系统可以创建或更新一个关于“用户”实体的记忆属性为“喜欢的水果芒果”。当用户后来问“有什么水果推荐吗”系统可以检索到“用户喜欢芒果”这条实体记忆从而给出个性化回答。这需要系统具备一定的信息抽取能力来识别和更新实体。一个成熟的记忆系统往往会结合这两种模式对话记忆保证流畅性实体记忆提升深度和个性化。注意记忆系统的设计永远是在“记忆力”、“上下文占用”和“计算开销”之间做权衡。一个记住一切细节的系统可能会因为检索到过多无关信息而干扰当前回答同时检索延迟也会很高。设计之初就要明确你的应用更看重哪一点。3. 关键技术与实现细节剖析3.1 向量检索记忆系统的“搜索引擎”向量检索是整个记忆系统高效运作的核心。其流程可以概括为“存向量” - “查向量”。当一段对话需要被记忆时系统会使用嵌入模型将其转换为一个固定长度的向量比如1536维然后连同原始文本一起存入向量数据库。向量数据库会为这些向量建立索引如HNSW、IVF以实现快速近似最近邻搜索。当需要进行回忆时系统将当前的查询语句例如用户的最新问题同样编码成向量然后向向量数据库发起查询“找出与这个查询向量最相似的K个向量”。数据库返回最相似的K条记录及其原始文本。这里的关键在于相似度度量。最常用的是余弦相似度它衡量的是两个向量在方向上的接近程度而不受其长度模长影响这对于文本语义比较非常合适。计算出的相似度分数0到1之间可以作为记忆相关性的置信度。# 一个简化的向量检索伪代码示例 import numpy as np from sentence_transformers import SentenceTransformer # 初始化编码模型 encoder SentenceTransformer(BAAI/bge-small-zh-v1.5) # 记忆入库 memory_texts [用户说他的家乡是北京。, 用户养了一只叫小花的猫。, 用户的工作是软件工程师。] memory_vectors encoder.encode(memory_texts) # 得到向量列表 # 此处应将 vectors 和对应的 texts 存入向量数据库如Chroma # 记忆检索 query 用户有宠物吗 query_vector encoder.encode(query) # 向向量数据库查询与 query_vector 最相似的向量 # 假设 db.search 返回相似度和文本 similarities, retrieved_texts vector_db.search(query_vector, top_k2) print(f查询: {query}) for score, text in zip(similarities, retrieved_texts): print(f 相关记忆 [相似度{score:.3f}]: {text}) # 可能输出 # 相关记忆 [相似度0.85]: 用户养了一只叫小花的猫。 # 相关记忆 [相似度0.23]: 用户的工作是软件工程师。3.2 记忆的生成与更新策略何时以及如何生成一条记忆是设计中的艺术。并非所有对话轮次都值得记忆。一些常见的策略包括基于重要性过滤可以使用一个LLM来判断当前对话内容是否包含值得长期记忆的信息。例如让LLM对当前语句打分1-10分只有高于阈值如7分的才存入长期记忆。这可以过滤掉“你好”、“谢谢”之类的寒暄。基于信息增量比较当前内容与已有记忆的相似度。如果当前内容提供了全新的、与已有记忆不重复的信息则创建新记忆如果是对已有记忆的补充或修正则更新原有记忆。定期摘要如前所述对于对话记忆可以设定一个触发机制如每5轮对话或当对话历史token数达到阈值时启动一个后台任务用LLM生成摘要并替换旧记忆。更新记忆时尤其是实体记忆可能会涉及更复杂的操作比如合并冲突的属性用户先说喜欢蓝色后说喜欢绿色可能需要记录“偏好颜色可能变化”或保留最新记录。3.3 与主LLM的集成方式记忆系统如何与主LLM如GPT-4协作主要有两种模式检索增强生成这是最主流的方式。在收到用户问题后系统先不从记忆库中检索相关记忆然后将这些记忆片段作为“参考材料”连同当前的用户问题一起构造成一个增强版的提示词Prompt发送给主LLM。例如你是一个助手拥有以下背景记忆 [记忆1]: 用户来自北京。 [记忆2]: 用户有一只叫小花的猫。 当前对话 用户推荐个周末活动。 助手LLM基于这些记忆和当前问题生成回复。这种方式对主模型透明无需修改模型本身。架构内集成一些更前沿的研究尝试将记忆机制设计成模型架构的一部分例如通过额外的记忆网络、外部注意力机制等。但这通常需要对模型本身进行修改或训练不属于llm_memory这类应用层项目的主要范畴。llm_memory通常采用第一种RAG模式。4. 实操构建从零搭建一个简易记忆系统4.1 环境准备与工具选型我们以Python环境为例构建一个最简单的、基于向量数据库的对话记忆系统。工具选择上我们追求轻量化和易用性嵌入模型选用SentenceTransformers库中的all-MiniLM-L6-v2模型。它体积小、速度快、效果不错且完全本地运行无需API密钥。向量数据库选用Chroma。它是一个轻量级、嵌入式的向量数据库可以直接用Python库操作无需单独部署服务器非常适合原型开发和中小型应用。主LLM为了示例完整我们使用OpenAI的GPT-3.5-turbo API需自行准备API KEY。你也可以替换为任何兼容OpenAI API的本地模型如通过Ollama部署的Llama 3。首先安装必要的库pip install sentence-transformers chromadb openai4.2 核心代码实现分步解析第一步初始化记忆库import chromadb from sentence_transformers import SentenceTransformer import openai import json # 初始化组件 openai.api_key your-api-key # 请替换为你的API Key embedder SentenceTransformer(all-MiniLM-L6-v2) chroma_client chromadb.PersistentClient(path./memory_db) # 数据持久化到本地目录 collection chroma_client.get_or_create_collection(nameconversation_memory)这里我们创建了一个持久化的Chroma客户端并建立了一个名为“conversation_memory”的集合相当于数据库的表来存储记忆。第二步定义记忆存储函数这个函数负责将一段有意义的对话文本存入记忆库。def save_memory(memory_text, metadataNone): 将一段文本作为记忆存储。 memory_text: 需要记忆的文本内容。 metadata: 可选的元数据如时间戳、对话轮次等。 if not memory_text or len(memory_text.strip()) 0: return # 生成文本的向量嵌入 embedding embedder.encode(memory_text).tolist() # 生成一个唯一ID这里简单用时间戳生产环境应用更健壮的方式 import uuid memory_id str(uuid.uuid4()) # 准备元数据 meta metadata or {} meta[text] memory_text # 将原始文本也存入元数据方便调试 # 存入Chroma collection.add( embeddings[embedding], documents[memory_text], # Chroma也可以存储原始文档 metadatas[meta], ids[memory_id] ) print(f[记忆已保存] ID: {memory_id}, Text: {memory_text[:50]}...)我们不仅存储了向量还把原始文本以documents和metadatas的形式存了一份这样检索后可以直接拿到可读的文本。第三步定义记忆检索函数这个函数根据当前查询从记忆库中找到最相关的记忆。def retrieve_memories(query_text, top_k3): 检索与查询相关的记忆。 query_text: 查询文本通常是当前用户问题或对话上下文。 top_k: 返回最相关的K条记忆。 # 生成查询向量 query_embedding embedder.encode(query_text).tolist() # 向Chroma发起查询 results collection.query( query_embeddings[query_embedding], n_resultstop_k ) # 解析结果 retrieved_memories [] if results[documents]: for doc, meta in zip(results[documents][0], results[metadatas][0]): retrieved_memories.append({ content: doc, metadata: meta }) return retrieved_memories第四步构建增强提示词并与LLM交互这是将记忆系统与LLM串联起来的关键步骤。def chat_with_memory(user_input, conversation_history[]): 带记忆的对话函数。 user_input: 用户当前输入。 conversation_history: 当前的会话历史用于滑动窗口。 # 1. 检索相关长期记忆 long_term_memories retrieve_memories(user_input, top_k2) memory_context if long_term_memories: memory_context 以下是你之前了解到的关于用户或当前对话的信息\n for i, mem in enumerate(long_term_memories): memory_context f[记忆{i1}] {mem[content]}\n # 2. 构建近期对话历史上下文滑动窗口例如最近4轮 recent_history conversation_history[-4:] if len(conversation_history) 4 else conversation_history history_context \n.join([f{role}: {text} for role, text in recent_history]) # 3. 构建最终的系统提示词 system_prompt f你是一个有帮助的助手能够参考长期记忆进行对话。 {memory_context} 当前对话历史 {history_context} 请根据以上信息回应用户的最新消息。 # 4. 调用OpenAI API messages [ {role: system, content: system_prompt}, {role: user, content: user_input} ] try: response openai.ChatCompletion.create( modelgpt-3.5-turbo, messagesmessages, temperature0.7, max_tokens500 ) assistant_reply response.choices[0].message.content # 5. 更新会话历史 conversation_history.append((user, user_input)) conversation_history.append((assistant, assistant_reply)) # 6. 可选判断当前对话是否值得存入长期记忆 # 这里简化处理如果用户输入包含明显的个人信息或重要陈述则保存 # 在实际项目中这里应该用一个更复杂的LLM调用或规则来判断 if 我叫 in user_input or 我喜欢 in user_input or 我有 in user_input: save_memory(user_input, metadata{type: user_fact, turn: len(conversation_history)}) return assistant_reply except Exception as e: return f调用API时出错{e}第五步运行一个简单的对话循环if __name__ __main__: history [] print(开始带记忆的对话输入退出结束) while True: user_msg input(\n你) if user_msg.lower() in [退出, exit, quit]: break reply chat_with_memory(user_msg, history) print(f助手{reply})这个简易系统已经具备了记忆的核心功能存储、检索和利用。当你告诉它“我叫小明”后这条信息会被存入向量库。后续你问“你知道我叫什么吗”检索函数会找到这条记忆并整合进提示词助手就能正确回答“你叫小明”。4.3 效果优化与高级功能拓展基础版本搭建完成后可以从以下几个方面进行优化记忆摘要实现一个summarize_memories函数定期将多条相关记忆用LLM概括成一条简洁的记忆。这能极大节省存储和上下文空间。记忆重要性评分在save_memory前加入一个LLM调用环节让模型判断“这段信息值得作为长期记忆吗”并给出理由或分数。元数据丰富化为每条记忆存储更丰富的元数据如创建时间、关联的实体人物、主题、记忆类型事实、偏好、计划、置信度等。这能让检索更精准例如可以按时间过滤过时记忆。多轮对话记忆触发不是每一轮都存而是检测到对话进入一个新主题或用户表达了完整观点后将之前几轮关于同一主题的对话打包生成一条记忆。前端界面可以构建一个简单的Web界面可视化地展示记忆库的内容、检索过程甚至允许用户手动编辑或删除记忆。5. 常见问题、挑战与优化策略在实际开发和测试中你会遇到一些典型问题。以下是我在实践中总结的“避坑指南”。5.1 检索质量不佳召回无关记忆或遗漏关键记忆这是最常见的问题。可能的原因和解决方案如下问题现象可能原因解决方案检索到的记忆完全不相关1. 嵌入模型不适合领域。2. 查询语句太短或模糊。3. 向量数据库索引参数需要调优。1.更换或微调嵌入模型对于中文场景可换用BAAI/bge系列对于专业领域可用领域数据微调模型。2.查询重写/扩展使用LLM将简短的用户问题扩展成更详细的、包含背景的查询语句。3.调整检索参数在Chroma中尝试不同的距离函数余弦相似度是默认、调整n_results数量。关键记忆检索不到1. 记忆存储时文本分块不合理。2. 记忆文本与查询文本表述差异大。1.优化文本分块如果记忆的是长文档不要简单按字数切分。应按语义段落、标题等进行智能分块。2.使用混合检索结合向量检索和传统关键词检索如BM25。先用关键词召回一批再用向量检索在其中精排兼顾语义和字面匹配。检索速度慢记忆库向量数量过多10万。1.使用更高效的索引确保向量数据库使用了HNSW等近似最近邻索引。2.硬件加速使用支持GPU的嵌入模型和向量数据库。3.记忆分级存储将高频访问的热记忆放在内存或SSD冷记忆归档。实操心得嵌入模型的质量是检索效果的“天花板”。如果预算允许在关键应用上使用付费的嵌入模型API如OpenAI的通常比小型开源模型效果有显著提升。同时查询构造至关重要。直接拿用户原句去搜效果往往不如将“当前用户问题 最近几轮对话历史”拼接起来作为查询语句去搜这样能提供更丰富的上下文给嵌入模型。5.2 记忆冲突与信息过载当记忆库中存储了相互矛盾的信息或者一次性检索到太多记忆片段时会干扰LLM生成质量。冲突解决可以在元数据中记录记忆的“来源”和“时间戳”。当检索到冲突信息时在提示词中明确告诉LLM“关于XX存在两条记录A时间T1和B时间T2。请根据最新信息或上下文判断。” 将决策权交给LLM。更复杂的系统可以实现一个置信度融合机制。信息过载这是RAG系统的通病。解决方案包括重排序检索到Top K条记忆后使用一个更精细的通常是交叉编码器模型对它们进行重新打分和排序只保留最相关的1-2条。记忆摘要如前所述这是终极武器。在存储前或检索后用一个LLM将多条相关记忆压缩成一条连贯的摘要。动态上下文窗口在构造提示词时优先放入高相关度的记忆如果token数快超限则舍弃低分记忆。5.3 长期运行的稳定性与性能对于需要长期运行的服务记忆系统需要关注记忆膨胀数据库会越来越大。需要制定“遗忘策略”。例如为每条记忆添加“最后访问时间”和“访问频率”定期清理长期未被访问的、低重要性的记忆。或者为记忆设置“有效期”。一致性在分布式部署中确保多个服务实例访问的是同一份记忆库需要引入中心化的向量数据库服务如Pinecone, Weaviate云服务或对嵌入式数据库如Chroma进行分布式改造。错误处理网络超时、嵌入模型调用失败、向量数据库异常等都需要有完善的重试和降级机制。例如检索失败时可以降级为仅使用最近的对话历史保证服务基本可用。构建一个健壮的LLM记忆系统是一个在准确性、性能、成本之间反复权衡和迭代的过程。从shohey1226/llm_memory这样的项目出发理解其核心思想然后根据自己应用的具体场景是开放域聊天还是垂直领域问答进行定制和深化才是正确的使用方式。它提供的不是开箱即用的终极解决方案而是一个极具启发性的起点和一套可组合的工具箱。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2585175.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!