ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

为Claude构建本地长期记忆系统:向量检索与参数调优实践指南

为Claude构建本地长期记忆系统:向量检索与参数调优实践指南 1. 为什么要做 claude-mem从一个反复踩过的坑聊起如果你用 Claude 写代码或者做 Agent一定遇到过这种情况同一个项目头一天还聊得好好的第二天打开新会话它完全不记得你昨天给过它的架构约束。每次都要把需求重新复述一遍遇到复杂的业务规则复述过程中漏掉一两句后续回答就完全跑偏。claude-mem 这个项目就是为了解决这个问题用本地记忆库给 Claude 补上一个“长期记忆”让它在多个会话之间记住关键信息而不是每次都从零开始。这套思路技术上不算复杂核心就三步把过去对话里的关键内容抽取出来分块存储把当前问题向量化再从记忆库里检索最相关的内容作为提示补充给 Claude。但实际做下来我发现真正的难点根本不在“调 API”而在记忆的写入时机、分块粒度、相似度阈值这些不起眼的设计点上。差一点效果就是天壤之别。我的目标读者分两类一类是做 Claude 深度应用的开发者想把长期记忆接进自己的项目里另一类是玩个人助手、跑本地服务的进阶用户希望 Claude 能记住自己的偏好和习惯。这篇文章我把整个思路、代码、参数调节和踩坑实录都放出来你可以直接照着抄也可以按自己的场景改。提示本文涉及到的代码都是我在本地小范围验证过的方案不是生产级工业系统但足够你跑通一个完整的记忆闭环。生产环境要加并发锁、连接池和更严格的数据隔离这个后面会单独说。2. 记忆系统设计的三层结构与选型逻辑2.1 短期上下文窗口与长期记忆的边界开始写代码之前我先想清楚了一个问题Claude 本身不是没有记忆它的上下文窗口就是短期记忆你把内容放在系统提示和对话历史里它都能看到。但窗口有限而且一旦会话关闭下一轮对话就全部清空。所以要做“长期记忆”本质上是把信息从上下文窗口里挪出去存到外部用的时候再挑一部分放回窗口。我一开始想着把所有历史都塞回去结果第一次实验就发现不行。放了二十多轮的聊天记录进去Claude 的表现反而变差了因为无关信息太多把注意力全带偏了。后来我才意识到长期记忆应该扮演的是“笔记”角色而不是“录像回放”。每个会话结束时整理出几条关键结论存起来比原封不动地存聊天记录要干净得多。这也引出了一个核心设计记忆分两层一层是工作记忆当前上下文窗口一层是长期记忆外部存储两者之间靠“写入摘要”和“请求触发读取”来同步。体现到项目里就是每次对话进行到一定阶段我把对话历史交给 Claude让它自己总结出不超过 200 字的要点然后把要点向量化后保存。到下一次用户提问时先把问题向量化再从库里找出相关的旧要点拼装进上下文。这样短期窗口负责深度推理长期记忆负责提供背景各司其职。2.2 向量化、存储和检索三个核心选型记忆要能被搜索最省事的方案就是向量检索。把文本映射成一组浮点数相近语义的文本在向量空间里距离也接近。日常说“帮我找一下上次讨论过的缓存策略”向量检索能匹配到一个大意相似的旧总结而关键词搜索大概率会失败因为字面上可能一个词都对不上。选嵌入模型的时候我在两个方向之间犹豫云 API 还是本地模型。云 API 效果好、省内存但会把对话文本发到外部服务本地模型多占一点内存但隐私能兜住。对这个项目我最后选了本地方案用 sentence-transformers 加载一个轻量的中文模型。理由很简单claude-mem 本身定位是本地记忆库如果嵌入也走云 API就要多维护一套鉴权而且长期运行成本更高。实测下来轻量模型的准确率够用关键是延迟低单条文本十几毫秒就能出向量。存储方面我没有引入重型向量数据库。因为单机场景、几千条记忆SQLite 完全够用。数据量小的时候直接在内存里算余弦相似度也很稳。只有当记忆条目超过几万条才需要考虑 HNSW 这类近似最近邻索引。我的经验是先跑通 SQLite 加线性扫描满足不了性能了再迁移到真正的向量库不要一上来就为不存在的规模买单。2.3 为什么不用现成的记忆 SDK做记忆系统的时候市场上已经有几个商业化的记忆 SDK能帮应用记住用户画像、聊天记录。我认真看过功能确实全面接入成本也不高。但最终没有用原因是这类 SDK 把记忆管理的逻辑封装成了黑盒我控制不了“什么该记”“什么不该记”。而很多 Agent 场景恰恰需要细粒度控制比如某些对话内容完全不能写入记忆库某些记忆只能保留 24 小时这些定制需求在黑盒里做起来很别扭。另外商业 SDK 的数据通常存储在对方的服务端虽然方便了多端同步但对本地部署和纯内网项目来讲反而是减分项。数据留在自己手里这个需求比我预想的要硬得多。所以 claude-mem 从立项起就决定本地存储、逻辑可读、代码可改。哪怕牺牲掉一部分开箱即用的便利也值得。这套取舍思路我觉得比具体技术选型更有参考价值。3. 核心实现细节记忆的写入、组织与读取3.1 写入时机什么时候该把东西沉淀下来记忆写入是整个系统里最容易做砸的环节。最早的版本我图省事每收到一条用户消息就先存进数据库结果库里垃圾信息一堆。比如用户说“等一下”这种话也被当成记忆存了下来。后来我改成两个触发条件一个是会话结束或者长时间停顿后让 Claude 对整个会话做总结另一个是每轮回答结束后根据信息量决定要不要更新记忆。实际操作中我会重点判断三个点这段对话里有没有明确的事实性约定比如端口号、存储路径、接口返回格式有没有提出过可复用的方法论比如“这个模块建议用事件驱动而非轮询”有没有涉及用户偏好比如“输出尽量简短”“不要用专业术语”这三个判断如果写成规则会非常死板。我的做法是直接在总结提示词里告诉 Claude只记录事实、决策、偏好和待办事项忽略寒暄、过程性讨论和明显过时的信息。实测下来这个做法比单纯存聊天记录干净 80% 以上。坏处是多一次模型调用但相比换回来的记忆质量这点成本完全可以接受。3.2 分块策略与嵌入模型选择先过语言关把文本向量化之前要先切块这是最容易忽略的细节。我试过整段文本一次性嵌入效果很糟糕因为一段千字内容里包含多个主题向量会被平均成一个谁都不像的中间态。检索的时候常常匹配到无关的内容。最后我把单条记忆控制在 150 到 500 字之间超过 500 字就拆成多个小块每块尽量保持一个完整主题。这里有个语言层面的坑。Claude 的中文能力很强但中文文本做向量化的模型选择不太一样。我对比过几个通用英文嵌入模型在英文场景下表现很好一换成中文长文就开始出现“词不达意”的匹配。后来换成了针对中文优化的轻量模型匹配准确性明显提升。做这个项目的一个直观感受是嵌入模型和主模型是两回事主模型要智能嵌入模型要贴语言两者不能混为一谈。注意中文文本分块不要按字符数硬切尽量按句子或者语义段落切。用句号、感叹号、问号作为切分边界再根据字数量做合并。否则很容易把一句话从中间截断向量语义会受损。3.3 检索策略相似度阈值和 top-k 怎么调检索环节有三个参数需要调相似度阈值、返回条数 top-k、以及时间衰减。相似度阈值的作用是把明显无关的记忆挡在外面。我一开始阈值设得很低导致很多杂音记忆被塞进上下文。后来把阈值从 0.4 一路往上调到 0.55 左右才稳定低于这个值的记忆即使返回了Claude 用起来也只会添乱。top-k 决定最多返回几条记忆。我试过 3、5、8、10 几个值发现 5 条左右效果最好。太少信息量不够太多又挤占了上下文空间。而且 top-k 不是固定的在长任务场景里我会把它降到 3只保留最核心的背景在闲聊场景里会提到 8让上下文显得丰富一点。时间衰减是后加的。因为记忆库里既有昨天的讨论也有上个月的方案如果不加限制很可能检索出来的全是老旧的过期信息。我的策略是给每条记忆打上时间戳计算相关度时把时间的衰减系数乘上去。简单做法是最终得分 余弦相似度 - 时间衰减惩罚值。这样昨天的笔记会比三个月前的优先被选中。这个调整非常关键尤其在做持续迭代的项目时旧方案和新需求往往语义相近但方案内容已经完全不同。4. 实操过程从零搭一个可用的 claude-mem4.1 环境准备与依赖安装我建议直接在虚拟环境里操作避免污染系统 Python。项目用到的主要依赖有这几项pip install anthropic sentence-transformers numpysentence-transformers 会自动拉取 PyTorch安装包体积比较大但一次装完以后本地推理就很方便。如果机器配置比较低可以换用更小的嵌入模型。我这里用的是中文场景下常见的轻量模型比如 BAAI/bge-small-zh-v1.5量化后占用内存不到 1GB普通笔记本都能带得动。如果你只是想在命令行里快速体验不需要自己做接入也可以直接 pip 安装社区版本。但我个人建议项目初期自己动手写一遍核心逻辑这样出了问题自己心里有数后面好排查。4.2 核心代码骨架记忆库的读写与检索下面是 claude-mem 最简版本的核心逻辑。我不会贴一个超大工程而是把记忆库的读写、向量化和检索拆成三个逻辑块方便你对照理解。import sqlite3 import numpy as np from datetime import datetime def get_embedding(text: str) - list: from sentence_transformers import SentenceTransformer model SentenceTransformer(BAAI/bge-small-zh-v1.5) return model.encode(text).tolist() def cosine_similarity(a: list, b: list) - float: a np.array(a) b np.array(b) return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) 1e-8)) class ClaudeMem: def __init__(self, db_pathclaude_mem.db): self.conn sqlite3.connect(db_path) self._init_table() def _init_table(self): self.conn.execute( CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, embedding TEXT NOT NULL, session_id TEXT, created_at TEXT NOT NULL ) ) self.conn.commit() def save_memory(self, content: str, session_id: str): embedding get_embedding(content) created_at datetime.now().isoformat() self.conn.execute( INSERT INTO memories (content, embedding, session_id, created_at) VALUES (?, ?, ?, ?), (content, str(embedding), session_id, created_at) ) self.conn.commit() def search_memories(self, query: str, top_k: int 5, threshold: float 0.55): query_emb get_embedding(query) rows self.conn.execute( SELECT id, content, embedding, session_id, created_at FROM memories ).fetchall() scored [] for row in rows: emb np.array(eval(row[2])) score cosine_similarity(query_emb, emb) if score threshold: scored.append({id: row[0], content: row[1], score: score, created_at: row[4]}) scored.sort(keylambda x: x[score], reverseTrue) return scored[:top_k]这个实现刻意简化了时间衰减和会话隔离但基本闭环已经有了“保存记忆”和“检索记忆”两个接口都可用。实际用下来SQLite 表里几千条记录时线性扫描性能完全能接受返回时间在几十毫秒级别不会成为瓶颈。4.3 接入 Claude API如何把记忆注入对话记忆库和 Claude 的衔接是另一个关键环节。我采用的是“系统提示注入法”每次请求前先拿用户当前的 query 去检索相关记忆再把记忆内容拼进 system prompt 里。这样 Claude 相当于一边看旧笔记一边回答新问题不会前后矛盾。import anthropic client anthropic.Anthropic() def ask_with_memory(query: str, mem: ClaudeMem, session_id: str): results mem.search_memories(query, top_k5) memory_lines \n.join([f- {r[content]} for r in results]) system_prompt 你是用户的项目助理。以下是与当前问题相关的过往记忆\n memory_lines response client.messages.create( modelclaude-sonnet-4-5-20250929, max_tokens1024, systemsystem_prompt, messages[{role: user, content: query}] ) return response.content[0].text上面这段代码是最小的接入示例但我强烈建议你在实际项目中加一层“来源标注”。比如每条记忆前面标注“来自 3 月 14 日的会议讨论”这样 Claude 既能参考也知道记忆可能存在有效期不会盲目采信。这个细节看起来简单实际效果却很明显。不加来源时Claude 经常把旧记忆当成确定事实加了来源后回答措辞会更加谨慎比如“根据上周记录你倾向于这种方式”。4.4 对话结束后的记忆沉淀我还做了一个比较关键的功能对话结束后自动沉淀记忆。流程是先统计这轮对话中是否有值得记忆的内容再单独调用一次 Claude让它用规定格式输出结构化记忆。相比直接保存原始聊天记录这种做法可以显著减少噪声。def summarize_and_save(history: list, mem: ClaudeMem, session_id: str): summary_prompt ( 请根据以下对话历史提取需要长期记住的事实、决策、偏好和待办事项。 每条不超过200字只输出要点列表不要输出寒暄和过程性讨论。\n\n \n.join(history) ) response client.messages.create( modelclaude-sonnet-4-5-20250929, max_tokens512, messages[{role: user, content: summary_prompt}] ) points response.content[0].text.strip().split(\n) for point in points: if point.strip(): mem.save_memory(point.strip().lstrip(- ), session_id)这段代码跑了很久之后我意识到一个问题summary 的输出格式不稳定。有时候输出“- 事实……”这种带前缀的格式有时候是纯文本。我在保存前会把行首的多余符号清掉但更稳妥的做法是让模型输出 JSON 数组然后用 json.loads 解析。这样做可以避免格式兼容性问题也为后面做记忆合并和去重提供方便。5. 常见问题与排查技巧实录5.1 记忆被污染检索到大量无关内容这是我最常被问到的问题。现象是 Claude 回答问题时突然引入一些和当前话题毫无关联的背景导致答案变乱。排查下来十有八九是分块策略出了问题或者相似度阈值设得太低。我后来在项目里加了一个“记忆审计”入口把库里所有记忆按 id 和内容导出人工看一眼有没有坏数据。这个方法笨但有用。另一个有效的办法是给每条记忆加“来源会话”和“类型”字段在检索时按类型过滤比如当前是技术提问时只检索类型为“技术决策”的记忆。字段维度越多过滤越精准。5.2 记忆过多挤占上下文窗口随着时间推移记忆库越来越庞大检索返回的 top-k 条记忆拼接进上下文后可能导致总 token 数超标。我遇到过一次在极端情况下 error 提示上下文窗口溢出的情况。解决方案有三个方向限制 top-k、压缩记忆长度、以及在拼入 system prompt 前做一个总体 token 估算。我实现了一个很实用的函数把当前对话的整体 token 数估算出来再用预算上限减去已用 token剩余配额优先满足 Claude 的回答长度记忆部分只占剩下的 30% 左右。这个比例是我反复试出来的如果记忆占比太大Claude 会过度依赖旧信息回答显得僵硬如果太小又起不到记忆的作用。5.3 旧知识与新需求冲突另一个典型问题是旧知识和新需求打架。比如周一约定用 A 方案周五又决定改成 B 方案但库里两条记录都存在。检索时可能同时返回两条Claude 就出现了自我矛盾。我最终的解决办法是“记忆覆盖机制”写入新方案时给相关旧方案打上 deprecated 标记检索时默认排除已经废弃的记录。不过这个操作有个难点判断两条记忆是否相关在简单规则下很难做到准确。我现在的做法是在保存新记忆时先用新记忆本身去检索一次旧记忆把相似度高的旧记忆标记为“被替代”。这个方法不能说完全可靠但能解决大部分方案迭代引发的矛盾配合时间衰减使用效果更好。5.4 表格速查常见问题与排查方向我把实际碰到的几个问题整理成了一个速查表方便你在日常使用中快速定位。现象原因排查方向检索结果明显跑题分块太大或阈值过低检查文本是否被硬切上调阈值至 0.5 以上Claude 回答矛盾新旧记忆同时存在为记忆加版本或废弃标记回答太“背课文”记忆比重过高降低 top-k限制记忆 token 配额记忆库增长极快无筛选地保存一切改用 Claude 总结并结构化后再写入中文匹配不准嵌入模型不贴合中文换用中文优化的嵌入模型会话间记忆串味缺少 session_id 隔离给每条记忆加数据源字段检索时过滤5.5 存储安全与隐私隔离本地存储虽然有隐私优势但没有做权限控制的本地存储依然是隐患。我在项目里加了几条硬规则数据库文件默认放到用户目录下权限设为 700记忆内容不允许包含明文密钥和密码涉敏感的信息在写入前做脱敏。你可以通过一个简单的前置过滤列表把像“密钥”“密码”这类高危词直接拦截提示用户不要写入记忆库。考虑到多人共用一个服务端的情况我建议给每条记忆添加 owner 字段在检索时强制带上这个条件避免不同用户的数据互相污染。这个设计虽然只加一个 WHERE 条件但能避免大量线上事故。6. 写在最后的实践体会claude-mem 做下来我最深的感受是“记忆系统本质上是一个代码之外的工程问题”。模型的选择、参数的调整、存储方案的取舍这些都有规律可循但真正让一个记忆工具变得好用的是对数据质量的持续管理。如果你只写代码不沉淀记忆那它只是一个普通问答接口如果你把每一轮对话都无脑存下来它就会变成一个越来越大但越来越难用的垃圾场。这几周踩坑下来我形成了一个很稳定的工作流会话开始先检索缓存会话中先判断有无值得记录的决策会话结束后统一总结入库隔一段时间手动清理失效条目。这样整个系统的记忆质量始终保持在可用线之上。最后分享一个实用技巧定期给记忆库做一次压缩合并把当时拆分成多个 200 字小块的旧记录重新汇总成一条完整的阶段性总结。压缩后不仅检索更快Claude 读起来也会流畅很多。哪怕你完全复用我的代码这个习惯也值得先养起来。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表