智能体记忆系统搭建指南:OpenClaw三层记忆架构与向量检索实践

发布时间:2026/10/22 18:05:45
智能体记忆系统搭建指南:OpenClaw三层记忆架构与向量检索实践 你有没有遇到过这种情况智能体每次对话都要重新自我介绍用户一小时前提的需求转头就忘明明投喂了大量行业文档关键时刻一个细节都想不起来。我最早用OpenClaw做智能体项目的时候记忆这块完全是空白每次会话都是一次“失忆重启”用户问第二次同样的问题它仍然一脸茫然地重新开始。后来我花了不少时间把记忆系统彻底重做了一遍沉淀出一套可以复制、可以改、可以直接拿去用的搭建模板今天就把这套模板和踩过的坑一次性讲清楚。这套模板解决的核心问题很直接让OpenClaw具备三种记忆能力——会话内的短期记忆、跨会话的长期事实记忆、以及用于压缩上下文的摘要记忆。适合正在用OpenClaw做智能客服、知识助手、个人AI助理或者任何需要“记住用户”的场景的开发者参考。不管你是刚接触OpenClaw的小白还是已经跑过几个Demo想完善生产方案的开发者这套模板都能直接套用。1. 记忆系统的整体设计与模块划分1.1 记忆到底解决什么问题先明确一个概念记忆不是把聊天记录存下来那么简单。很多人一开始就把所有历史消息倒进向量库以为这样就“有记忆”了结果跑起来又慢又费钱回答质量反而更差。记忆系统真正的目标是在合适的时间、把合适的记忆、以合适的形态交给模型。打个比方人的大脑不会把所有经历都原封不动存着。你记得昨天中午吃了什么但不会记得三年前某天中午吃的什么你记得好朋友的生日但不会记得上周三他和你说过的每句话的每个字。智能体的记忆也应该是分层的重要的长期留存次要的短期保留过期的自动遗忘。OpenClaw作为智能体调度框架本身不自带成熟的记忆方案但它的插件机制和上下文管理接口给了我们自己做记忆系统的空间。我搭建的这套模板本质上是在OpenClaw的外部封装了一层记忆管理层负责拦截、存储、检索和注入记忆。1.2 三层记忆架构短期、长期、工作记忆我推荐的架构分三层每一层的职责单一清晰短期记忆Short-term Memory存放当前会话内的对话记录采用滑动窗口机制控制Token开销。长期记忆Long-term Memory存放跨会话的关键事实、用户偏好、历史知识采用向量库结构化存储双通道。工作记忆Working Memory每次请求时从短期和长期记忆中筛选出来的、注入到当前提示词的那部分内容。工作记忆是“最终被模型看到的内容”另外两层是“候选池”。所有记忆管理的核心逻辑都是在决定候选池里的哪些内容能进入工作记忆。1.3 模板目录结构与配置骨架这套模板的目录结构如下我建议你直接照抄openclaw-memory-template/ ├── config/ │ └── memory_config.yaml ├── memory/ │ ├── __init__.py │ ├── short_term.py │ ├── long_term.py │ ├── summary.py │ ├── working_memory.py │ └── storage/ │ ├── vector_store.py │ └── sqlite_store.py ├── utils/ │ ├── token_counter.py │ └── time_decay.py ├── tests/ │ └── test_memory.py └── main.py配置文件的骨架决定了整个记忆系统的行为边界我习惯把所有参数外置避免硬编码。下面这份配置是我目前在生产环境使用的版本每项参数的含义在后面章节逐一展开# memory_config.yaml short_term: max_tokens: 4000 # 短期记忆最大Token预算 window_size: 20 # 最大保留的消息轮数 summary_threshold: 3000 # 达到此Token数触发摘要压缩 long_term: vector_dim: 768 # 向量维度需与Embedding模型对齐 top_k: 5 # 默认检索条数 min_score: 0.45 # 相关性阈值低于此分不召回 time_decay_factor: 0.02 # 时间衰减因子 max_facts_per_user: 500 # 每个用户最多存储的结构化事实数 storage: vector_db: sqlite-vec # 可替换为其他向量库 sqlite_path: ./data/memory.db embedding_model: bge-small-zh # 中文场景推荐注意所有路径和模型名都要和你实际环境对齐向量维度不一致会导致存储故障这是最容易被忽视的问题之一。2. 核心模块短期会话记忆的实现2.1 滑动窗口与Token预算短期记忆的第一要务是控本。如果你的模型上下文窗口是8K对话历史全量塞进去再好的Prompt也救不回来模型要么截断要么报错。我的做法是设定一个Token预算比如4000预留一半空间给系统指令、检索到的长期记忆和用户当前输入。滑动窗口的逻辑很简单每次新消息进来把消息加入队列然后从最旧的消息开始淘汰直到总Token数降到预算以内。但这里有一个关键细节不能无脑按条数淘汰因为有的消息很长、有的很短。必须按Token数逐一减去而不是按条数硬切。具体实现时我封装了一个带Token计数的环形队列# memory/short_term.py from collections import deque class ShortTermMemory: def __init__(self, max_tokens: int, window_size: int): self.max_tokens max_tokens self.window_size window_size self.messages deque(maxlenwindow_size) self.token_count 0 def add_message(self, message: dict): msg_tokens estimate_tokens(message[content]) # 先按条数窗口淘汰 if len(self.messages) self.window_size: old self.messages.popleft() self.token_count - estimate_tokens(old[content]) self.messages.append(message) self.token_count msg_tokens # 再按Token预算淘汰 while self.token_count self.max_tokens and len(self.messages) 1: old self.messages.popleft() self.token_count - estimate_tokens(old[content]) def get_recent(self, max_tokens: int): 返回不超过max_tokens的最近消息 result [] tokens 0 for msg in reversed(self.messages): t estimate_tokens(msg[content]) if tokens t max_tokens: break result.append(msg) tokens t return list(reversed(result))这里有个值得注意的细节淘汰顺序要从最旧开始但返回时要返回最近的。deque的双端特性刚好满足这两个方向的需求比用列表手动管理索引干净得多。2.2 会话摘要如何触发当Token预算被占满且当前对话涉及重要信息不能简单丢掉时就需要摘要机制上场。摘要的本质是牺牲细节换容量把十轮对话压缩成一段概述释放出空间给后续对话。触发时机我建议设在前面配置的summary_threshold。每次写短期记忆时检查当前Token数超过阈值且消息条数达到窗口上限就触发摘要。摘要本身调用一次模型用专门的Prompt把既有消息压缩成“结构化摘要关键事实列表”然后把原始消息清空把摘要作为一条特殊消息存入短期记忆。摘要Prompt我踩过好几次坑最初用的是“请总结这段对话”结果模型经常丢关键信息。后来改成带约束的版本效果好很多SUMMARY_PROMPT 请将以下对话压缩为结构化的会话摘要要求 1. 保留用户的核心诉求、明确给出的偏好、未完成事项 2. 保留已确认的关键决策和具体细节时间、数量、名称等 3. 删除寒暄、重复、与主题无关的内容 4. 输出格式背景概述100字内 关键事实列表每行一条 对话内容 {conversation} 摘要不仅要给模型看也要给检索用。所以摘要生成后我同时写入长期记忆的向量库这样跨会话也可以检索到“之前聊过什么”弥补短期记忆只有本会话可见的局限。2.3 短期记忆的持久化设计短期记忆默认在内存里进程一退就没了。如果只是Demo无所谓但生产环境必须落盘。我的做法是把短期记忆的每条消息同时写进SQLite表结构带session_id、role、content、created_at重启后通过session_id恢复。这里有三个字段强烈建议加索引session_id、created_at以及两者组成的联合索引。不要觉得数据量小无所谓我遇到过会话数到几万条之后恢复会话时查询直接超时的情况加索引之后从几百毫秒降到个位数毫秒。3. 长期记忆向量存储与混合检索3.1 向量库选型与对比长期记忆的核心是向量检索。向量库的选型决定了你能支撑的用户量级和查询延迟。我按场景分成三档场景推荐方案优点缺点个人项目/原型SQLite sqlite-vec零部署、文件即库、随应用打包不支持分布式容量有限中小团队/几十万条记忆独立向量库服务延迟稳定、支持过滤、生态成熟需要运维多一个依赖大规模生产/千万级记忆分布式向量数据库高可用、水平扩展、混合索引成本高部署复杂我个人最常用的是“本地SQLitevec”起步用脚本压测发现瓶颈后再迁移。过早引入分布式组件只会拖慢迭代速度这跟“先用手写记账本账多了再换财务系统”是一个道理。选型时还有一个容易忽略的点向量库和Embedding模型必须匹配。有些向量库对向量归一化有要求有些要求指定距离算法。如果你把OpenAI的Embedding结果塞进为Cosine距离优化的库问题不大但如果库内部默认L2距离而你用的是不归一化的向量召回质量会明显下降。解决方案是入库前统一做L2归一化并显式声明距离算法。3.2 记忆写入切分、向量化、元数据长期记忆不是把整段对话原样丢进向量库那会稀释检索精度。写入前必须做三件事切分、向量化、附加元数据。切分方面我按语义段落切而不是按固定字符数硬切。固定切分会把一个完整事实劈成两半检索时哪一半都匹配不上。我的做法是用标点符号和换行做锚点段落到250字左右收束到最近的句号。这样每个chunk都是一个语义完整的单元。向量化方面中文场景我目前用bge-small-zh兼顾效果和速度。维度768单条文本在CPU上向量化大约几毫秒算力需求不高。如果你面向英文为主可以考虑同类英文模型。值得警惕的是不要在写入和检索时用不同的Embedding模型哪怕只是版本升级也要做一次全量重新向量化否则检索结果会“飘”。我吃过这个亏升级模型后忘了重刷存量数据线上召回率掉了一半。写入时元数据比向量本身更重要因为元数据决定你能做到多精确的过滤。我每条记忆记录包含以下字段memory_entry { id: mem_xxxxx, user_id: user_001, session_id: session_002, type: fact|preference|behavior|summary, # 记忆类型 content: 用户提到他对价格敏感优先推荐性价比路线, embedding: [0.01, 0.02, ...], # 768维向量 importance: 3, # 重要度 1-5 access_count: 0, # 被访问次数 created_at: 1708230000, last_accessed_at: 1708230000 }importance和access_count是后面做时间衰减和淘汰策略的关键输入。如果没有这两个字段所有记忆在系统看来价值相等那就根本无法做“遗忘”。3.3 混合检索相似度 关键词 时间衰减向量检索解决“语义相似”的问题但单靠它远远不够。举一个我实际遇到的例子用户问“上次你说那个便宜又耐用的方案”向量检索可以召回包含“便宜”“耐用”的对话片段但如果库里相似内容很多排序就会失准。这时候需要关键词匹配和时间衰减来辅助重排。我的做法是三路召回加一套加权融合向量召回按余弦相似度取Top 20。关键词召回对用户问题做分词提取关键名词从记忆表中做倒排匹配取Top 20。最近活跃召回取该用户最近3天内产生的高重要度记忆不管相关性如何先捞回来。融合公式我经过多轮实验调整参数目前稳定在下面的形态final_score 0.55 * vector_score 0.30 * keyword_score 0.15 * recency_score其中recency_score由最后访问时间和时间衰减因子计算。衰减函数我用的是指数衰减越久没被访问分越低。这模拟了人的遗忘曲线长时间不用的记忆即便相关也不应该占据靠前位置。这里有一个心得min_score阈值一定要设。如果没有阈值拿一个完全不相关的问题去检索系统也会硬把最相似的一条拿出来注入上下文导致模型被无关记忆干扰。我把默认阈值设为0.45实际使用中这个值在0.4到0.5之间比较平衡太低噪声太多太高容易漏召回。4. 结构化记忆用户画像与事实库4.1 动态抽取用户偏好光有向量记忆还不够因为向量适合“模糊回忆”不适合“精确查证”。比如用户说“我住南山”你需要在调用某个服务时精确知道地址靠向量检索可能召回一段关于南山的闲聊但拿不到准确的“居住地南山”这个事实。所以要单独建一张结构化事实表专门存这种“一句话能说清、且不容混淆”的事实。抽取环节我采用“事后抽取”策略每一轮对话结束后调一次轻量模型用固定Prompt从对话中抽取结构化事实。注意不是每句话都要抽也不是整个会话结束后再抽而是每个对话回合结束后增量抽取避免上下文太长导致抽取质量下降。抽取的Prompt我经过几轮调整核心约束是“只抽稳定的、跨会话有用的事实不抽临时状态”FACT_EXTRACT_PROMPT 从以下对话中抽取用户的关键事实包括 - 用户提供的身份信息姓名、职业、所在地等 - 用户的明确偏好价格敏感度、风格取向、沟通习惯 - 用户当前正在进行的长期事务项目、计划、周期性任务 规则 1. 只抽取明确陈述或强烈暗示的内容不做过度推断 2. 删除一次性临时信息如今天天气、当前心情 3. 冲突时保留最新陈述 4. 输出JSON数组格式[{type: preference, fact: 用户对价格敏感, certainty: 0.9}] 对话内容 {conversation} 4.2 事实库的更新与冲突消解事实库最大的坑是冲突。用户第一轮说“我在上海”第三轮说“我下个月搬到北京”。如果两句话都存进去模型就不知道用户到底在哪。解决办法是“同主键覆盖”将事实按(user_id, fact_type, attribute)做唯一键新事实写入时检查同键旧事实若存在且certainty不低于旧值则覆盖若新事实certainty很低比如用户只是随口一说则保留旧值并记录疑点。这个规则听起来简单但实际工程中要加上冲突检测提示。我在抽取模型返回时加了字段replaces让模型显式指出它认为哪条旧事实应该被替换。这样冲突消解从“写时被动处理”变成“模型主动声明”准确率提升非常明显。4.3 记忆回写与过期清理策略记忆系统不能只进不出否则会无限膨胀。我在配置里设了max_facts_per_user: 500超过上限后按重要度和最近访问时间排序把低重要度且长期未访问的记录归档到冷存储表不再参与在线检索。同时还有一套定期清理任务每周跑一次超过180天未访问且重要度低于3的事实直接删除。向量库中对应的向量一并删除不然会产生“有向量无事实”的孤儿记录。摘要记忆保留最近30天的更早的只保留按周聚合的周摘要。这套策略是跟一个做推荐系统的朋友聊天时受到的启发没有遗忘机制的记忆系统就像一个不打扫的房间时间越久越是寸步难行。别怕丢数据有价值的记忆会被重复访问自然留存长期不用的记忆丢掉成本其实很低。5. 实际操作从零搭建一套记忆系统模板5.1 环境准备与依赖安装开始之前先把基础环境准备好。我是在Linux服务器上跑Python 3.10虚拟环境隔离。核心依赖就四个OpenClaw框架本体、向量存储、SQLite驱动、Embedding模型加载库。# 创建虚拟环境 python3 -m venv venv source venv/bin/activate # 安装核心依赖 pip install openclaw pip install sqlite-vec pip install sentence-transformers # 验证安装 python -c import openclaw, sqlite_vec, sentence_transformers; print(ok)如果你的网络环境拉取模型困难提前把Embedding模型下载到本地目录然后通过环境变量指定模型路径避免每次启动都检查下载。这一点在受限网络环境下尤为重要。5.2 OpenClaw接入配置OpenClaw的接入配置是记忆系统能否真正生效的关键。需要在OpenClaw的配置文件中把记忆模块注册为中间件并指定上下文的注入方式# openclaw_config.yaml middlewares: - name: memory_middleware module: memory.middleware config: short_term_max_tokens: 4000 long_term_top_k: 5 working_memory_budget: 1500 context: inject_memory: true memory_position: before_user_input system_prefix: | 【历史记忆】 {memory_block} 【当前输入】memory_position我建议放在用户输入之前、系统指令之后。原因很实际模型对越靠前的内容注意力越强核心系统指令必须占据最前位置记忆块紧跟其后这样记忆的指令遵循度最高。放在系统指令前面会削弱系统指令的约束力这个顺序别搞反。5.3 核心代码实现记忆管理中间件记忆系统的核心是一个中间件它拦截每一次对话请求走“读取工作记忆 → 注入上下文 → 响应后写入新记忆”的流程。这里我给出一版可以运行的简版实现# memory/middleware.py import time from .short_term import ShortTermMemory from .long_term import LongTermMemory from .summary import generate_summary class MemoryMiddleware: def __init__(self, config): self.config config self.short_term ShortTermMemory( max_tokensconfig[short_term_max_tokens], window_sizeconfig.get(window_size, 20) ) self.long_term LongTermMemory( top_kconfig[long_term_top_k] ) def before_request(self, user_id, session_id, user_input): 请求前组装工作记忆并注入上下文 # 1. 从短期记忆中获取最近对话 recent self.short_term.get_recent(1500) # 2. 从长期记忆中检索相关事实 related self.long_term.search( queryuser_input, user_iduser_id, top_kself.config[long_term_top_k] ) # 3. 组装工作记忆块 memory_block if related: memory_block 【相关历史记忆】\n for mem in related: memory_block f- {mem[content]}\n if recent: memory_block 【最近对话】\n for msg in recent: memory_block f{msg[role]}: {msg[content]}\n return memory_block def after_response(self, user_id, session_id, user_input, response): 响应后写入短期记忆并异步抽取长期事实 # 1. 写入短期记忆 self.short_term.add_message({role: user, content: user_input}) self.short_term.add_message({role: assistant, content: response}) # 2. 短期记忆超阈值则生成摘要 if self.short_term.token_count self.config.get(summary_threshold, 3000): summary generate_summary(self.short_term.get_recent(20000)) self.short_term.reset_with_summary(summary) # 3. 异步抽取长期事实 self.long_term.extract_and_store(user_id, session_id, user_input, response)实际生产版本还需要把SQLite落盘、向量重排、并发锁这些细节补上但整体的流程骨架就是这样。把这个中间件注册到OpenClaw之后记忆系统就真正跑起来了。5.4 压测与效果验证搭好之后不要急着上线我建议做三轮验证。第一轮是功能验证连续对话20轮确认模型能记住中段提到的信息而不是只记得最近两条。第二轮是跨会话验证新开会话问“我之前提过一个需求……”确认长期记忆能召回相关内容。第三轮是压力验证模拟50个用户并发对话观察写入和检索的延迟。我的经验数据供参考SQLite本地向量方案在个人电脑上单条记忆写入约15ms检索Top 5约30ms50并发下延迟涨幅在2倍以内没有出现死锁或超时。如果你发现延迟暴涨优先排查是不是每次请求都重新加载了Embedding模型务必把模型常驻内存。6. 常见问题与排查技巧实录6.1 记忆失效模型还是“不认识你”症状明明存了用户的名字和偏好新会话一问三不知。排查步骤先查长期记忆表确认用户ID下是否有记录。有时问题出在用户ID传递不一致同一用户在Web端和API端可能拿到不同的匿名ID记忆当然对不上。再查检索逻辑。看min_score阈值是不是设得过高导致所有记忆都被过滤。我把0.45作为默认值但如果你用的是不同Embedding模型分数分布可能差异很大可以临时把阈值降到0.1观察召回情况。确认记忆块真的注入到了提示词中。OpenClaw有个调试模式可以打印最终发送给模型的完整提示词看memory_block是否为空。这三点检查完大部分“失忆”问题都能定位到原因。其中用户ID不一致是最隐蔽的也是我踩过最深的一个坑。6.2 向量检索召回不准症状召回的记忆和当前问题语义相关度不高甚至完全跑偏。我遇到过三次这种情况原因各不相同第一次是切分粒度太粗一段记忆包含多个主题检索时噪声大于信号第二次是Embedding模型和领域不匹配通用模型在专业领域表现平平第三次是缺乏关键词召回单纯靠向量扛不住精确术语匹配。解决路径也很清晰细化切分粒度250字左右、换领域匹配的Embedding模型、增加关键词召回通道。三者都做完召回质量会有质的提升。6.3 记忆膨胀与Token开销过大症状系统运行一周后响应越来越慢Token消耗越来越高。这个问题的根源通常是记忆写入太“贪婪”。我的排查经验是打开记忆明细按用户统计记忆条数和Token占用找出膨胀最严重的用户去看他的记忆类型分布。大概率会发现大量低价值记录比如“用户说好的谢谢”也被当成事实存了进去。根治办法是提高事实抽取的certainty阈值并在写入前加一道“价值判断”这条记忆如果不存三个月后用户问起模型能不能承受丢失的代价不能承受才值得存。这道题问下来能拦掉至少一半的垃圾记忆。6.4 多并发场景下的记忆串线症状A用户的问题模型回答中出现了B用户的信息。这是最严重的安全事故级故障。原因通常出在缓存和连接池没有按用户隔离。我排查过一起此类问题最终定位在向量库的连接复用时没有在每次查询前清空上一用户的过滤条件导致检索结果混入了其他用户的数据。修复办法很简单检索前强制重新设置user_id过滤参数并且在代码审查清单里加一条“所有记忆读写必须显式携带user_id禁止使用无过滤条件的默认查询”。这个清单项之后帮我避免了好几轮潜在事故。6.5 常见问题速查表症状可能原因排查优先级解决方案记忆完全失效用户ID不一致高统一会话身份标识记忆完全失效min_score过高高降低阈值或调模型召回不准切分粒度过粗中按语义段落切分召回不准Embedding不匹配中统一模型并重刷向量Token膨胀低价值记忆过多中提高certainty阈值并发串线缺少用户过滤器紧急强制显式user_id过滤这套速查表是我在维护记忆系统过程中一点一点沉淀下来的每次线上出问题先按表排查大多数时候能在五分钟内定位根因。最后再分享一个我的个人体会。当初设计这套模板的时候我追求的是“大而全”想把所有记忆能力一次性做进去。后来被现实教育了几次才明白记忆系统的核心不是功能多少而是边界是否清晰什么时候该记、什么时候该忘、什么记忆该给模型看比“记住一切”重要得多。这套模板我迭代了好几版从最初只能存会话记录到后来能主动维护用户画像每一版都在做减法。如果你也在搭类似的系统建议先跑通短期记忆和跨会话召回再加摘要、再上事实库一步步来别一口吃成胖子。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询