
我自己在本地搭过不少 Claude 相关的工具链先说一个每次都会被问到的痛点Claude 官方 API 是无状态的你上一轮告诉它“我的项目结构是 xxx”它下一轮就忘了。刚接触claude-mem这类项目的人多半是被“记忆”两个字吸引来的但真正动手做才发现这不仅仅是把 history 存下来再塞回去而是要把“对话记录”转成“结构化记忆”做到会话内连续、会话间可召回。这篇文章就围绕claude-mem的工程思路把记忆层的设计拆开讲清楚附带我实际搭建和踩坑的过程适合那些准备给 Claude 应用加长期记忆、又不想只做字符串拼接的开发者。1. 先搞清楚为什么 Claude 需要“记忆层”很多人的第一个想法是Claude 上下文窗口那么大把历史消息全拼进去不就行了短期确实可以但只要你做过真实业务场景的对话机器人很快就会发现这不是记忆而是搬运工的活。1.1 无状态 API 带来的真实问题Claude 的每次 API 调用都是独立的模型看到的只有你这次请求里携带的内容。这意味着两件很麻烦的事会话连续性问题用户昨天跟你聊了“我用的是 Django 4.2”今天新开一个会话问“帮我改一下 models.py”Claude 根本不知道 models.py 在哪个目录、依赖什么配置。如果你把它做成客服机器人用户每一次提问都要重新交代背景体验极度断裂。token 成本失控有人为了弥补无状态把全部历史对话一股脑塞进 system prompt。聊到 200 轮的时候光历史消息可能就占了 5 万 token每次请求都在给这笔历史账单付费而且容易把模型注意力稀释掉。claude-mem这类工具的定位就非常清晰它不是一个聊天 UI而是一个中间层接管“记忆”这件事。它帮你做三件事当前会话上下文管理、跨会话记忆沉淀、以及把记忆塞回下一次请求的 prompt 里。1.2 记忆层整体架构拆解我在本地跑通 claude-mem 之后把它的逻辑抽象成了一套通用架构大致是这几块模块职责类比Session Manager记录会话内消息、生成 session_id、控制上下文长度会议秘书负责记录每次谈话Memory Extractor用 LLM 从对话里抽取值得长期保留的事实会议纪要员提炼重点而不是逐字记录Vector Store把记忆片段向量化并存储支持相似度检索档案库按语义分类存放Retriever新对话开始时召回相关的旧记忆图书馆检索员按主题找出相关档案Cache Manager短期高频数据做缓存减少重复计算桌面备忘录随拿随用这套设计的核心逻辑是短期记忆靠上下文窗口长期记忆靠向量库二者必须分开管理。不至于因为长期记忆里堆了几千条碎片把每次请求的 prompt 撑爆。2. 核心机制从“存储消息”到“沉淀记忆”我最早犯过一个错以为把用户的每句话都存进数据库就算是“记忆”了。实际上逐字存消息是存储不是记忆。认真沉淀过的记忆应该是一份精简、可检索、能回答未来问题的事实库。2.1 会话历史短期记忆怎么管短期记忆要做到三步写入、裁剪、更新。写入每次用户和 Claude 对话后把 (session_id, role, content, timestamp) 写入会话表。这里建议用独立表不要和长期记忆表混在一起否则检索时会把闲聊内容也召回。裁剪当会话内消息超过一定 token 阈值时不是暴力删除而是做一次摘要折叠。比如把已经聊过 20 轮的排障过程浓缩成一条“已完成修复了数据库连接超时最终原因是没有配置连接池”把它放回上下文里同时丢弃原始 20 轮消息。更新会话结束时把最终摘要和关键结论写进长期记忆库。注意是“结束或阈值触发时”写入不要每轮都写否则向量库里全是你写到一半的思考过程噪音极大。如果你用的是 Claude 的 system prompt可以把“当前会话摘要”作为固定段放在最前面再放最近几轮原始消息。实测下来这个做法能让模型在长对话里持续跟踪主线不会聊到 50 轮之后把早期目标忘了。2.2 向量检索长期记忆怎么召回长期记忆如果还用“关键词搜索”就太粗糙了。比如用户之前提到“我用的是 psql 连不上”下次问“数据库又报错了”关键词匹配很难把“psql”和“数据库”关联起来。这个场景必须靠向量化召回。在 claude-mem 的流程里每一条沉淀的记忆是这样生成的先把一段对话交给 LLM让它提取用户偏好、项目背景、技术栈、明确结论并整理成一段流畅的陈述句然后调用 embedding 模型转成向量存入向量库。检索时把用户的当前问题也做 embedding再在库里做 top-k 相似度搜索。需要注意一个细节embedding 模型要固定。你不能今天用 openai 的 embedding明天换本地的 bge-m3因为不同模型生成的向量空间不兼容旧数据的向量对比新查询向量相似度会失真。这一点踩过的人很多属于换模型一时爽、召回火葬场。2.3 记忆写入与压缩策略记忆写入不能太勤快也不能太懒惰我的经验是分三个触发点轮次级触发每 5-10 轮对话后做一次增量记忆提取只抽取新增信息。质量级触发当对话中出现明确结论比如“就用 Redis 做缓存吧”、用户偏好“我不喜欢代码注释太啰嗦”、故障根因等信息立即标记为高优先级记忆。会话级触发会话结束或 token 超限时做一次全局压缩归档。压缩策略我用的是“层次化记忆”原始消息 → 会话内摘要 → 长期事实。每一层都会丢弃一部分细节换来的是一次请求里能携带更高质量的信息。刚开始别太自信建议压缩之前把原始记录留一份备份等跑了 3-5 个真实会话再对照着评估压缩质量。3. 实操记录我把 claude-mem 跑起来的过程下面这段就是我真实搭过的流程涉及主要模块的选型和核心代码逻辑。整体环境是 Python 3.11 FastAPI SQLite Chroma模型用的是 Claude 官方 API。3.1 环境准备与安装我本地用了一个独立的虚拟环境避免和全局包互相污染。安装完主要依赖后目录结构是这样的. ├── main.py # FastAPI 入口 ├── memory/ │ ├── extractor.py # LLM 记忆提取 │ ├── retriever.py # 向量召回 │ ├── session.py # 会话管理 │ ├── schemas.py │ └── store.py # 向量库封装 ├── config.py # 模型与数据库配置 └── requirements.txtrequirements 里最重要的几个包anthropicClaude SDK、chromadb本地向量库、openai如果 embedding 也走 OpenAI 风格接口。如果你不想用 OpenAI 家的 embedding可以用sentence-transformers配合本地模型把向量化这步也完全本地化成本更可控但检索质量要自己跑测试集评测。安装时我踩了一个坑Chroma 的版本和 Python 3.11 的某些组合在导入时会报sqlite3相关错误。解决办法就是把 chromadb 升到最新版并且确认系统 SQLite 版本不低于 3.35。如果你用 conda 环境建议先用conda install sqlite更新一下底层库。3.2 配置模型与存储config.py 里我维护了一份比较简单的配置import os ANTHROPIC_API_KEY os.getenv(ANTHROPIC_API_KEY) CLAUDE_MODEL claude-sonnet-4-5 EMBEDDING_MODEL text-embedding-3-small EMBEDDING_DIM 1536 # 向量库配置 CHROMA_PATH ./chroma_db COLLECTION_NAME claude_mem # 会话控制 MAX_CONTEXT_TOKENS 16000 SUMMARY_THRESHOLD_TOKENS 12000 TOP_K 5 SIMILARITY_THRESHOLD 0.72MAX_CONTEXT_TOKENS 这个参数很关键。它决定了每次请求给模型的上限不是越大越好。我是按照“固定摘要段 近期原始消息 召回记忆”三个部分加起来不超过这个值的思路来配。SIMILARITY_THRESHOLD是召回过滤的底线低于这个分数的记忆一律不注入 prompt宁可让模型说“不知道”也不能让它把不相关的记忆当成事实来回答。embedding 维度必须和向量库 collection 创建时一致否则插入数据时会报维度冲突。我一开始没注意本地换了 embedding 模型后旧 collection 直接没法写入最后删掉重建才解决。3.3 核心实现记忆提取与召回先说记忆提取。我用 Claude 来做这一步prompt 会设计成专门从对话里提炼“可复用的长期事实”而不是复述对话内容。大致逻辑如下from anthropic import Anthropic import json client Anthropic() def extract_memories(transcript: str) - list[str]: prompt f你是一名记忆分析助手。请从下面的对话中提取值得长期记住的事实。 要求 1. 只提取稳定的、跨会话有用的信息如用户偏好、项目技术栈、已确定的决策、事实背景。 2. 忽略寒暄、临时状态、与任务无关的闲聊。 3. 每条记忆用一句流畅的陈述句表达不要带前缀。 4. 以 JSON 数组返回。 对话内容 {transcript} 提取结果 resp client.messages.create( modelclaude-sonnet-4-5, max_tokens1000, messages[{role: user, content: prompt}], ) try: text resp.content[0].text return json.loads(text) except Exception as e: print(f提取失败: {e}) return []这里建议把返回格式约束成 JSON然后手动解析。Claude 偶尔会在 JSON 前后加说明文字解析时要做容错比如去掉 markdown 代码块标记。每一轮新会话开始时的召回逻辑是这样的先拿到用户的第一个问题做 embedding去向量库里检索 top-5 相关记忆再拼到 system prompt 末尾。这样模型不用每次从零认识用户而是带着“旧印象”进入新对话。3.4 写入链路什么时候把新记忆写进库不能每次对话都写否则记忆碎片非常多。我的流程是FastAPI 接口收到一次完整请求 → 构造消息发给 Claude → 拿到回复发给用户 → 异步把这一轮对话加入待处理队列。当队列累计的 token 超过了 SUMMARY_THRESHOLD_TOKENS或者用户主动调用 /memory/flush 接口就触发一次批量提取把新记忆写进向量库。这里有一个实际经验写入前要做去重。用户今天说“我用的 Linux 服务器是 CentOS 7”明天说“这台服务器 CentOS 7 跑不动了”如果不做去重库里有两条近似记忆召回时容易一起出现甚至让模型误以为用户在说两台服务器。我的做法是写入前先根据文本内容做一次相似度查询如果已存在相似度高于 0.95 的记忆则用新内容覆盖旧内容而不是新增。3.5 一次完整请求的代码骨架下面的代码是我 main.py 里的核心逻辑串联了召回、对话、记忆归档三步from fastapi import FastAPI, Request from memory.retriever import retrieve_memories from memory.session import save_session_message, should_summarize, summarize_and_flush from memory.store import add_memories from anthropic import Anthropic import asyncio app FastAPI() client Anthropic() app.post(/chat) async def chat(req: Request): data await req.json() user_msg data[message] session_id data.get(session_id, default) # 步骤1检索相关长期记忆 memories retrieve_memories(user_msg, top_k5) memory_block \n.join(f- {m} for m in memories) if memories else 无 # 步骤2构造带记忆的请求 system_prompt f你是用户长期使用的 AI 助手。以下是关于用户的长期记忆 {memory_block} 基于这些记忆回答问题但不要主动提及根据我的记忆之类的措辞。 response client.messages.create( modelclaude-sonnet-4-5, max_tokens1500, systemsystem_prompt, messages[ {role: user, content: user_msg} ], ) reply response.content[0].text # 步骤3异步保存会话并在阈值触发时归档记忆 asyncio.create_task(handle_post_processing(session_id, user_msg, reply)) return {reply: reply} async def handle_post_processing(session_id: str, user_msg: str, reply: str): save_session_message(session_id, user, user_msg) save_session_message(session_id, assistant, reply) if should_summarize(session_id): new_memories await summarize_and_flush(session_id) if new_memories: add_memories(session_id, new_memories)注意别漏了session_id的传递。很多人的记忆串台问题本质上是不同的用户会话混在了同一个 key 下面。我的方案是每个会话在启动时生成 session_id并将 session_id 作为向量库的 metadata 字段存进去所有记忆按用户维度隔离。召回时除了相似度过滤还会加where{session_owner: user_id}条件保证 A 用户永远不会看到 B 用户的记忆。4. 常见问题与避坑指南这部分是真实跑出来的教训。每一步我都踩过或者看同行的项目踩过整理出来能帮你省很多时间。4.1 上下文膨胀与“记忆注入过多”问题问题现象系统 prompt 里塞的召回记忆越来越多模型回答质量反而下降。这是因为召回的记忆里混入了“背景噪音”比如用户三个月前随口说的一句“我以后可能学 Go”在后面的无关对话里被反复召回模型甚至会基于这句话改变回答方向。解决思路严格设置相似度阈值只召回语义高度相关的内容不要为了“显得有记忆”而强行召回。每次召回后加一个过滤步骤让 LLM 判断这几条记忆和当前问题是否真的相关不相关就去掉。限制召回条数。我实测下来top-k 在 3-5 比较合适召回 10 条以上的时候上下文中事实冲突的概率会显著上升。4.2 向量召回质量差记忆“串味”表现用户问“怎么优化登录接口”召回出来的却是“用户说他的登录接口总是报超时”。看起来相关但其实是另一个会话里的旧问题时间线错位。排查方法确认 embedding 模型是否固定有没有中途换过。检查 chunk 切分是否合理。每条记忆写得太长会让向量表征被稀释太短则缺少语境。我试下来每条记忆控制在 30-80 字效果最好。在召回结果上加上时间衰减权重。最近 7 天的记忆权重为 1超过 30 天的权重按线性衰减到 0.6。这样旧记忆不会频繁霸占召回结果。4.3 记忆写得太快库里全是碎片这是我最早犯的错之一。每轮对话都调提取接口结果第二天看向量库里面全是“用户提到他喜欢喝咖啡”“用户今天项目中用了 Redis”这种低价值信息。碎片记忆一多召回命中率反而下降。正确做法是只在关键节点提取比如完成了某个多轮任务、用户做了明确决策、或者 token 阈值触发。另一条经验是给记忆加 type 分类偏好类、项目事实类、决策类、临时状态类。决策类和项目事实类优先级最高临时状态类默认 7 天自动过期删除。4.4 隐私与合规问题如果你把 claude-mem 用在真实产品里用户对话数据会被持久化到向量库。这不是一个纯技术问题而是一个合规问题。我的建议是默认不存储敏感信息。在提取记忆的 prompt 里明确要求忽略身份证号、银行卡号、家庭住址等隐私字段。提供“清除全部记忆”的接口用户随时可以一键删除自己的记忆数据。向量库里不要明文存用户原始消息只存 LLM 提取后的陈述性记忆。5. 一些实测数据和个人心得跑了一段时间的 claude-mem 之后我记录了以下几个关键数字供你参考指标不用记忆层用简单拼接历史用 claude-mem 记忆层用户背景信息需重复提供每次都要前几轮不用跨会话基本不用100 轮对话后平均 token 消耗稳定爆炸式增长稳定在设定阈值内跨会话准确回答旧问题完全不行有时可以稳定命中取决于阈值回答中被无关旧记忆干扰无干扰因为无记忆无干扰因为太占位偶发靠阈值和召回后过滤控制从这些数据能看出来记忆层的最大价值不是“看起来智能”而是解决两个实际问题减少用户重复描述背景的负担并防止上下文无限膨胀。它适合的典型场景是个人知识库助手、长期陪伴型对话应用、代码项目助手让 AI 记住你项目的目录习惯和依赖偏好以及客服系统的跨会话跟进。我在实际使用中最放心的一点是这种架构把“记忆”变成了显式的数据流可以审计、可以删除、可以调试。你可以打开向量库看看模型到底“记住”了什么而不是望着一个越滚越大的 system prompt 干瞪眼。最后分享一个小技巧为了调试方便我每次请求会在返回体里带上两个调试字段——recalled_memories和session_summary。前者是本次召回了哪些记忆后者是当前会话摘要。这样前端看不到但日志里能还原“模型为什么这么回答”排查问题时帮了大忙。如果你也打算在自己项目里做记忆层这个习惯建议从一开始就养成。