不止一个接入了Claude API的朋友问过我:明明每次请求都是按文档发的,为什么机器人聊多了还是像失忆一样?我一般直接甩一个项目链接过去,里面是一个叫claude-mem的小工具。它的全部工作就一句话:给无状态的对话接口套一层外置记忆,让会话可以滚动裁剪、自动摘要、跨轮次留存。你要是正在做AI客服、个人助理、自动化脚本这类东西,八成也会撞上同一个坎——上下文越堆越长,钱越花越多,回答反而越来越飘。这篇文章我会把设计选择、核心算法和我实际踩过的坑全部摊开,关键代码可以直接抄走。
1. Claude无状态API为什么需要一份持久记忆层
1.1 无状态接口的尴尬你迟早会撞上
Claude的messages接口本身是无状态的,这一点很多新手一开始没意识到。你发一个请求,它返回一个响应,然后就什么都没有了。下一次请求,你必须把之前所有对话历史重新塞进messages数组里,模型才知道你们在聊什么。
这种设计在早期做测试时完全没问题,但一旦进入真实业务场景,你会立刻发现两个刺眼的问题。
第一个是重复计费。假设用户已经聊了15轮,历史消息可能是18000个token,用户最新一句只是“按刚才说的继续”。你的请求必须携带那18000个token,只为换一个几十token的回答。这相当于你每次出门都把整个衣柜的衣服穿上,只为了给邻居递个钥匙。
第二个是上下文溢出。Claude模型虽然有很长的上下文窗口,但窗口是死的,对话是活的。聊天一旦超过窗口,最后只有一个结果:程序报错,或者你手动截断,模型突然忘记前面聊了什么。
我还见过有人试图绕过这个问题,把历史消息存在内存里,用的时候拼个字符串请求。方案能跑,但程序一重启,记忆全丢。重启十次,同一个用户就要自我介绍十次。这哪是AI助手,分明是金鱼。
1.2 记忆不是一种东西,至少要分四层
做过几版记忆功能之后,我意识到“记忆”这个词太模糊了,把它当一个整体来设计必然翻车。实际拆开看,至少要分四层。
短期窗口,对应最近几轮对话的原始消息,这是模型回答问题时最需要的信息,因为离得近、上下文还热。工作记忆,是放在system prompt里的少量事实约束,比如“用户偏好正式语气”“项目代号是Hawk”。长期记忆,是历史对话经过压缩提取后的摘要,可以跨会话存活,用来回答“我们上次聊到哪里了”这类问题。技能记忆,则是工具定义、回复模板、常用参数这些可复用配置。
claude-mem第一版只做了第一层和第二层,后面才补上第三层。第四层我直接放配置文件里管理,没写进记忆模块。你要是一开始就想着把四层一次做全,大概率会在代码里绕晕,先跑通前两层更有价值。
1.3 我在真实场景里被逼出来的需求
这个项目不是凭空写的,是被几个实际需求逼出来的。
第一个场景是定时任务机器人。我每天让它汇总项目进展,希望它记得上周说的结论和遗留问题。可是定时任务每次触发都是新进程,不带任何历史。没有记忆,它每天给我一份互相矛盾的日报,看得我头疼。
第二个场景是同一个API Key下跑多个用户。我的工具同时服务不同项目组,如果所有人共用一个全局上下文,A组的信息会被B组看到,后果很严重。所以记忆模块必须支持会话隔离,这是底线。
第三个场景是自动化写作助手。我需要它持续保持同一篇文章的立场和风格,但这个需求没法靠临时发送历史内容解决,几十万字的素材也不可能全部抛给模型。你只能把已经写过的关键信息压缩到摘要里,按需拼接。
这些需求看起来各不相同,本质上都是同一个问题:状态到底放哪里、怎么管理、怎么在合适的时间塞给模型。想清楚这一点,设计方向就定了。
2. 为什么不直接上向量库:存储方案对比与三段式架构
2.1 选型对比:从内存字典到向量数据库的取舍
我第一次做记忆存储时,最朴素的方案是拿一个全局dict存历史消息。写起来爽,跑起来也快,但程序只要重启一下,所有记忆烟消云散。这个缺陷在开发阶段还能忍,上线后就是事故。
后来我认真对比了几个常见的存储方案,各有利弊。
| 方案 | 查询能力 | 持久性 | 复杂度 | 适合阶段 |
|---|---|---|---|---|
| 全局字典 | 按key取,简单 | 无 | 零 | 原型验证 |
| 文件/JSON落盘 | 按key取,需自己管理 | 有 | 低 | 单机脚本 |
| SQLite | SQL查询,支持索引 | 有 | 中 | 个人项目、中小应用 |
| Redis等内存库 | 高性能KV | 可配置 | 中高 | 多实例部署 |
| 向量数据库 | 语义检索 | 有 | 高 | 大规模RAG场景 |
如果你问我第一次上生产用什么,我建议SQLite。理由很简单:它足够持久,重启之后数据还在;它支持SQL查询,你可以按时间、按会话、按消息角色筛选记录;它没有任何额外服务需要部署,一个文件全搞定。
向量数据库不是不能用,是多数场景根本用不到。会话历史量没有大到需要语义检索的时候,加向量库只会多出“选型、部署、配置embedding模型”一系列负担。先把核心链路跑通,再加复杂度,这是我一直坚持的推进方式。
2.2 三段式记忆架构:原始窗口、摘要层、按需召回
claude-mem最终采用的结构,是我自己总结的三段式,后来发现社区里不少类似方案也是这么做的。
第一段是原始消息窗口,只保留最近N轮对话的完整内容,保证模型回答时有足够鲜活的上下文。第二段是历史摘要层,窗口之外的旧消息不直接保留,而是在被清出前压缩成一段摘要,存到数据库里。第三段是按需召回层,当新问题涉及陈旧信息时,从摘要库里把相关片段提取出来,重新注入上下文。
你可能会问,为什么不全量保留原始消息,非要用摘要?因为上下文窗口有限,token成本也有限。留原始消息,几千轮对话根本塞不下;留摘要,同样的窗口容量能装下的信息密度高出几个量级。
还有一点容易被忽略:摘要本身也是一种记忆,它比原始消息更适合长期保存。因为原始对话里充满寒暄、重复表达和无关细节,模型在压缩过程中会主动把噪音滤掉,留下的往往是决策点和结论。
2.3 项目目录设计:把存储、逻辑、提示词分开
代码组织上,我吃过“所有逻辑塞一个文件”的亏。项目一长大,想改摘要提示词都得翻半天代码,还容易误伤其他逻辑。
所以我的目录是这么拆的:
claude-mem/ ├── claude_mem/ │ ├── __init__.py │ ├── storage.py │ ├── memory.py │ ├── prompts.py │ └── token_budget.py ├── examples/ │ └── chat_demo.py └── requirements.txtstorage.py只负责SQLite读写,memory.py负责记忆管理算法,prompts.py集中放所有提示词模板,token_budget.py做token估算和阈值计算。这样做的直接好处是:调整摘要提示词时不用碰算法代码,替换存储层时也不用改上层逻辑。
很多人写AI应用时最不在意这一点,觉得提示词就是字符串,混在业务代码里没问题。实际上提示词是整个系统最容易迭代的部分,把它独立出来,后面改起来会轻松非常多。
3. 核心算法:token到底怎么算、窗口怎么裁、摘要怎么抽
3.1 一切从token计数开始,算错了后面全是事
memory裁剪和摘要的前提是知道当前上下文到底有多大。如果你连自己拼出来的请求体占多少token都不知道,那裁剪阈值、摘要触发时机都无从谈起。
计算token有两种途径。第一种是看上一次API返回里的usage.input_tokens字段,它准确反映了你这次实际发出去的token数,这是最权威的校准依据。第二种是本地估算,因为Anthropic没有开放和tiktoken完全等价的库,我一般会维护一个简单的近似函数,用字符数粗算。
import re def estimate_tokens(text: str) -> int: if not text: return 0 # 中文按约 1 字 1.3 token,英文按单词 1 token 做粗估 chinese_chars = len(re.findall(r'[\u4e00-\u9fff]', text)) other_chars = len(text) - chinese_chars return int(chinese_chars * 1.3 + other_chars * 0.3) + 1这个估算肯定不够精确,但决策时够了。你只需要知道当前是否接近触发值,不需要精确到个位。真正要精确,就把上一轮返回的usage字段存起来,作为本轮计算的基准。
有一个坑必须提醒:系统提示词中的固定模板也要计入token。很多人只统计消息内容,忘记统计system prompt和工具定义。半年前我接的一个项目,系统提示词写了800个token,工具定义又1200个,测试时没算,结果上线后没聊几轮就触发了上限,排查了两个小时才发现是固定部分占了大头。
3.2 滚动窗口裁剪:为什么要用0.6这个触发比例
滚动窗口的想法很简单:维护一个消息队列,不断往里追加新消息,当总token数超过阈值,就把最老的一批消息交给摘要器,然后丢弃原始文本,把摘要结果追加到队列里。
关键在于“什么时候触发裁剪”。我一开始用的是等窗口满了再裁,比如20k窗口,等到19k才开始处理。结果发现,摘要生成本身也要消耗token,旧消息加上摘要过程一起发送,极容易在压缩瞬间二次超限。
后来我改成提前触发,设置一个比例阈值。默认情况下,trigger_tokens = max_context_tokens * 0.6,也就是上下文用到60%就开始执行压缩。这样压缩过程消耗的token还有余量,不会把自己逼到崩溃边缘。
from collections import deque class RollingMemory: def __init__(self, max_context_tokens=20000, trigger_ratio=0.6): self.messages = deque() self.max_tokens = max_context_tokens self.trigger_tokens = int(max_context_tokens * trigger_ratio) def add(self, role: str, content: str): self.messages.append({"role": role, "content": content}) def exceeded(self) -> bool: total = sum(estimate_tokens(m["content"]) for m in self.messages) return total > self.trigger_tokens def pop_oldest_batch(self) -> list: batch = [] count = 0 # 每次弹出约 1/3 的消息量,让压缩收益足够明显 target_count = max(1, len(self.messages) // 3) while count < target_count and self.messages: m = self.messages.popleft() batch.append(m) count += 1 return batch为什么每次只弹出三分之一而不是全部?因为全部压缩一次生成成本太高,而且新思路是保留最近对话的原始感,没必要把刚聊过的几句话也强行压成摘要。压缩一次,收益和成本比较平衡的比例就在25%-35%之间。
这个触发比例也不是死参数。实际使用中,如果你用的模型上下文窗口是200k,可以把它调成0.7,因为余量大;如果是32k的小窗口,建议保守一点调成0.5,给自己留足处理空间。
3.3 让Claude自己压缩Claude的对话
裁剪算法好写,难的是摘要有质量。我最初尝试过直接截断,就是把旧消息前2000字符硬切下来当作摘要。结果模型经常忘记关键决策,尤其是用户中途提出的约束条件,在硬截断里几乎必然丢失。
所以后来我改成用模型自己来压缩历史对话。让大模型读一遍旧对话,然后输出一段结构化摘要,这是目前效果最好的做法。
SUMMARY_PROMPT = """你需要对一段对话历史做压缩总结。要求如下: 1. 保留用户的所有明确偏好和约束,例如“不要用正式语气”“价格上限是500元”。 2. 保留已经做出的关键决策、待办事项、争议点。 3. 保留所有数字、日期、项目代号等硬信息。 4. 省略寒暄、重复表达和过程性描述。 5. 用条目式输出,全文不超过400字。 待压缩的历史对话: {history} """这里有几个细节值得展开。
第一,输出长度要限制。如果摘要本身就有3000字,那压缩毫无意义。我通常限制在300-500字,这已经是很好的密度了。
第二,要让摘要可以迭代更新。历史摘要和新消息之间,我的做法是先把旧摘要拿出来,连同新一批旧消息一起再压缩一次,而不是每次从原始对话重新生成全部摘要。这样省token,也避免把全量历史一直拽着不放。
第三,摘要结果一定要单独存一张表,并且标记它覆盖的消息范围。你不仅要知道这段摘要说什么,还要知道它对应哪个时间段的对话,这为以后排查和召回提供了坐标。
有个取巧的地方,摘要里可以强制用固定格式,比如先写“用户目标”,再写“已确定事项”,再写“待确认问题”。模型对这种结构化提示非常敏感,输出质量比自由摘要稳定得多。这算是我测试了大量提示词之后学到的甜头。
3.4 上下文重建的顺序问题,比你想的敏感
怎么把摘要和最近消息拼在一起,是一件看起来无所谓、实际影响很大的事。如果顺序不对,模型会分不清哪些是历史摘要,哪些是正在进行的对话,回答时容易张冠李戴。
我推荐的组装顺序是:
system prompt(固定偏好和身份) → 历史摘要(一条独立的user/assistant消息,明确标注) → 最近原始消息(按时间顺序) → 当前用户问题历史摘要建议用单独一条消息包裹,并且加上标记,让它和最近对话之间有清晰的边界。比如:
def build_messages(mem, question): messages = [ {"role": "system", "content": SYSTEM_PROMPT}, ] if mem.summary: messages.append({ "role": "user", "content": "[历史摘要,来自更早的对话,非当前会话内容]\n" + mem.summary }) messages.append({ "role": "assistant", "content": "收到,我会在后续回答中参考以上历史信息。" }) for m in mem.recent_messages: messages.append(m) messages.append({"role": "user", "content": question}) return messages你可能觉得“收到,我会在后续回答中参考以上历史信息”这句很多余。但我实测下来,加这一句可以让模型明确把摘要当作参考资料,而不是当作当前对话的一部分。这个小改动直接减少了一大类“模型把旧话拿来重复回答”的问题。
顺序问题背后其实是一个基本原则:要让模型分得清“记忆”和“事实”。记忆是背景信息,事实是当下正在发生的对话。二者混在一起,模型的注意力会被稀释,回答质量不稳定。
4. 完整实现:一个能直接跑起来的最小版本
4.1 数据表设计:三张表搞定会话、消息、摘要
存储层我用SQLite,原因前面说过。表结构我设计成三张,职责分离,后面查问题也方便。
CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, user_key TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, token_count INTEGER NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS summaries ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, summary TEXT NOT NULL, start_msg_id INTEGER, end_msg_id INTEGER, model TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, id);sessions表里的user_key就是给会话隔离用的。同一个用户的所有会话都挂在同一个key下面,但每条会话的session_id不同,记忆互不干扰。
messages表存原始消息,token_count字段每次写入时顺手估算一下,后面做统计和裁剪都不用临场再算一遍。
summaries表存摘要,start_msg_id和end_msg_id记录这段摘要覆盖了哪些消息,这样我可以保证摘要生成的范围不会重复,也不会遗漏。
这三张表看着简单,但我实际用下来够跑所有本地场景。如果以后数据量上来,再迁移到PostgreSQL也不难,因为查询逻辑基本就是按session_id取数据。
4.2 MemoryManager核心类,把算法和存储串起来
光有表还不够,核心是把滚动窗口、摘要生成、上下文重建串到一起。我把这个逻辑收敛在MemoryManager类里。
import sqlite3 from datetime import datetime class MemoryManager: def __init__(self, db_path, session_id, max_context_tokens=20000): self.conn = sqlite3.connect(db_path) self.session_id = session_id self.trigger_tokens = int(max_context_tokens * 0.6) def add_message(self, role, content, token_count): cur = self.conn.execute( "INSERT INTO messages (session_id, role, content, token_count) VALUES (?,?,?,?)", (self.session_id, role, content, token_count) ) self.conn.commit() return cur.lastrowid def total_tokens(self): row = self.conn.execute( "SELECT COALESCE(SUM(token_count),0) FROM messages WHERE session_id=?", (self.session_id,) ).fetchone() return row[0] def load_recent(self, limit=20): rows = self.conn.execute( "SELECT role, content FROM messages WHERE session_id=? ORDER BY id DESC LIMIT ?", (self.session_id, limit) ).fetchall() return [{"role": r[0], "content": r[1]} for r in reversed(rows)]这个类只做了三件基础事:加消息、算token、取最近消息。真正的压缩动作放在独立的压缩流程里。
我故意把MemoryManager和压缩函数分开,因为压缩要调用模型,会造成外部依赖;如果直接写在类里,测试时就很难脱离API单独验证。所以压缩我单独写成函数,传入历史消息,返回摘要文本。
这里有个细节值得提:load_recent里我用了ORDER BY id DESC LIMIT再反转,而不是BETWEEN,原因是SQLite对带索引的倒序查询优化很好,量大之后性能差距很明显。一个刚开始写这类应用的人很容易用SELECT * FROM messages WHERE session_id=?全量拉数据,等到历史达到几千条时,内存直接撑爆。
4.3 跑一个真实联调,看记忆带来的收益
我搭了一个简单的演示脚本,模拟三回合对话,对比无记忆和有记忆的差异。
做一个量化实验,我们假设每一轮用户问题50个token,助手回答200个token。无记忆方案里,第二轮要发送第一轮整段对话,加上当前问题,总共大约450个token;到第三轮变成700个token。而claude-mem的方案,到第三轮时,第一轮已经被压缩成一条约80个token的摘要,最近对话窗口里只保留第二轮和当前问题,总token只是摘要80加当前轮250,大约330个token。
| 轮次 | 无记忆发送token | claude-mem发送token |
|---|---|---|
| 第1轮 | ~250 | ~250 |
| 第2轮 | ~450 | ~450 |
| 第3轮 | ~700 | ~330 |
| 第5轮 | ~1200 | ~600 |
真实对话里增长更快,因为用户上下文越聊越丰富。我跑了一个10轮的测试,无记忆方案在第10轮需要发送约3000个token,而claude-mem一般稳定在800-1000。这个差距到长会话时会被拉得更悬殊。
联调时我还特意让程序崩溃重启了一次,模拟生产环境的进程死掉。重启后,因为消息和摘要都落在SQLite里,重新加载时记忆完整恢复。这一步验证通过,我心里才算踏实。
5. 实战中的常见坑:摘要越压越贵、状态串线、中文token超标
5.1 摘要越压越贵是怎么回事
我遇到过一个非常反直觉的现象:加了摘要逻辑后,总token反而比不加还高。排查后发现,摘要压缩本身也是一次API调用,也需要消耗token。尤其当摘要触发过于频繁,比如每来两条消息就压缩一次,这个压缩成本早就超过了省下的token收益。
解决办法有几个方向,我最后用的是“累计摘要频控”。摘要只在消息新增数或token增量超过阈值时触发,比如每新增20条消息,或者新增了3000 token,才允许再压缩一次。中间的小额增长就让它攒着,不动它。
另一个办法是复用摘要结果做“增量摘要”。每次压缩时,不是把整段历史重新读一遍,而是拿着上一次的摘要,加上新进来的一批旧消息,一起交给模型压缩。这样虽然摘要信息会丢失一些细节,但成本能省一大截。我的实践是,增量摘要适合高频场景,全量重摘要适合每周或每月做一次彻底整理。
还有一个坑,摘要结果如果总是重新生成,旧的摘要记录就变成了死数据。我每天会跑一个清理任务,把超过7天且没有关联新摘要的旧摘要删掉,避免数据库越胀越大。
5.2 模型把摘要当成当前对话状态
这个坑是我在客服机器人项目里发现的。模型经常在回复里重复摘要里已经解决过的问题,或者把某条历史结论当成当前状态的必然描述,搞得用户以为机器人根本没更新。
根因就是上下文拼装时没有把摘要和当前对话区分清楚。我前面提到的“历史摘要”标记就是为这个准备的。你可以在摘要前加上“以下信息来自历史对话,仅供背景参考”这类提示,把摘要的定位钉死。
如果模型仍然犯错,就进一步缩短摘要时间范围,保留更多最近原始消息。你可以把最近窗口从8轮改成12轮,对应地摘要素材缩短一些,让当前信息权重更大。
我在实践中的经验是:与其反复调提示词,不如先看懂模型的行为模式。如果你的机器人经常答非所问,先看上下文里摘要占比是否过高。摘要占比超过60%,模型会明显偏向历史背景,而不是当下问题。
5.3 API限流、超时和异常,别让重试把数据写重复
在真实生产环境里,API调用不可能每次都成功。限流、超时、上游5xx轮番出现。我一开始直接在调用外面套了个简单重试,结果发现一个问题:请求超时了,但服务端实际上已经处理成功,只是响应没回来。重试发送同样的摘要请求,就在数据库里写了两份一样的摘要,后续上下文组装就乱了。
解决方法是给每次摘要生成加一个幂等键。我取当前会话ID加最新一条消息ID做唯一标识,确保哪怕重复请求,存储层也只写入一次。
def generate_summary(messages, idempotency_key): resp = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=messages, # 具体参数以当时官方SDK支持为准 ) return resp.content[0].text这个idempotency_key其实就是业务逻辑里的request id。记在日志里,排查问题时能直接定位到某次重复调用。
另外,限流之后不要立刻重试。正确的做法是做指数退避,第一次等1秒,第二次等2秒,第三次等4秒,最多到30秒就停。我见过有人写了个没有退避的for循环,每秒重试一次,结果同一个窗口被上游限流了整整十分钟,白跑白等。
5.4 中文对话的token消耗,比你想象的狠
很多中文用户在开发时会犯一个典型错误:拿英文场景的token估算方式往中文上套。实测下来,中文同一个字符的token消耗通常是英文单词的1.5倍左右。同样是500字的历史对话,英文可能只有120个token左右,中文直接冲上400以上。
这意味着中文场景下,窗口和摘要策略要缩得更狠。我一般会把中文项目的trigger_ratio从0.6下调到0.5,摘要输出字数也压到200-300字,不然摘要本身就会变成一笔不小的开销。
如果你的业务人群主要是中文用户,还有一个更省的做法:在写入数据库之前先做一次简单的归一化,比如去掉重复的空格、统一标点、把明显重复的问候语删除。这些小动作能帮你省下5%-10%的token,累计起来非常可观。
不过要注意,归一化如果做过头,会破坏消息的语义完整性。我吃过一个亏,把用户消息里的换行全部删了,结果模型把两句不同的话当成一句,回答直接跑偏。现在我只做安全性精简化,不对正文结构大动干戈。
6. 顺着这个方向还能做哪些升级
6.1 第一版就要把session隔离做对
我在做记忆层时犯过的最大的错,就是第一版没做session隔离。当时我图省事,直接用同一个全局memory对象处理所有用户请求,结果用户A的消息出现在用户B的上下文里。还好那是在内部测试环境,如果上线后发生这种串线,轻则业务数据混乱,重则隐私泄露。
所以无论你的项目多简单、用户多少,第一版就应该引入user_key和session_id。数据库表里从一开始就留好这两个字段,不要等以后再迁移。后面想支持多租户,也只需要在查询和写入时统一走这两个字段,改动成本很小。
6.2 给新手的启动建议:先窗口,再摘要,最后才向量
如果你想把这个方案应用到自己的项目里,我的建议是分三步走,不要一次到位。
第一步,只做滚动窗口,不做摘要。把最近N轮消息保存好,超过窗口的直接丢掉。这个版本能解决程序重启丢记忆的问题,以及上下文无限增长的问题。缺点是丢掉的旧消息无法找回,但作为第一个可运行版本完全够用。
第二步,引入摘要压缩。这一步能把保留历史信息的成本降下来。当你发现最近的对话数量已经足够覆盖大部分问题,但偶尔又需要回忆更早的决策时,就说明该上摘要了。
第三步,再加语义召回。摘要做到位之后,如果你仍然觉得提取信息不够精准,可以考虑引入embedding做相似度检索。这一步最好在确认前两步确实不够用之后再动手。我见过不少人第一步都没跑稳,就急吼吼上向量库,最后只是给自己添堵。
6.3 后续可以扩展的几个方向
目前我在自己项目里用claude-mem这套逻辑,已经满足语音助手和定时机器人两个主力场景。如果继续往下扩展,我比较看好的方向有几个。
一个是把摘要检索从关键词换成更语义化的召回。SQLite的LIKE查询在关键词明确时够用,但遇上“上次说的那个客户问题是什么样的”这种模糊问法就抓瞎了。这时可以接一个本地embedding模型,把摘要向量化,做到真正的语义搜索。
另一个是支持多会话之间的信息合并。两个会话聊了相近主题,摘要可以自动合并去重,避免同一个事实被重复记忆。
还有一块是记忆的权限控制。如果这个工具从单机挪到服务端,不同用户可以访问哪些摘要、哪些不能访问,需要有一套权限层。这个方向上没有特别标准的做法,我目前还在边做边摸索。
最后说一个我自己的使用习惯。claude-mem不是说要把所有历史都堆给模型,那只会把上下文变成一个庞大而无序的垃圾场。真正有用的记忆,是那些能让模型更快理解用户当前意图的关键约束和决策点。我在设计裁剪参数和处理逻辑时,始终提醒自己:你的目标不是保留一切,而是把最有价值的那部分信息,在合适的时机、以合适的密度重新放回上下文。抱着这个原则做出来的记忆模块,才不会越做越臃肿。