1. 从零搭建一个本地记忆中枢claude-mem 到底在解决什么问题第一次看到claude-mem这个名字我脑子里蹦出来的第一个念头是终于有人把「记忆」这件事从对话窗口里拎出来了。做过 AI 应用开发的人都知道大模型本身是无状态的每一次对话都是「初次见面」。你昨天跟它聊过的项目背景、上周定下的命名规范、上个月踩过的那个坑只要超出上下文窗口全部归零。这不是模型笨而是它的工作方式决定的——它只活在当前这一次请求里。claude-mem这个项目本质上就是给这类对话式 AI 外挂一套可持久化的记忆层。它要解决的核心问题非常具体让 AI 在跨会话、跨项目、跨时间的场景下依然能记住你是谁、你在做什么、你之前做过什么决定。听起来像是个小功能但真正落地过的人会明白这里面牵扯到的东西一点都不少——存储结构怎么设计、记忆怎么检索、什么时候写入、什么时候遗忘、怎么避免把噪音也当成记忆存进去每一个都是坑。我之所以对这个方向特别感兴趣是因为过去大半年我一直在做本地化的 AI 辅助开发工作流。用过各种方案之后发现绝大多数「记忆」功能要么是云端黑盒你根本不知道它记了什么、怎么用的要么就是简单粗暴地把历史对话全塞进向量库检索出来的东西驴唇不对马嘴。claude-mem走的是另一条路本地优先、结构化存储、按需检索。这三点决定了它的适用人群——如果你只是偶尔用 AI 聊聊天那它对你意义不大但如果你是那种每天要跟 AI 协作好几个小时、项目周期动辄几周几个月的人这套东西能实实在在省下你反复「重新介绍背景」的时间。这篇文章我会从设计思路、核心机制、实操搭建、问题排查四个维度把claude-mem这类本地记忆系统彻底拆开讲一遍。不管你是刚接触 AI 工作流的新手还是已经在折腾各种记忆方案的老手应该都能从里面找到能直接抄作业的部分。我会尽量把每个「为什么这么设计」讲透而不是只丢一堆配置让你照抄——因为记忆系统这东西配置是死的你的使用习惯是活的理解原理才能调出真正顺手的方案。2. 记忆系统的整体设计与思路拆解2.1 为什么不能只靠上下文窗口硬扛很多人第一反应是现在模型的上下文窗口不是越来越大了吗几十万 token 都能塞为什么还要单独搞记忆系统这个问题我一开始也想过但实际用下来发现上下文窗口和记忆系统解决的是两个完全不同的问题。上下文窗口是短期工作记忆它解决的是「这一次对话里前后文要连贯」。而记忆系统是长期情景记忆它解决的是「跨对话、跨天、跨项目的信息留存」。你把三个月的项目历史全塞进上下文窗口先不说 token 成本和延迟光是「注意力稀释」就够你受的——模型在几十万 token 里找关键信息的能力远不如你精准地喂给它三五条相关记忆。更现实的问题是成本。假设你每天跟 AI 协作 4 小时产生大约 5 万 token 的对话内容一个月就是 150 万 token。如果每次都全量带上费用和响应速度都会崩。而记忆系统的思路是平时把信息压缩存起来需要的时候只取相关的那一小部分。这跟人脑的工作方式其实很像——你不会记得过去一个月说过的每一句话但你会记得「那个项目用的是 PostgreSQL 不是 MySQL」这种关键结论。claude-mem的设计正是基于这个逻辑。它不追求记住所有东西而是追求在正确的时机取出正确的记忆。这个取舍非常关键也是后面所有设计决策的出发点。2.2 本地优先架构的取舍与理由claude-mem选择本地优先这个决策背后有几层考虑我觉得值得展开说。第一层是隐私与数据主权。你的项目背景、代码片段、决策记录这些东西很多是敏感的。放到云端记忆服务里你永远不知道它会被怎么处理。本地存储意味着数据不出你的机器这对做企业项目或者处理敏感信息的人来说是硬需求。第二层是可控性。云端记忆服务通常是个黑盒你没法直接查看、编辑、删除某条记忆。而本地存储意味着你可以用任何工具打开数据库看到底存了什么哪条记错了直接改哪条不想要直接删。这种「可审计」的特性在调试记忆效果的时候极其重要。第三层是离线可用与低延迟。本地检索不需要网络往返响应速度是毫秒级的。对于高频调用的场景这个差异体感很明显。当然本地优先也有代价。最直接的就是多设备同步麻烦——你在台式机上存的记忆笔记本上用不了。claude-mem这类项目通常的做法是把存储层抽象出来默认用本地文件或本地数据库但保留切换到远程存储的接口。我的建议是如果你主要在一台机器上工作本地存储完全够用如果多设备协作是刚需那就得在存储层上多花点心思比如把数据目录放在同步盘里或者自己搭一个轻量的同步服务。2.3 结构化存储 vs 纯向量检索这是记忆系统设计里最容易走偏的地方。很多人一提到「记忆」就想到向量数据库觉得把内容 embedding 一下存进去检索的时候算相似度就完事了。我早期也这么干过结果发现纯向量检索有几个绕不开的问题。问题一相似不等于相关。向量检索找的是语义相近的内容但记忆检索需要的是「当前情境下真正有用的信息」。举个例子你问「这个函数怎么优化」向量检索可能给你翻出一堆关于「函数」的历史讨论但真正有用的是「这个项目里函数命名规范是动词开头」这种上下文。语义相似度抓不住这种情境相关性。问题二无法精确过滤。向量检索很难做「只在这个项目范围内找」「只要最近一周的」这种结构化过滤。而记忆系统恰恰经常需要这类过滤。问题三可解释性差。向量检索出来的结果你很难解释为什么它排第一。调试的时候一头雾水。claude-mem的思路是混合存储结构化字段时间、项目、类型、标签用传统数据库存内容本身用文本存检索的时候先做结构化过滤缩小范围再做语义或关键词匹配排序。这样既保留了精确过滤的能力又有语义检索的灵活性。这个设计我觉得是这类系统的正解后面实操部分我会详细讲怎么落地。2.4 记忆的生命周期写入、检索、遗忘一个完整的记忆系统必须回答三个问题什么时候写、怎么取、什么时候删。这三件事构成了记忆的生命周期任何一环设计不好系统都会退化成一堆没用的数据。写入时机上claude-mem通常采用「显式 隐式」结合的策略。显式是指你主动说「记住这个」系统直接存隐式是指系统从对话里自动提取值得记的信息比如检测到「决定」「约定」「规范」这类关键词时触发。隐式提取是最容易出问题的部分——提取太激进存一堆噪音提取太保守关键信息漏掉。我的经验是隐式提取的阈值要调得偏保守宁可漏存也别乱存因为噪音记忆的破坏力远大于缺失记忆。检索策略上核心是「相关性排序」。前面说的结构化过滤 语义匹配是基础但真正拉开差距的是时间衰减和使用频率加权。最近用过的记忆、经常被检索到的记忆权重应该更高。这跟人脑的记忆强化机制是一个道理——常用的记忆会越来越清晰不用的会慢慢淡忘。遗忘机制是最容易被忽视的一环。很多人搭记忆系统只想着怎么存不想着怎么删结果几个月后数据库里全是过时信息检索质量断崖式下跌。claude-mem一般会提供几种遗忘策略按时间过期比如超过 90 天的低权重记忆自动归档、按容量淘汰超过阈值时淘汰最久未使用的、手动清理。我个人的习惯是每周花十分钟过一遍最近新增的记忆把明显没用的删掉这个习惯能让系统长期保持高质量。3. 核心细节解析与实操要点3.1 存储层选型SQLite 为什么是首选聊到本地存储绕不开 SQLite。claude-mem这类项目十有八九默认用 SQLite这不是偷懒而是经过权衡的最优解。SQLite 的优势在于单文件、零配置、跨平台、支持全文检索。你不需要装任何服务一个.db文件就是全部数据拷走就能迁移。它内置的 FTS5 全文检索扩展做关键词匹配性能很好配合结构化字段的索引完全能撑起个人级别的记忆系统。数据量到几十万条之前SQLite 的性能都不会成为瓶颈。对比一下其他选项纯文本文件JSON/Markdown虽然可读性好但检索和并发写入是硬伤PostgreSQL 这类服务型数据库功能强但对个人使用来说太重了装个数据库服务就为了存几千条记忆性价比太低向量数据库单独用又缺结构化能力。所以 SQLite 是那个「刚刚好」的选择。下面是一个典型的记忆表结构设计我把它拆开讲每个字段的用意CREATE TABLE memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, -- 记忆正文 project TEXT, -- 所属项目用于隔离 category TEXT, -- 类型decision/fact/preference/context tags TEXT, -- 逗号分隔的标签便于过滤 importance INTEGER DEFAULT 5, -- 重要度 1-10影响检索权重 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_used_at DATETIME, -- 最近一次被检索到的时间 use_count INTEGER DEFAULT 0 -- 被检索次数用于频率加权 ); CREATE VIRTUAL TABLE memories_fts USING fts5( content, tags, contentmemories, content_rowidid );这里有几个设计细节值得说。project字段是记忆隔离的关键不同项目的记忆默认不互相干扰避免「A 项目的规范污染 B 项目」这种问题。category字段让检索时可以按类型过滤比如你只想要「决策类」记忆。importance和use_count配合last_used_at构成了检索排序的权重基础。FTS5 虚拟表跟主表通过content_rowid关联实现全文检索。注意FTS5 表需要手动维护同步插入、更新、删除主表数据时要同步操作 FTS 表否则检索结果会跟实际数据对不上。这是新手最容易踩的坑之一。3.2 记忆的提取从对话里捞出值得存的东西存储结构搭好之后下一个问题是怎么从对话流里识别出「值得记」的内容。这一步做得好不好直接决定记忆库的质量。我的做法是规则 模型双层过滤。第一层用规则快速筛成本低、速度快第二层用模型精判准确率高但成本高只对第一层筛出来的候选做处理。规则层主要抓这几类信号决策类出现「决定用」「就选」「定下来」「以后都」这类词规范类出现「规范是」「约定」「统一用」「命名规则」这类词偏好类出现「我喜欢」「我习惯」「不要用」「避免」这类词事实类出现具体的版本号、路径、配置值、接口地址模型层则负责判断候选内容是否真的值得长期保存。这里可以用一个轻量模型做二分类prompt 大概是「以下内容是否包含值得跨会话记住的长期信息只回答是或否」。这个判断不需要太强的模型小模型足够。实操中我发现一个反直觉的点不是所有决策都值得记。比如「这次先用方案 A 试试」这种临时决策记下来反而是噪音。真正值得记的是那些「会影响后续多次决策」的信息。所以模型层的 prompt 里要强调「长期性」和「复用性」两个判断维度。3.3 检索排序让对的记忆浮上来检索是记忆系统的门面。存得再好取不出来等于白搭。claude-mem的检索排序我总结成一个公式score 语义相似度 × 0.5 关键词匹配 × 0.2 重要度归一化 × 0.15 时间衰减 × 0.1 使用频率 × 0.05这个权重分配不是拍脑袋定的是我调了好几轮之后的结果。语义相似度占大头因为它最能反映「内容相关」关键词匹配作为补充防止语义模型漏掉精确匹配的情况重要度、时间、频率作为调节项让高质量、新鲜、常用的记忆优先。时间衰减用的是一个简单的指数衰减import math from datetime import datetime def time_decay(last_used_at, half_life_days30): if last_used_at is None: return 0.5 days (datetime.now() - last_used_at).days return math.pow(0.5, days / half_life_days)半衰期设 30 天意思是 30 天没用过的记忆权重降到一半。这个值可以根据你的使用节奏调——如果你项目周期长可以设 60 天如果切换频繁设 14 天也行。使用频率加权用对数函数避免高频记忆过度主导def frequency_weight(use_count): return math.log(use_count 1) / math.log(100)这样用 1 次和用 10 次的差距比线性加权要温和得多。实操心得检索排序的权重不要一次调到位先跑一段时间收集真实使用数据看看哪些记忆该出来没出来、哪些不该出来老出来再针对性调整。我第一版把关键词权重设得过高结果检索出来全是字面匹配但语义不相关的内容后来把语义权重提上去才正常。3.4 记忆注入怎么把检索结果喂给模型检索出记忆之后怎么把它们塞进 prompt 也是有讲究的。最粗暴的做法是把所有检索结果拼成一坨丢进去但这样既浪费 token又可能干扰模型判断。我的做法是分层注入。把检索结果按重要度和相关性分成三档核心记忆top 3高相关直接放进 system prompt作为必须遵守的上下文参考记忆4-10 名放在 user message 开头标注为「可能相关的历史信息」边缘记忆10 名之后不注入只在需要时通过工具调用按需检索这样既保证了关键信息一定被看到又不会让 prompt 被噪音淹没。核心记忆的数量控制在 3 条以内是因为超过这个数模型对每条的记忆强度会下降。注入的格式也很重要。我习惯用这样的结构[历史记忆 - 请作为背景参考] - [决策] 项目使用 PostgreSQL 15不用 MySQL2024-03 确定 - [规范] API 路径统一用 kebab-case不用 camelCase - [偏好] 日志用结构化 JSON 格式方便后续分析每条记忆前面标注类型和日期让模型知道这条信息的性质和新鲜度。日期特别重要——模型看到「2024-03 确定」和「2023-01 确定」对信息时效性的判断完全不同。4. 实操过程与核心环节实现4.1 环境准备与依赖安装动手之前先把环境理清楚。claude-mem这类项目通常是 Python 或 Node.js 写的我以 Python 版本为例讲Node 版本逻辑类似。基础依赖就三样Python 3.10、SQLitePython 内置不用单独装、一个 embedding 模型。embedding 模型我推荐用本地的比如sentence-transformers里的all-MiniLM-L6-v2体积小、速度快、效果够用关键是离线可用不依赖任何外部服务。pip install sentence-transformers sqlite-utils numpy如果你想要更好的中文语义效果可以换成paraphrase-multilingual-MiniLM-L12-v2代价是模型大一点、慢一点。我的建议是先用小的跑通流程觉得效果不够再换大的。目录结构我习惯这样组织claude-mem/ ├── data/ │ └── memories.db # SQLite 数据库 ├── models/ # 本地 embedding 模型缓存 ├── src/ │ ├── store.py # 存储层 │ ├── extract.py # 记忆提取 │ ├── retrieve.py # 检索排序 │ └── inject.py # prompt 注入 └── config.yaml # 配置文件把数据、模型、代码分开好处是备份和迁移的时候目标明确——data/和models/拷走就行代码可以重新拉。4.2 数据库初始化与 FTS 配置初始化脚本我写成一个独立的init_db.py跑一次就行import sqlite3 def init_db(db_path): conn sqlite3.connect(db_path) cur conn.cursor() cur.execute( CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, project TEXT, category TEXT, tags TEXT, importance INTEGER DEFAULT 5, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_used_at DATETIME, use_count INTEGER DEFAULT 0 ) ) cur.execute( CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5( content, tags, contentmemories, content_rowidid ) ) # 触发器保持 FTS 同步 cur.execute( CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN INSERT INTO memories_fts(rowid, content, tags) VALUES (new.id, new.content, new.tags); END ) cur.execute( CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN INSERT INTO memories_fts(memories_fts, rowid, content, tags) VALUES (delete, old.id, old.content, old.tags); END ) cur.execute( CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN INSERT INTO memories_fts(memories_fts, rowid, content, tags) VALUES (delete, old.id, old.content, old.tags); INSERT INTO memories_fts(rowid, content, tags) VALUES (new.id, new.content, new.tags); END ) cur.execute(CREATE INDEX IF NOT EXISTS idx_project ON memories(project)) cur.execute(CREATE INDEX IF NOT EXISTS idx_category ON memories(category)) conn.commit() conn.close()用触发器同步 FTS 表比在应用层手动维护要可靠得多。我早期就是手动同步结果有次更新逻辑漏了 FTS 表导致检索结果跟实际数据对不上排查了半天才发现。触发器虽然有点「魔法」但在这种场景下确实省心。4.3 记忆写入的完整流程写入流程分四步接收原始文本、提取候选记忆、去重、落库。去重这一步很多人会忽略但特别重要。同一个信息被反复存进去检索的时候就会重复出现浪费 token 还干扰排序。我的去重策略是先算新记忆跟已有记忆的语义相似度超过 0.9 就认为是重复不存在 0.8 到 0.9 之间的更新已有记忆的last_used_at和use_count相当于「强化」这条记忆。import numpy as np from sentence_transformers import SentenceTransformer model SentenceTransformer(all-MiniLM-L6-v2) def is_duplicate(new_content, existing_embeddings, threshold0.9): new_emb model.encode(new_content) for emb in existing_embeddings: sim np.dot(new_emb, emb) / (np.linalg.norm(new_emb) * np.linalg.norm(emb)) if sim threshold: return True return False实际跑的时候为了性能我不会每次都跟全库比对而是先用 FTS 检索出 top 20 相似候选再在这 20 条里做语义去重。这样既保证了准确率又把计算量控制住了。落库的时候importance字段的赋值我有一套简单规则决策类默认 8规范类默认 7偏好类默认 6事实类默认 5。这个初始值后续会被使用频率和时间衰减动态调整所以不用太纠结初始值重要的是有个合理的起点。4.4 检索接口的实现与调优检索接口是整个系统调用最频繁的部分性能要重点优化。我的实现分三步结构化过滤、候选召回、重排序。def retrieve(query, projectNone, categoryNone, top_k10): # 第一步结构化过滤 FTS 召回 conn sqlite3.connect(DB_PATH) cur conn.cursor() sql SELECT m.id, m.content, m.category, m.importance, m.last_used_at, m.use_count, bm25(memories_fts) as fts_score FROM memories_fts f JOIN memories m ON f.rowid m.id WHERE memories_fts MATCH ? params [query] if project: sql AND m.project ? params.append(project) if category: sql AND m.category ? params.append(category) sql ORDER BY fts_score LIMIT 50 cur.execute(sql, params) candidates cur.fetchall() # 第二步语义重排 query_emb model.encode(query) scored [] for row in candidates: content_emb model.encode(row[1]) semantic np.dot(query_emb, content_emb) / ( np.linalg.norm(query_emb) * np.linalg.norm(content_emb) ) final_score ( semantic * 0.5 (1 / (1 row[6])) * 0.2 # bm25 越小越相关转成 0-1 (row[3] / 10) * 0.15 time_decay(row[4]) * 0.1 frequency_weight(row[5]) * 0.05 ) scored.append((row[0], row[1], final_score)) scored.sort(keylambda x: x[2], reverseTrue) return scored[:top_k]这里有个性能陷阱对每个候选都实时算 embedding如果候选有 50 条每次检索就要算 50 次编码延迟会很明显。优化方案是预计算并缓存 embedding——写入记忆的时候就把 embedding 算好存起来检索时直接读。SQLite 存二进制 embedding 可以用 BLOB 字段读出来用np.frombuffer还原。# 写入时 emb model.encode(content) cur.execute(INSERT INTO memories (content, embedding, ...) VALUES (?, ?, ...), (content, emb.astype(np.float32).tobytes(), ...)) # 检索时 emb np.frombuffer(row[7], dtypenp.float32)这个优化能把检索延迟从几百毫秒降到几十毫秒体感差异很大。4.5 与对话流程的集成方式记忆系统最终要跟你的 AI 对话流程接起来。集成方式有两种主动注入和工具调用。主动注入是在每次发请求前自动检索相关记忆并拼进 prompt。这种方式对用户透明体验最顺滑缺点是每次都要检索有一定开销而且检索不准的时候会干扰对话。工具调用是把记忆检索做成一个工具让模型自己决定什么时候查。这种方式更灵活模型可以按需检索缺点是模型有时候「忘了」用工具导致该查的时候没查。我的做法是两者结合核心记忆项目级、高重要度主动注入保证基础上下文一直在细节记忆走工具调用让模型按需查。这样既保证了连贯性又保留了灵活性。集成代码大概长这样def build_prompt(user_message, project): # 主动注入核心记忆 core_memories retrieve(user_message, projectproject, top_k3) memory_block \n.join([f- [{m[2]}] {m[1]} for m in core_memories]) system_prompt f你是一个有记忆的助手。 以下是关于当前项目的历史记忆请作为背景参考 {memory_block} 如果需要更多历史信息可以调用 search_memory 工具。 return system_prompt, user_message注意注入的记忆块不要太大控制在 500 token 以内。超过这个量模型对每条记忆的关注度会明显下降而且会挤占正常对话的空间。5. 常见问题与排查技巧实录5.1 检索结果不相关怎么办这是最高频的问题。检索出来的记忆跟当前问题八竿子打不着原因通常有三个。原因一embedding 模型不适合你的语言。如果你主要用中文但用的是纯英文模型语义匹配质量会很差。解决办法是换多语言模型或者用中文语料微调过的模型。我实测下来多语言 MiniLM 在中文场景下比纯英文模型好一大截。原因二查询太短。用户输入「这个怎么改」这种短查询embedding 信息量太少检索质量自然差。解决办法是在检索前做查询扩展——用模型把短查询扩写成更完整的描述再拿扩展后的文本去检索。比如「这个怎么改」扩展成「用户想修改当前讨论的代码或配置需要相关的修改建议和历史决策」。原因三权重配置失衡。前面提过关键词权重过高会导致字面匹配主导。排查方法是把检索结果的各项分数打出来看如果发现某类分数异常主导就调低它的权重。排查流程我整理成一张表现象可能原因排查方法解决方向结果完全不相关embedding 模型语言不匹配看模型是否支持中文换多语言模型结果字面匹配但语义无关关键词权重过高打印各项分数调低关键词权重短查询检索差查询信息量不足看查询长度加查询扩展老记忆频繁出现时间衰减失效检查 last_used_at修复衰减逻辑重复记忆多去重阈值太松看相似度分布调高去重阈值5.2 记忆库膨胀与性能下降用了一两个月之后记忆库可能涨到几千上万条检索开始变慢。这时候要做两件事归档和索引优化。归档是把低价值记忆移出主表。我的标准是use_count 0且created_at超过 90 天的记忆移到memories_archive表。这些记忆不是删除而是「冷存储」需要的时候还能查但不参与日常检索。这个操作能把主表规模控制在一个合理范围。索引优化主要是检查 FTS 表和普通索引是否都建对了。project、category、created_at这几个高频过滤字段都要有索引。另外 SQLite 可以定期跑VACUUM和ANALYZE整理碎片、更新统计信息对性能有提升。VACUUM; ANALYZE;这两个命令我一般一个月跑一次跑完检索延迟能降个 20% 左右。5.3 记忆冲突与更新策略同一个事实前后记了两次但内容矛盾这种情况很常见。比如三个月前记「用 MySQL」上个月记「改用 PostgreSQL」。如果不处理检索的时候两条都出来模型就懵了。我的处理策略是新记忆覆盖旧记忆但保留历史。具体做法是给记忆加一个superseded_by字段新记忆写入时如果检测到跟旧记忆冲突同 project、同 category、语义相似但内容矛盾就把旧记忆标记为「已被取代」检索时默认过滤掉。判断「矛盾」这一步纯靠语义相似度不够需要模型介入。我用一个简单的 prompt 让模型判断两条记忆是否冲突「以下两条记忆是否描述同一事实但内容矛盾只回答是或否。」这个判断不需要太强的模型小模型够用。实操心得冲突检测不要做得太激进。有些记忆看起来矛盾其实是不同场景下的不同结论比如「开发环境用 SQLite生产环境用 PostgreSQL」。这种不是冲突是条件性事实。所以判断冲突的时候要把「条件」也考虑进去别一刀切。5.4 多项目记忆隔离的坑如果你同时维护多个项目记忆隔离一定要做好。我踩过的坑是早期没做隔离结果 A 项目的命名规范被检索到 B 项目里模型给出的建议完全不符合 B 项目的实际情况。隔离的关键是检索时强制带 project 过滤而不是靠检索后再筛。因为如果不带过滤其他项目的记忆会挤占候选名额导致本项目的记忆反而排不进来。但完全隔离也有问题有些通用偏好是跨项目的比如「我喜欢简洁的代码风格」。这种记忆应该标记为project global检索时project IN (current_project, global)既隔离了项目特定信息又保留了通用偏好。sql AND (m.project ? OR m.project global)这个设计我觉得是记忆隔离的最优解既避免了污染又不至于把通用知识也隔离掉。5.5 常见问题速查表把上面这些整理成一张速查表方便你遇到问题时快速定位问题快速排查常用解决检索慢看候选数量、embedding 是否预计算预计算 embedding、加索引检索不准看查询长度、模型语言、权重分布查询扩展、换模型、调权重记忆重复看去重阈值、相似度分布调高阈值、加去重逻辑记忆冲突看是否有 superseded 标记加冲突检测、标记取代跨项目污染看检索是否带 project 过滤强制过滤 global 例外库膨胀看总条数、冷记忆占比归档冷记忆、定期 VACUUM注入 token 过多看注入记忆条数和长度限制 top_k、压缩记忆内容6. 记忆质量的长期维护与迭代搭好系统只是开始真正决定这套东西好不好用的是长期维护。我用了大半年总结出几个让记忆质量持续在线的习惯。每周花十分钟过一遍新增记忆。系统自动提取的记忆不可能 100% 准确每周手动过一遍把明显没用的删掉把表述不清的改清楚。这十分钟的投入能换来一周的高质量检索。每月看一次检索日志。记录每次检索的 query 和返回结果月底翻一翻看看哪些检索效果差、哪些记忆从来没被用到。没被用到的记忆要么是存错了要么是检索没覆盖到两种情况都值得处理。每季度调一次权重。你的使用习惯会变项目阶段会变检索权重的理想配置也会变。季度性地重新评估一下权重分配能让系统跟上你的节奏。记忆内容要「自包含」。一条记忆单独拿出来看应该能看懂不依赖上下文。比如「改用 PostgreSQL」这种记忆就是不合格的应该是「项目 X 的数据库从 MySQL 改用 PostgreSQL原因是需要 JSONB 支持」。自包含的记忆检索出来直接能用不需要再去翻历史。重要度要动态调整。初始重要度只是起点真正重要的是使用反馈。一条记忆被检索到之后如果用户明显采纳了比如后续对话里用上了就把use_count加一如果用户忽略了就不加。长期下来真正有用的记忆会自然浮上来。这套系统我现在的状态是记忆库稳定在两千条左右日常检索延迟 30 毫秒以内检索准确率我自己估摸着在 80% 以上。这个水平谈不上完美但已经能实实在在省下我每天反复「重新介绍背景」的时间。如果你也在做类似的东西我的建议是别追求一步到位先把最小可用版本跑起来然后在真实使用中慢慢调。记忆系统这东西调优的素材只能来自真实使用闭门造车调不出好效果。