智能体长期记忆管理:Scope-Recall-Hermes架构解析与工程实践

📅 2026/8/17 2:55:14
智能体长期记忆管理:Scope-Recall-Hermes架构解析与工程实践
1. 项目概述Scope-Recall-Hermes 是什么最近在折腾AI智能体Agent的时候我发现一个挺普遍的问题很多智能体在处理长对话或者需要长期记忆的任务时表现得像个“金鱼”聊几句就忘了之前说过什么。这直接影响了任务的连贯性和用户体验。为了解决这个痛点我深入研究了几个开源项目最终把目光锁定在了410979729/scope-recall-hermes这个仓库上。简单来说Scope-Recall-Hermes 是一个为 Hermes 系列智能体设计的、专注于提升长期记忆与精准召回能力的记忆管理模块。它的核心价值在于不是简单地把所有对话历史都塞给模型而是像给你的智能体配备了一个智能的“记忆秘书”。这个秘书能理解对话的上下文和任务的范围Scope然后从海量的历史交互中精准地提取出与当前最相关的记忆片段喂给大语言模型LLM。这样一来智能体就能做出更连贯、更符合上下文的决策和回复。无论是构建一个能陪你聊几天几夜的聊天伴侣还是开发一个需要记住复杂用户偏好的任务型助手这个模块都提供了坚实的技术基础。它主要面向有一定Python和AI应用开发经验的开发者特别是那些正在基于Hermes、LangChain等框架构建复杂智能体的朋友。2. 核心架构与设计思路拆解2.1 为什么需要专门的“记忆召回”模块在深入代码之前我们先聊聊为什么单纯的对话历史记录不够用。假设你开发了一个旅游规划助手用户上周说“我喜欢安静的海边小镇”今天又问“推荐个适合度假的地方”。如果你只是把上周的整段对话扔给模型模型需要自己从中找出“安静”、“海边”、“小镇”这几个关键信息效率低且容易受到无关信息干扰。更复杂的情况是用户可能在不同时间点表达了看似矛盾实则情境不同的偏好比如工作日想要高效快捷周末想要慵懒放松。scope-recall-hermes的设计哲学就是解决上述问题。它将记忆管理拆解为几个核心步骤记忆的存储、记忆的索引、记忆的检索召回以及记忆的关联范围界定。其思路是结构化存储将每次交互的“记忆”不是存为纯文本而是转化为包含内容、时间戳、可能还有自定义元数据如对话场景、情感标签的结构化片段。高效索引利用向量数据库如LanceDB为这些记忆片段创建语义索引。简单理解就是把每段记忆的意思转换成一串数字向量意思相近的记忆其数字串也相似。精准召回当需要回忆时将当前的问题或上下文也转换成向量然后在向量数据库中快速找到语义上最相近的几段历史记忆。范围Scope控制这是项目的关键创新点。“Scope”可以理解为记忆检索的过滤器或上下文窗口。它可以根据对话主题、任务阶段、用户ID等维度限定只从某个“范围”内寻找记忆避免召回无关或过时的信息。比如只检索“关于旅行偏好”这个Scope下的记忆或者只检索“最近一周”的记忆。2.2 技术栈选型解析SQLite、LanceDB与Python的协同项目关键词提到了SQLite、LanceDB和Python这三者构成了项目的技术骨架。SQLite扮演“元数据管家”和“关系记录者”的角色。它不适合存储大量的向量数据但非常适合用来存储记忆片段的元信息比如记忆的唯一ID、创建时间。记忆所属的“Scope”如conversation_session_1,user_preference_travel。记忆的类型是用户输入、系统输出还是内部思考。记忆之间的关联关系例如记忆B是对记忆A的回应。 使用SQLite的好处是轻量、无需单独服务、事务支持好能快速地进行基于Scope、时间等条件的查询和过滤。LanceDB扮演“语义搜索引擎”的角色。它是一个高性能的向量数据库专门为AI应用设计。它的核心工作是存储由文本嵌入模型如OpenAI的text-embedding-ada-002或开源的BGE、SentenceTransformer生成的记忆向量。提供高效的近似最近邻搜索ANN根据当前查询向量毫秒级返回最相似的K条记忆。LanceDB支持磁盘存储易于部署并且与Python生态集成非常好非常适合作为智能体的嵌入式记忆检索引擎。Python作为“胶水语言”和“主控程序”。整个记忆模块的逻辑包括与SQLite和LanceDB的交互、Scope的逻辑处理、与上游Hermes智能体的接口对接全部由Python编写。Python丰富的AI库如langchain,chromadb,sentence-transformers也使得集成各种嵌入模型变得非常方便。选型理由这个组合在轻量级、高效能和开发便利性之间取得了很好的平衡。SQLite管理结构化关系LanceDB处理非结构化的语义搜索Python统筹全局。对于大多数中小型智能体应用这个架构完全足够避免了引入重型数据库如PostgreSQL pgvector的运维复杂度。3. 核心细节解析与实操要点3.1 记忆Memory的数据结构设计一个健壮的记忆系统首先依赖于良好的数据结构。在scope-recall-hermes中一段记忆Memory Item通常不会只是一个字符串。一个典型的设计可能包含以下字段class MemoryItem: def __init__(self, id: str, content: str, embedding: List[float], scope: str, timestamp: float, metadata: dict): self.id id # 唯一标识可以是UUID self.content content # 记忆的文本内容 self.embedding embedding # 内容对应的向量 self.scope scope # 所属范围如 “user_123/preferences” self.timestamp timestamp # 创建时间戳 self.metadata metadata # 扩展信息如 {“type”: “user_message”, “emotion”: “positive”}关键点解析scope字段这是实现精准召回的核心。你可以设计多级Scope例如用/分隔global/weather表示全局天气相关记忆user:alice/project:beta表示用户Alice在Beta项目中的记忆。检索时可以指定精确的Scope路径也可以进行前缀匹配。metadata字段这是一个灵活的字典用于存放任何有助于过滤和理解的附加信息。例如你可以在这里标记记忆的“重要性”分数或者在后续实现基于元数据的混合检索先按metadata过滤再向量搜索。向量生成embedding字段的生成是关键一步。你需要选择一个合适的文本嵌入模型。对于中文场景BGE或text2vec系列是不错的开源选择。确保所有记忆和查询都用同一个模型生成向量否则相似度计算会失效。注意在实际存储时MemoryItem对象会被拆开。id,content,scope,timestamp,metadata通常序列化为JSON字符串存入SQLite表。而id和对应的embedding向量则存入LanceDB表并通过id进行关联。这就是经典的“元数据向量”分离存储模式。3.2 召回Recall策略与算法有了存储下一步是如何“回忆”。单纯的向量相似度搜索语义搜索有时会召回相关但并非当前最急需的记忆。因此一个优秀的召回策略通常是多路混合的。基于Scope的过滤这是第一道也是最重要的过滤器。系统首先根据当前对话的上下文确定一个或多个目标Scope例如当前用户、当前活跃的任务模块。然后只在SQLite中查询属于这些Scope的记忆ID列表。这极大地缩小了搜索空间。语义向量检索将上一步得到的记忆ID列表对应的向量在LanceDB中进行限定范围的搜索。或者更常见的做法是先进行全局的向量搜索得到一组候选记忆ID再用Scope条件对这组ID进行过滤。时间衰减加权人类的记忆是有遗忘曲线的越近的记忆越清晰。我们可以在相似度得分上引入时间衰减因子。例如最终得分 语义相似度得分 * exp(-衰减系数 * 时间差)。这样即使一段记忆语义上高度相关但如果它发生在很久以前其排名也会下降。关键词增强可选对于某些明确的关键词查询如产品型号、特定人名可以结合传统的BM25等关键词匹配算法与向量搜索的结果进行融合如加权求和提升召回精度。实操心得在实际编码中召回策略的实现是一个调度器Recall Strategy。你可以定义不同的策略类比如SemanticRecallStrategy,TimeWeightedRecallStrategy,HybridRecallStrategy。智能体根据当前需求选择合适的策略或者使用一个策略管道按顺序执行过滤、搜索、重排等步骤。4. 实操过程与核心环节实现4.1 环境搭建与初始化假设我们基于Python来构建这个记忆模块。首先需要安装核心依赖。# 创建虚拟环境是良好的习惯 python -m venv venv source venv/bin/activate # Linux/Mac # venv\Scripts\activate # Windows # 安装核心库 pip install lancedb sentence-transformers # LanceDB和嵌入模型 # SQLite是Python标准库无需额外安装 pip install numpy # 用于处理向量数组 pip install pydantic # 可选用于数据验证和设置管理接下来初始化记忆系统。我们需要创建SQLite数据库表、LanceDB数据表并初始化嵌入模型。import sqlite3 import lancedb from sentence_transformers import SentenceTransformer import uuid import time class ScopeRecallMemorySystem: def __init__(self, sqlite_path: str “:memory:”, lancedb_path: str “./.lancedb”, embed_model_name: str “BAAI/bge-small-zh-v1.5”): # 1. 初始化SQLite连接和表 self.sqlite_conn sqlite3.connect(sqlite_path) self._init_sqlite_tables() # 2. 初始化LanceDB连接和表 self.db lancedb.connect(lancedb_path) self.table_name “memory_vectors” # 如果表不存在则创建表结构包含id和vector字段 try: self.table self.db.open_table(self.table_name) except: # 假设向量维度是384bge-small-zh的维度实际需根据模型确定 schema lancedb.schema([(“id”, lancedb.schema.string()), (“vector”, lancedb.schema.vector(384))]) self.table self.db.create_table(self.table_name, schemaschema) # 3. 加载嵌入模型 self.embed_model SentenceTransformer(embed_model_name) print(f“记忆系统初始化完成。模型: {embed_model_name}”) def _init_sqlite_tables(self): cursor self.sqlite_conn.cursor() cursor.execute(“”” CREATE TABLE IF NOT EXISTS memories ( id TEXT PRIMARY KEY, content TEXT NOT NULL, scope TEXT NOT NULL, timestamp REAL NOT NULL, metadata TEXT, -- 存储JSON字符串 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) “””) # 可以为scope和timestamp创建索引以加速查询 cursor.execute(“CREATE INDEX IF NOT EXISTS idx_scope ON memories(scope)”) cursor.execute(“CREATE INDEX IF NOT EXISTS idx_timestamp ON memories(timestamp)”) self.sqlite_conn.commit()4.2 记忆的存储与索引流程当智能体产生一段需要记住的交互时调用add_memory方法。def add_memory(self, content: str, scope: str, metadata: dict None): “”“添加一段记忆”“” memory_id str(uuid.uuid4()) timestamp time.time() # 1. 生成文本向量 # 注意embed_model.encode 返回的是numpy数组需转为list embedding self.embed_model.encode(content).tolist() # 2. 存储元数据到SQLite meta_str json.dumps(metadata) if metadata else “{}” cursor self.sqlite_conn.cursor() cursor.execute( “INSERT INTO memories (id, content, scope, timestamp, metadata) VALUES (?, ?, ?, ?, ?)”, (memory_id, content, scope, timestamp, meta_str) ) self.sqlite_conn.commit() # 3. 存储向量到LanceDB # LanceDB的add方法期望一个字典列表 data [{“id”: memory_id, “vector”: embedding}] self.table.add(data) print(f“记忆已添加ID: {memory_id}, Scope: {scope}”)关键操作解析原子性这里存在两个写操作SQLite和LanceDB。在严格的生产环境中需要考虑事务性确保两者要么都成功要么都失败。可以引入更复杂的逻辑或使用分布式事务的变通方案但对于很多应用即使出现部分失败也可以通过后台清理任务来修复。批处理如果遇到需要批量添加记忆的场景如历史数据导入应该将add操作改为批量进行LanceDB的add方法支持传入字典列表能显著提升性能。4.3 记忆的检索与召回实现这是模块的核心功能。我们实现一个基础的混合召回方法先按Scope过滤再进行向量搜索。def recall_memories(self, query: str, target_scope: str, limit: int 5): “”“从指定Scope中召回与查询最相关的记忆”“” # 1. 将查询文本转换为向量 query_embedding self.embed_model.encode(query).tolist() # 2. 从SQLite中获取目标Scope下的所有记忆ID如果记忆量巨大这里可能需要分页 cursor self.sqlite_conn.cursor() cursor.execute(“SELECT id FROM memories WHERE scope ? ORDER BY timestamp DESC”, (target_scope,)) scope_memory_ids [row[0] for row in cursor.fetchall()] if not scope_memory_ids: return [] # 该Scope下无记忆 # 3. 在LanceDB中限定在这些ID的向量中进行搜索 # LanceDB的search方法可以接受一个filter但这里我们采用先查后过滤的方式更清晰 # 注意实际使用中如果scope内记忆很多应使用LanceDB的ANN搜索并后过滤。 # 这里简化演示假设我们直接加载这些向量仅适用于小型数据集。 # 更优做法使用LanceDB的 where 条件进行过滤如果id是标量字段。 # 优化方案直接进行全局搜索然后过滤结果。这是更通用的做法。 results self.table.search(query_embedding).limit(limit * 3).to_list() # 多取一些结果 recalled_memories [] for r in results: if r[“id”] in scope_memory_ids: # 获取完整的记忆信息 cursor.execute(“SELECT content, timestamp, metadata FROM memories WHERE id ?”, (r[“id”],)) mem_data cursor.fetchone() if mem_data: recalled_memories.append({ “id”: r[“id”], “content”: mem_data[0], “timestamp”: mem_data[1], “metadata”: json.loads(mem_data[2]) if mem_data[2] else {}, “_distance”: r[“_distance”] # LanceDB返回的相似度距离 }) if len(recalled_memories) limit: break # 4. 按相似度距离排序距离越小越相似 recalled_memories.sort(keylambda x: x[“_distance”]) return recalled_memories[:limit]实现要点性能考量上述代码在召回时先进行了全局向量搜索。当总记忆量非常大百万级以上时即使使用ANN全局搜索也可能较慢。更高效的架构是为每个主要的Scope建立独立的LanceDB表或分区。这样检索时直接打开对应Scope的表进行搜索避免了全局扫描和后期过滤。结果融合这里只用了向量相似度排序。在实际应用中你应该将第3步扩展为一个“策略管道”可以依次或并行执行多种召回策略如关键词召回、时间加权召回然后将所有结果去重、打分、融合得到最终排序列表。5. 与Hermes智能体的集成实践5.1 作为Memory Provider接入Hermes智能体框架通常有一个“记忆提供者Memory Provider”的抽象接口。scope-recall-hermes模块的目标就是实现这样一个Provider。你需要创建一个类继承Hermes框架的BaseMemory类或实现其约定的接口。# 假设Hermes框架有一个BaseMemory类 from hermes.agent.memory import BaseMemory class ScopeRecallMemoryProvider(BaseMemory): def __init__(self, system: ScopeRecallMemorySystem, default_scope: str): self.memory_system system self.default_scope default_scope self.current_scope default_scope def set_scope(self, scope: str): “”“动态切换当前对话的Scope”“” self.current_scope scope def store(self, message: str, role: str “user”, **kwargs): “”“存储一条交互信息到记忆”“” metadata {“role”: role, **kwargs} self.memory_system.add_memory(contentmessage, scopeself.current_scope, metadatametadata) def recall(self, query: str, limit: int 5) - list: “”“根据当前查询召回相关记忆”“” return self.memory_system.recall_memories(query, self.current_scope, limit) def get_context(self, query: str, limit: int 5) - str: “”“将召回的记忆格式化为LLM可理解的上下文字符串”“” memories self.recall(query, limit) if not memories: return “” context_lines [“以下是相关的历史对话或信息”] for mem in memories: # 可以根据metadata中的role来格式化如“用户说...”、“系统回答...” role mem.get(“metadata”, {}).get(“role”, “unknown”) context_lines.append(f“{role}: {mem[‘content’]}”) return “\n”.join(context_lines)这样在你的Hermes智能体配置中就可以将ScopeRecallMemoryProvider实例作为记忆后端注入。智能体在每次需要生成回复前会调用get_context方法获取相关的历史记忆并将其作为系统提示词或上下文的一部分送给大语言模型。5.2 Scope的动态管理与生命周期Scope的管理是灵活性的关键。以下是一些常见的Scope管理策略会话级Scope每个对话会话一个唯一的Scope ID如session_uuid。这保证了不同对话之间的记忆隔离。用户级Scope每个用户一个Scope如user_user_id。用于存储用户的长期偏好和特征。任务级Scope每个独立任务一个Scope如task_task_id。用于存储与该任务相关的所有中间步骤和结果。混合Scope可以使用层级结构如user:alice/session:current检索时可以通过前缀匹配来灵活选择范围。在智能体运行过程中需要根据对话状态动态切换或组合Scope。例如# 当用户开始一个新任务时 memory_provider.set_scope(f“user_{user_id}/task_{new_task_id}”) # 当需要回忆用户的通用偏好时 memory_provider.set_scope(f“user_{user_id}/preferences”) # 或者进行跨Scope的回忆 scopes_to_search [f“user_{user_id}/preferences”, f“user_{user_id}/task_{current_task_id}”] # 需要修改recall方法以支持多Scope查询6. 常见问题与排查技巧实录在实际部署和测试scope-recall-hermes这类系统时我踩过不少坑这里总结几个典型问题和解决方法。6.1 召回结果不相关或质量差问题表现输入的查询明明有相关历史但召回的记忆风马牛不相及。排查步骤检查嵌入模型确认存储和查询使用的是同一个嵌入模型。模型更新后旧向量和新向量不兼容。如果是跨语言中英文混合确保模型是多语言或针对目标语言训练的。检查向量维度创建LanceDB表时指定的向量维度必须与嵌入模型输出的维度完全一致。bge-small-zh是384维text-embedding-ada-002是1536维弄错了会导致搜索完全失效。审视Scope过滤打印出target_scope和从SQLite查出的scope_memory_ids确认Scope逻辑是否正确是否意外过滤掉了所有记忆。查看原始相似度在recall_memories方法中打印出LanceDB返回的原始结果包括_distance和id然后手动检查这些ID对应的记忆内容是否真的与查询相关。如果不相关问题可能出在嵌入模型本身不适合你的领域考虑微调或更换模型。解决技巧在开发初期可以建立一个简单的测试集一组查询 期望召回的记忆。每次修改模型或代码后跑一遍测试确保召回精度没有下降。6.2 记忆存储或检索速度慢问题表现添加记忆或召回记忆时延迟明显影响智能体响应速度。排查步骤SQLite索引确保memories表在scope和timestamp字段上建立了索引。使用EXPLAIN QUERY PLAN命令分析你的查询语句。LanceDB搜索规模如果记忆总量很大10万确保使用了ANN索引。LanceDB在add数据时会自动创建索引但索引类型和参数会影响性能。检查是否在超大表上进行了全表扫描式的搜索。Scope分区策略如果某个Scope下的记忆数量巨大例如全局Scope检索速度会变慢。考虑按时间如每月一个表或按主题进行分区将大Scope拆分成多个小物理表。嵌入模型推理速度生成向量可能是瓶颈。考虑使用更轻量的模型如all-MiniLM-L6-v2或者对嵌入模型进行量化、使用GPU加速。解决技巧对于添加操作实现批量添加接口。对于检索操作实现异步或后台预加载。对于高频查询可以引入缓存层缓存最近或常用的查询结果。6.3 内存或磁盘占用过大问题表现随着运行时间增长数据库文件异常增大或程序内存占用过高。排查步骤向量数据膨胀LanceDB存储的向量是浮点数数组占用空间大。一个100万条384维向量的表占用空间约100万 * 384 * 4字节 ≈ 1.5GB。评估你的数据量是否合理。记忆无限增长没有设计记忆的遗忘或归档机制。所有记忆永久保存。SQLite日志文件SQLite的WALWrite-Ahead Logging模式会产生-wal和-shm文件在异常关闭时可能不会自动清理。检查目录下是否有此类文件堆积。解决技巧实施记忆淘汰策略为记忆设计“重要性”分数可在metadata中并定期清理低分记忆。或者为每个Scope设置记忆数量上限或总大小上限采用LRU最近最少使用策略进行淘汰。定期归档将旧的、不常访问的记忆导出到冷存储如压缩文件并从在线数据库中删除。清理SQLite空间定期对SQLite数据库执行VACUUM;命令以回收空间。对于LanceDB可以查看其版本管理功能清理旧版本数据。6.4 与上游智能体框架的兼容性问题问题表现记忆模块单独测试正常但接入Hermes等框架后无法工作或报错。排查步骤接口协议不匹配仔细阅读上游框架对Memory Provider的接口定义。方法名、参数、返回值类型是否完全一致例如框架可能要求store方法返回一个记忆ID或者recall方法返回特定格式的对象列表。异步调用冲突很多现代AI框架使用异步IOasyncio。确保你的记忆模块提供的方法是异步的async def或者框架支持同步调用。如果模块是同步的而框架在异步上下文中调用它可能会导致阻塞或错误。初始化时机检查记忆系统的初始化连接数据库、加载模型是否在框架启动的正确生命周期内完成。避免在第一个请求到来时才初始化造成延迟。解决技巧为你的ScopeRecallMemoryProvider编写适配器Adapter模式。创建一个符合上游框架接口的薄层内部调用你实现的核心逻辑。这样核心逻辑保持独立适配器负责处理兼容性细节。同时在项目README中明确说明兼容的框架版本和所需的配置步骤。