免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

Claude API 无状态对话怎么办?构建长期记忆层实战

Claude API 无状态对话怎么办?构建长期记忆层实战 直接说结论Claude 这类大模型 API 天生是“没记性”的——今天你跟它聊完项目方案明天它连你叫什么、当前做到哪一步、上回踩了什么坑全都不知道。每次调用都是从零开始这对做 AI 助手、Agent、个人知识库的人来说非常难受。claude-mem 这个项目就是在我自己的对话应用上补了一层“长期记忆”让模型在每次新对话里能自动“想起来”过去聊过什么。这篇文章就围绕这个记忆层的设计思路、核心实现、接入方式和踩坑记录展开适合正在用 Claude API 做应用、却被“无状态对话”折磨的人。1. 核心痛点拆解为什么大模型需要加一层记忆1.1 无状态的 API 和有状态的用户体验之间隔着一整层工程用 Claude API 开发过东西的人都有同感API 本身是纯无状态的。你传一段 messages 过去模型返回一段回复然后这段对话就结束了。服务端不会为任何用户保存聊天记录下一次你再传消息时它完全不记得上次的输入。这在技术上叫“无状态设计”它是 HTTP 接口最常见的形态但对做产品的人来说这几乎是不可接受的——因为真实用户的预期恰恰相反用户默认你“应该记得”之前说过的话。打个比方你在同一个窗口里找同一个客服聊事儿第一次问完进度第二天再问一次结果对方完全不记得你俩昨天说过什么要求你重新把问题描述一遍。这体验就很离谱。可如果你把“再问一次”之前的对话历史全部塞进请求里又会碰到两个硬问题token 越长成本越高长对话会话的 token 消耗会指数级膨胀模型上下文窗口有上限一次只能容纳几万到几十万个 token装不下无限增长的聊天记录。所以我需要做的第一件事就是搞明白“无状态”和“上下文窗口”的区别上下文窗口只是模型这一次回答时能看到的“工作台”而记忆是跨会话、跨时间保存下来的“笔记”。claude-mem 的定位就是这层“笔记系统”它插在应用和模型之间每次调用前自动去翻笔记把相关的历史信息捞出来塞进请求里。1.2 从“会话内”到“会话间”记忆要解决的三个层次最初我只想做“会话内记忆”也就是像很多聊天框那样把用户刚才几轮对话拼起来重新发给模型。做了几天后发现这只是最浅的一层真正有价值的是另外两类记忆会话级记忆这一轮对话里用户透露的细节比如“我家有三个房间正在装修”。用户级记忆跨多次会话沉淀下来的长期事实比如“用户住在杭州”、“用户做跨境电商”、“用户上次问了某产品的报价”。风格与偏好记忆用户希望回复简短、喜欢表格、不用敬语、更喜欢问原因而不是直接给结论。说个真实例子。我自己的知识库 bot 一开始只做“会话内拼接”用户问了三次“你们这个 API 的限流策略是啥”它每次都一模一样地回答。后来我意识到用户第二次问这个问题时其实是在表达“上次你说的我没看懂”或者“你上次漏了关键细节”而不是“我忘了你再讲一遍”。只有当系统记得“这个问题上次已经回答过”才会去追问“你是哪里没理解”而不是机械地复制答案。这就是跨会话记忆的价值它不只是提高回答连续性更是理解用户需求变化的基础。1.3 我的设计目标让记忆层“轻、快、可插拔”看了几个现成方案后我决定自己把 claude-mem 做成一个独立的小服务而不是塞进业务代码里。核心目标是三条轻不引入重型的分布式存储单机就能跑个人项目跑得动小团队也能驾驭快记忆检索必须在 200 毫秒内完成否则用户感知会非常明显可插拔不绑定特定框架不管是 FastAPI 写的接口、LangChain 搭的 agent还是简单的命令行脚本都能通过同一个 HTTP 接口或 Python SDK 接入。这个决定基于一个朴素的观察凡是要跟“模型调用”耦合得太深的中间层最后都会因为模型升级、框架换血而被废弃。记忆层是一个相对独立的横切关注点它的数据格式、存取逻辑、检索策略其实跟“用哪个模型”没有必然关系只是恰好要服务这个模型。所以我在架构上让它尽量独立只通过输入输出做对接。2. 核心技术拆解记忆的存储、检索与更新2.1 记忆不能“全部存原文”要先做结构化和压缩刚开始做的时候我踩了第一个大坑把对话原文一字不差地全部存进数据库。这个方法操作起来最简单但用起来全是问题。一是 token 复用率低原文冗余太多检索出来的内容经常是废话二是原文里有大量“嗯”“对吧”“然后呢”这类对话填充词检索向量化时它们会干扰语义三是隐私风险高所有敏感信息裸奔式地躺在库里。后来我改成“先摘要后存储”的策略。每一轮对话结束后用一个轻量的模型调用或者规则模板把这段对话压缩成几条结构化记忆。保留的字段大概是主体用户 / 项目 / 某个实体事实或偏好发生时间来源会话重要程度评分举个例子用户说“我下周三要去上海出差顺路想去看看你推荐的打印机型号”。这句话会被拆成两条记忆一条是“用户在 2025 年 6 月 18 日去上海出差”另一条是“用户对打印机型号有持续兴趣”。前者是临时日程后者是长期偏好两者以后被调用的频率和场景完全不同。2.2 检索不能靠关键词要用向量做语义召回记忆存进去了关键是怎么“找出来”。最天真的做法是数据库 LIKE 查询把包含“打印机”三个字的记忆全捞出来。但用户的问法千变万化今天说“打印机”明天说“办公设备”后天说“上次你给我推荐的那个机器”关键词匹配基本废掉。我采用的是标准的向量检索方案先给每条记忆做 Embedding嵌入向量把文字变成几百维的浮点数数组检索时把用户当前的问题也做 Embedding然后计算用户向量和历史记忆向量之间的余弦相似度取 Top-K 条相似度最高的记忆作为上下文。这里面有三个关键工程点选什么 Embedding 模型。我当时对比过几个常见方案结论是要么用专门的中文向量模型要么用一个效果稳定但速度更快的通用小模型。对个人项目来说模型的维度不一定要高但分词效果和对中文语义的判断力很重要。相似度阈值。刚开始我把阈值设得特别低0.5结果什么鸡毛蒜皮的记忆都被查出来注入到上下文里全是噪声。后来逐步调到 0.72 以上召回精度明显提升但过度调高又会漏掉有用记忆。这个阈值本质上是要在“召回率”和“精确率”之间取平衡没有绝对正确答案必须根据你自己的数据调。混合检索。纯向量检索有个通病就是它对实体名、日期、数字这类精确信息很不敏感。用户问“上周三说的那个”向量检索可能抓不住“上周三”这个时间点。所以我额外加了一条精确匹配通道凡是记忆里带时间、价格、型号、地点的标签字段都会走一遍结构化匹配再和向量召回结果做加权融合。2.3 记忆更新的“写入-衰减-遗忘”机制记忆不是写进去就永远不变的。用户会改主意、会搬家、会换工作如果你把一个半年前的陈旧信息当成最新事实Bot 就会在细节上显得很蠢。我设计了三级处理机制写入时去重用户重复表达同一件事时不新增记录而是更新旧记录的“更新时间”和“置信度”衰减每被查询或命中一次记忆的“活跃值”会上升长时间不被命中的记忆权重逐渐下降当低于某个阈值时会在检索结果中被过滤掉遗忘超过设定生命周期我默认设的是 180 天且权重很低的记忆周期性从活跃库搬到归档库不再参与检索。举个例子用户去年说“喜欢喝美式”今年某次提到“最近开始喝手冲”那“美式”那条记忆的置信度会被下调“手冲”的记忆会被新增出来。再过一阵如果用户再也没提过美式它的权重就一路降到检索不到了。这样做的本质是让记忆系统具备“人类或者说用户关系那套常识里的遗忘节奏”但又不彻底删除因为说不定哪天用户又会提起来。2.4 存储选型SQLite 起步向量库按需接入我第一版用的是纯 SQLite加上一个额外的向量索引。在个人项目、几千条记忆的量级下这套组合完全够用检索速度也能保持在百毫秒级别。直到我后来把数据规模做到几十万条才开始考虑把向量部分切到独立的向量数据库SQLite 只留下结构化字段。说实话对绝大多数“给 Claude 加记忆”的需求一上来就上分布式向量库是严重的过度设计。本地记忆文件加内存索引配合简单的缓存就能覆盖绝大多数场景。只有当你要做多人 Web 应用、需要横向扩容、或者打算支撑百万级记忆条目的时候才需要认真考虑独立的向量数据库服务。各层的数据流向大概是这样对话产生后先经过“摘要器”压缩成结构化的记忆条目条目进入 SQLite 做主存储同时生成向量放入向量索引新对话发起时带着用户 ID 和当前消息去检索检索结果经重排后拼装成 system 前缀随请求发给 Claude。3. 实操过程把 claude-mem 接进你的对话应用3.1 最小可用闭环记录、检索、注入我自己实际使用的 claude-mem 工作流本质上就是三个环节的循环。无论你用什么语言、什么框架最终要落地的都是这三件事。先把最小闭环跑通再谈优化。第一步在每次 API 调用结束后把用户消息和模型回复交给记忆层处理异步生成摘要并写入存储。第二步在新请求进来时先用当前用户消息做检索取得历史记忆。第三步把检索到的记忆和原始对话拼接起来作为本次请求的上下文发送给模型。下面是一个简化但完整可读的 Python 示例用的库和变量名是我自己的封装结构你换成自己项目的对象名就行import os import anthropic from claude_mem import MemoryStore store MemoryStore(db_path./memory.db, embedding_modeldefault) client anthropic.Anthropic(api_keyos.environ[ANTHROPIC_API_KEY]) def chat(user_id: str, message: str) - str: # 1. 从记忆库检索该用户的历史相关信息 memories store.search(user_id, message, top_k5, threshold0.72) # 2. 拼装系统提示词 memory_block \n.join([f- [{m[time]}] {m[content]} for m in memories]) system_prompt 你是一个乐于助人的助手。\n if memory_block: system_prompt 用户过去提到过这些信息\n memory_block # 3. 调用 Claude API response client.messages.create( modelclaude-sonnet-3-5, max_tokens1024, systemsystem_prompt, messages[{role: user, content: message}], ) # 4. 异步把本轮对话写入记忆 store.record(user_id, message, response.content[0].text) return response.content[0].text这个流程看起来简单但有个特别容易被忽略的地方一定要在“写入记忆”这一步做异步化处理也就是不要让用户等待摘要生成完才返回结果。对话的响应速度直接影响体验而记忆写入晚几秒完全没问题。我第一版是同步写入结果每次对话都要多等一两秒钟用户明显感觉变慢。后来改成后台任务异步写入体验立刻舒服了。3.2 关键参数的调优经验这个系统里有几个参数属于“看着不起眼、实际决定成败”的那种我直接给你可以抄作业的经验值。第一个是 Top-K 召回条数。我最初设成 10 条结果上下文中塞满了“曾经提过这个话题”的相似记忆很多信息其实是重复的模型反而容易抓错重点。后来我把 K 降至 5同时要求检索结果做一次“去重合并”把讲同一件事的不同记忆合并成一条。体感上准确度提升了至少三成。第二个是相似度阈值。我建议你做一个简单的标注集拿 50 条真实的用户问题人工标出“哪些记忆是应该被召回的”然后遍历阈值找 F1 最高的那个值。别拍脑袋定阈值这是我在实战中验证过最终效果差异比较大的一个环节。第三个是摘要模型的选择。给记忆做摘要不需要用到顶级模型因为摘要任务对推理深度要求低但对信息抽取的准确性要求高。我试过用小巧的模型做摘要碰到复杂多轮对话时容易漏掉关键实体换成更强模型以后摘要质量上来了成本也没有增加太多因为摘要调用频率远低于对话调用。省成本不应该省在这里。3.3 用户隔离与记忆的“命名空间”设计很多人做记忆功能时会犯一个严重的错误所有用户的记忆混在一个池子里。做个人工具时无所谓但一旦做多用户服务这就会变成灾难。用户 A 的消息检索出用户 B 的记忆模型会一本正经地觉得 A 就是 B甚至说出“上次你说你母亲生病”这种话那可真就是事故了。我的做法是每条记忆都带上一个命名空间字段这个字段通常是用户 ID 或会话 ID。检索时的过滤条件强制要求带命名空间绝不允许跨命名空间召回。同时在写入记忆时还要额外存一个“记忆指纹”——把记忆内容做一下规范化处理比如全角半角统一、去停用词、只保留核心实体组合用来判断两条看似不同、实际指同一件事的记忆。这个指纹还能用来做“这类用户普遍会问什么”的统计但注意不要直接用个体敏感信息做统计。3.4 把 claude-mem 集成到 FastAPI 应用的完整示例如果你想把它做成一个独立的服务最简单的办法是包一层 FastAPI。这样你的主业务代码不需要动只要在调用原对话接口之前先调用一下记忆服务的接口拿到上下文再发给模型。下面是一个 mini 版的服务代码from fastapi import FastAPI, HTTPException from pydantic import BaseModel import anthropic from claude_mem import MemoryStore app FastAPI() store MemoryStore(db_path./data/memory.db) class ChatRequest(BaseModel): user_id: str message: str app.post(/chat) def chat(req: ChatRequest): memories store.search(req.user_id, req.message, top_k5) system 你是一个有帮助的助手。 if memories: system 用户可以记住的上下文如下\n \n.join(memories[:5]) client anthropic.Anthropic() resp client.messages.create( modelclaude-sonnet-3-5, max_tokens1024, systemsystem, messages[{role: user, content: req.message}], ) store.record(req.user_id, req.message, resp.content[0].text) return {reply: resp.content[0].text}你看核心逻辑并没有比最简版多多少但它已经把“记忆”这个横切关注点包装成了一个独立 HTTP 接口。之后无论你是写爬虫脚本、命令行工具还是嵌套到别的 agent 框架里复用起来都很方便。3.5 运行时性能与成本控制跑了一段时间之后我发现真正的成本大头不是记忆存储而是每次检索时重新 Embedding 问题、以及在请求里多塞的那几条历史记录。有四个我已经验证有效的省钱省时技巧给用户消息做缓存同一用户重复问同一个问题时直接复用之前的完整响应不再调用模型也不再重算 Embedding摘要写入批量合并对话非常短、信息量很低时不逐条写记忆而是攒够几条之后再合并成一条摘要减少写入次数限制注入的最大记忆 token 数检索出来的记忆哪怕有 10 条真正注入的只保留 token 量不超过预算的部分长记忆自动截断或压缩控制记忆更新的频率同一用户连续 30 分钟内重复出现的相似话题不对旧记忆做重复更新只更新一次。说实话刚开始做记忆层的时候我总想把所有历史都塞给模型总觉得“多给点信息它就能答得更准”。实际跑下来之后发现给的信息太多、太杂模型反而像被灌了迷魂汤回答变得飘忽不定。后来我给自己立了一条规矩每次注入的记忆必须保证每一条都和当前问题“有明确的因果关联”要不是时间上的先后关系要不是实体上的直接引用否则宁可不给。4. 常见问题与排查技巧实录4.1 问题一加了记忆之后回答反而变傻了这是我在第一周碰到的最诡异的问题。用户明明问的是“这家店的营业时间是几点”系统却把三天前“用户说喜欢吃辣火锅”的记忆拉了出来然后模型一本正经地回答“你可以去试试那家火锅店”。后来排查发现原因是用户当天早些时候聊过“打算和朋友去吃火锅”所以“火锅”和“那家店”在向量空间里距离偏近被误判为相关记忆。解决方案是我后来加了“时间衰减权重”和“会话内优先”的规则同等相似度下本次会话内早些时候产生的记忆优先于几天前的记忆几小时内的记忆优先于上周的记忆。同时在摘要中给每条记忆打上“话题域”标签比如“美食”“工作”“购物”检索时只有当标签匹配或确实没有同标签的记忆时才允许跨标签召回。这个问题的本质是检索出来的记忆不能只看“语义相似”还要看“意图是否匹配”。语义相似指的是字面意思接近意图匹配指的是解答当前问题需要这条记忆作为前提条件。两条记忆字面上都在说“吃饭”但一条是偏好记录、一条是待办日程它们对回答“现在几点开门”都没有帮助。4.2 问题二上下文膨胀token 成本直线上升用记忆系统的终极宿命就是“记忆越来越多每次请求都越来越重”。我早期没做控制的时候单个用户的平均请求 token 涨了大概 60%成本也跟着涨。排查思路是分成两路。先看“单次注入”是不是太贪我给记忆块设了 token 上限比如 800 token检索结果超过的部分宁可不要也不硬塞。再看“长期记忆”是不是已经过期了一些三个月前的对话摘要其实已经没有任何参考价值它们只是躺在库里“占名额”每次检索都会被碰巧带出来。最终我敲定了一套规则你可以直接参考每次请求注入的记忆总 token 数不超过 800大约相当于 3-5 条中等长度的记忆时间超过 30 天、且没有被主动访问过 1 次以上的记忆不参与默认检索对用户最近 30 分钟内的对话不做摘要直接拼接原文对更早的对话才做摘要在不同时间粒度上分级处理。这套“近期原文远期摘要”的策略效果极其明显。模型在近期上下文里能看到最新细节远期又有压缩后的要点兜底token 开销大幅下降回答质量反而更高。4.3 问题三用户隐私与数据删除权做记忆系统有一件事绝对不能逃避隐私和数据合规。Claude 本身会处理输入输出的数据你做记忆层就等于是把用户数据又复制了一份存在自己手里中间任何一环管理不当都是雷。我在项目里做这几件事强烈建议你也照做脱敏写入记忆入库前对明显的个人识别信息做占位符替换比如手机号、地址、姓名等用 、 代替只在真正需要时通过专门接口原样取出用户控制权提供“清除记忆”的接口用户一调用就把该用户名下所有记忆硬删除不是打标记是真正 DELETE审计日志记录什么时候、为什么会把某些记忆检索出来给模型看方便排查“模型为什么知道这些”的争议数据过期策略默认生命周期超过 180 天的记忆自动归档归档区不参与检索用户明确问到时才允许手动访问。我到现在都记得一次紧急排查用户质问“为什么这个 AI 会知道我昨天去看了牙医我根本没告诉过它”。后来查日志发现这句话是用户女朋友在同一个账号下用同一台设备问的。问题不在于 AI 说漏嘴了而在于我的用户隔离是按账号而不是按设备导致女友的对话记忆被共享过来了。这提醒我一个重要原则在设计记忆系统时用户对“是否共享记忆”的需求是高度场景化的。最好的做法不是一刀切而是把“记忆共享”做成显式可选的模式。4.4 问题四记忆写入了但模型就是“想不起来”这个问题比前面的更隐蔽。系统明明已经存储了“用户说过自己是产品经理”新对话里用户问了个跟产品经理职责相关的问题检索也把那条记忆查出来了注入也注进去了但模型回答起来还是像第一次听说。我排查了很久才发现问题出在注入位置和语气上。核心原因有两个。第一我把记忆放在了 system prompt 里但 system 里内容太多太杂模型的实际权重分配会偏向更靠后的内容第二我用的是“用户可以记住的上下文如下”这种很弱的措辞模型也不知道这些信息应该被当成“硬性事实”还是“闲聊背景”。我把注入措辞改成了更明确的指令式比如“这些是用户确认过的事实比如他的职业、偏好和日程当用户问题涉及这些方面时你应该默认它们是已知条件不要反问”。这个改动之后模型引用记忆的频次明显提高偶尔甚至会主动说“根据你之前的介绍……”。效果好得多。所以我给所有做记忆注入的人一个建议不要把记忆简单拼在 prompt 里就完事要给模型明确的“使用协议”——什么时候必须用、什么时候不能乱用、冲突时以哪边为准。模型是需要被“指导”的而不是被“喂料”的。5. 从工具到方法论记忆层还能怎么演进5.1 把它变成个人知识库的“第二大脑”如果你只把它当个聊天记忆工具那就太浪费了。我在自己的项目中把 claude-mem 的记忆层抽象成了“个人知识输入管道”我读过的文章、看过的视频、随手记的灵感全部通过同一个接口写入记忆库。这样当我以后再向 Claude 提问时它检索的范围不只是“我们俩的对话”而是“我这一整年的输入”。举个例子我把自己过去几个月写的技术笔记、报销时填写的细节、拜访客户后的记录都丢进记忆库。某天我问 Claude “我上次去见那个客户的时候他提到最头痛的是哪件事”它真的能把这些信息召回来然后拼接成一个完整的回答。这个感觉就像是在用 AI 之前先给 AI 配了一本自己的日记。做知识库的关键是写入时要附带来源和标签检索时才能按来源过滤、按时间过滤。我后来专门加了一个 “source” 字段区分记忆来自对话、文章、还是手动录入这样即使内容相近也能按用户偏好进行不同排序。5.2 从“提取记忆”到“生成人物画像”跑久了以后我发现另一个高价值玩法让模型定期把某个用户或某个项目的长期记忆汇总成一份“画像”。这份画像不是从对话里截取碎片而是综合分析多条记忆形成结论性的描述。比如记忆库里有“用户是跨境电商从业者”“用户之前做过亚马逊店铺”“用户问过 Shopify 怎么迁移”模型可以据此生成一条画像记录“该用户可能正在从亚马逊转向 Shopify关注点是店铺迁移和数据同步”。这个画像可以被系统用来做更精准的推荐也可以作为下轮对话的强上下文。但我提醒一下不要依赖模型自动生成这种“推断性结论”。它的准确率不是 100%一旦基于错误画像去回答错误会自我强化越跑越偏。我的做法是每条画像记录都保留证据链接也就是对应是哪几条记忆推导出来的如果发现画像和用户最新说法冲突系统自动降低该画像的权重。5.3 给记忆层加入“反思与定期回顾”能力我最近在做的一个能力是让记忆系统周期性地“回看”过去一段时间的日志主动发现那些“曾经重要、但快要被遗忘”的信息并把它们提升为活跃记忆。这类似于人类复习时做的“间歇式提取”。举个例子用户三个星期前说过“下个月要去日本出差”这个信息在临近出差日期时会变得特别重要但如果不靠系统定期纠偏它在向量空间里的权重早就被别的信息冲掉了。实现方法也不复杂我每周跑一次离线任务对过去一个月的高权重记忆做一次“重要性重估”结合时间临近度和任务完成状态动态调整权重。这套机制目前还在打磨但至少在类似“帮用户预约会议提醒”这类场景里效果已经肉眼可见了。5.4 多模型共存记忆层不是 Claude 专属虽然项目名叫 claude-mem但实际落地时我很快发现记忆层完全可以做得和具体模型解耦。Claude 负责“现场回答”但它看到的内容从哪来、哪些历史信息被复用这套机制其实可以被任何模型共享——今天你换一个开源模型部署记忆层的输出格式稍微对齐一下马上就能继续用。这就是为什么我一直强调记忆层的位置应该在“模型之上、应用之下”而不是“模型之内、应用之外”。它承载的不是模型的“思维”而是用户和应用之间积累下来的“事实关系”。写在最后项目中我学到的一件事做 claude-mem 这一路最深的体会不是技术选型多重要而是“别一上来就追求全面”。我第一版试图把摘要、向量检索、衰减、画像、权限全部一次做完结果哪一块都做得不够好。后来砍到只剩“记录对话、检索记忆、拼进 prompt”这个最小闭环团队协作和日常维护的复杂度一下子降了下来才有精力去打磨真正影响体验的细节。如果你也想给 Claude 加记忆我建议你从最小闭环开始先跑通“存得进、查得到、拼得上”再去想“怎么查得准、怎么衰减、怎么保护隐私”。记忆的准确性和可靠性永远是在数据量增加之后才开始显现的你还未积累足够多真实数据却早早开始做那些复杂机制只会提前引入不必要的复杂度。最后分享一个小技巧无论你的系统做得多完善都要给“记忆失忆”留一个逃生舱——当模型发现自己拿到的历史记忆跟用户当前说法矛盾时你有义务提供“忘记这条记忆”或者“修正这条记忆”的入口。这不仅是产品层面的体贴也是做工程的人给自己留的一条后路机器不可能永远准确承认这点系统才更可靠。
返回列表