
1. 为什么你的 LLM 应用总是“失忆”MemPalace 记忆系统要解决的真实问题如果你正在做 AI Agent、代码助手或者长期陪伴型对话应用大概率遇到过这个场景昨天花了两个小时跟模型对齐了数据库表结构、接口约定和异常处理策略今天新开一个会话它像第一次见面一样问你“请问你想做什么项目”。这不是模型变笨了而是它的上下文窗口本质上是一次性的工作台会话结束工作台就被清空了。很多人第一反应是“那我每次把历史对话全塞进去不就行了”。真这么做你会发现两个问题一是 token 成本线性上涨聊到第三天可能一次请求就要几万 token二是模型在超长上下文里对早期信息的注意力会衰减你塞了 50 轮对话它可能只记得最近 5 轮。所以真正需要的不是“更长的上下文”而是一套能按需召回、持久化存储、并且能被模型主动调用的记忆系统。MemPalace 就是冲着这个痛点来的。它的定位是一个 AI 记忆系统核心思路是把历史对话和决策过程转成向量存进 ChromaDB再通过 MCPModel Context Protocol协议暴露给 LLM让模型在需要的时候自己去“翻记忆”。它和简单的关键词搜索不一样用的是语义向量检索——你哪怕忘了当时那个函数叫什么名字只要描述清楚功能它也能把相关记忆捞回来。这套方案适合谁我总结下来是三类人第一类是在做长期项目的开发者需要 AI 记住架构决策和踩坑记录第二类是在搭 Agent 的工程师希望 Agent 有跨会话的连续性第三类是单纯被“每次都要重新解释背景”折磨烦了的个人开发者。接下来我会把 MCP 服务注册、ChromaDB 初始化、记忆读写接口这一整套最小闭环跑通配置都是可以直接复制的。2. 前置准备TaoToken 接入与 MemPalace 运行环境搭建在动手写记忆系统之前得先把模型调用这条链路打通。MemPalace 本身负责记忆的存取但真正做推理和 embedding 的还是要靠 LLM 服务。我这边用的是 TaoToken 作为模型接入层它的好处是兼容 OpenAI 风格的接口配置起来比较省事而且 MCP 相关的工具调用也能正常走通。先说环境。MemPalace 对 Python 版本有要求建议 3.10 以上因为部分依赖用到了较新的类型语法。我实测在 3.9 上装依赖时会报TypeError: type object is not subscriptable升级到 3.11 就没事了。虚拟环境一定要建ChromaDB 和 embedding 模型的依赖比较重污染全局环境后患无穷。python -m venv mempalace_env source mempalace_env/bin/activate # Windows 用 mempalace_env\Scripts\activate pip install --upgrade pip pip install mempalace chromadb sentence-transformers mcp这里有个细节sentence-transformers第一次运行会自动下载 embedding 模型默认是all-MiniLM-L6-v2大概 80MB。如果你的网络环境下载慢可以提前用huggingface-cli download拉下来放到本地缓存目录然后设置SENTENCE_TRANSFORMERS_HOME环境变量指向它。接下来是 TaoToken 的配置。你需要先去控制台创建一个 API Key然后把它写进环境变量不要硬编码在代码里。我习惯用.env文件管理# .env TAOTOKEN_API_KEYsk-你的实际key TAOTOKEN_BASE_URLhttps://taotoken.net/api MEMORY_DB_PATH./memory_store EMBEDDING_MODELall-MiniLM-L6-v2这里TAOTOKEN_BASE_URL填https://taotoken.net/api就行不要加多余的路径后缀。Key 的获取入口在控制台的 API Keys 页面创建后只显示一次记得当场复制。如果你还没账号从官网进去注册后就能在控制台看到入口。环境变量加载我用的是python-dotenv在代码入口处load_dotenv()一下即可。到这一步模型调用和记忆存储的基础依赖就齐了。下一节开始写真正的配置文件和 MCP 注册。3. 可复制配置ChromaDB 初始化与 MCP 服务注册完整片段这一节是整篇的核心我会把 ChromaDB 的持久化配置、MemPalace 的记忆管理器初始化、以及 MCP 服务的注册配置全部给出来。你直接复制改路径就能用。先看 ChromaDB 的初始化。很多人踩的坑是用了chromadb.Client()这种内存模式结果一重启数据全没了。正确做法是用PersistentClient并指定路径# memory_store.py import os import chromadb from chromadb.config import Settings from sentence_transformers import SentenceTransformer DB_PATH os.getenv(MEMORY_DB_PATH, ./memory_store) EMBEDDING_MODEL os.getenv(EMBEDDING_MODEL, all-MiniLM-L6-v2) # 持久化客户端数据落盘到 DB_PATH client chromadb.PersistentClient( pathDB_PATH, settingsSettings(anonymized_telemetryFalse, allow_resetTrue) ) # 获取或创建集合用 cosine 距离更适合语义检索 collection client.get_or_create_collection( namemempalace_memory, metadata{hnsw:space: cosine} ) # embedding 模型只加载一次全局复用 embedder SentenceTransformer(EMBEDDING_MODEL) def add_memory(memory_id: str, text: str, metadata: dict None): vector embedder.encode(text).tolist() collection.add( ids[memory_id], embeddings[vector], documents[text], metadatas[metadata or {source: conversation}] ) return {status: ok, id: memory_id} def query_memory(query: str, top_k: int 5, threshold: float 0.75): q_vector embedder.encode(query).tolist() results collection.query( query_embeddings[q_vector], n_resultstop_k, include[documents, metadatas, distances] ) # cosine 距离越小越相似这里做阈值过滤 filtered [] for doc, meta, dist in zip( results[documents][0], results[metadatas][0], results[distances][0] ): if dist (1 - threshold): filtered.append({text: doc, metadata: meta, distance: dist}) return filtered注意threshold这里我做了个转换ChromaDB 返回的是 cosine 距离范围 0 到 2距离越小越相似。我习惯用相似度来表达所以判断条件是dist 1 - threshold。如果你直接用距离阈值把这段逻辑改掉即可。然后是 MCP 服务的注册。MCP 的本质是让 LLM 能调用你定义的工具所以我们要把add_memory和query_memory包装成 MCP tool。下面是一个基于mcp库的服务端片段# mcp_server.py import asyncio from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent from memory_store import add_memory, query_memory app Server(mempalace) app.list_tools() async def list_tools(): return [ Tool( namesave_memory, description保存一段对话或决策到长期记忆, inputSchema{ type: object, properties: { memory_id: {type: string}, text: {type: string}, tag: {type: string} }, required: [memory_id, text] } ), Tool( namerecall_memory, description根据语义检索相关历史记忆, inputSchema{ type: object, properties: { query: {type: string}, top_k: {type: integer, default: 5} }, required: [query] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name save_memory: res add_memory( arguments[memory_id], arguments[text], {tag: arguments.get(tag, general)} ) return [TextContent(typetext, textf已保存: {res[id]})] elif name recall_memory: hits query_memory(arguments[query], arguments.get(top_k, 5)) if not hits: return [TextContent(typetext, text没有找到相关记忆)] lines [f[{h[distance]:.3f}] {h[text]} for h in hits] return [TextContent(typetext, text\n.join(lines))] async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ __main__: asyncio.run(main())如果你用的是 Claude Code 或者 Cline 这类支持 MCP 的客户端需要在它们的配置文件里注册这个 server。以 Claude Code 的settings.json为例{ mcpServers: { mempalace: { command: python, args: [/绝对路径/mcp_server.py], env: { TAOTOKEN_API_KEY: sk-你的key, TAOTOKEN_BASE_URL: https://taotoken.net/api, MEMORY_DB_PATH: /绝对路径/memory_store } } } }这里三个要素必须齐全Base URL、API Key、Model ID。Model ID 在 MCP 场景下通常由客户端决定但如果你在代码里直接调模型做 embedding 之外的推理记得把 model 参数显式写上比如gpt-4o-mini或你账号下可用的模型名。路径一定要用绝对路径相对路径在 MCP 启动时的工作目录不确定很容易找不到文件。4. 验证请求一次写入与召回的成功结果演示配置写完了得验证它真的能跑。我设计了一个最小闭环测试先写入三条记忆然后用一个语义相关但用词不同的 query 去召回看能不能命中。先写一个测试脚本# test_memory.py from memory_store import add_memory, query_memory # 写入三条记忆 add_memory(m1, 项目决定使用 PostgreSQL 而不是 MySQL因为需要强事务一致性, {tag: arch}) add_memory(m2, 用户认证采用 JWTtoken 有效期设为 2 小时刷新 token 7 天, {tag: auth}) add_memory(m3, 部署环境用 Docker Compose数据库和 Redis 都挂载了持久化卷, {tag: deploy}) # 用不同措辞召回 print( 查询为什么不用 MySQL ) for hit in query_memory(数据库选型的原因, top_k3): print(hit[text], | distance:, round(hit[distance], 4)) print(\n 查询登录凭证怎么管理的 ) for hit in query_memory(登录凭证怎么管理的, top_k3): print(hit[text], | distance:, round(hit[distance], 4))跑起来的结果大概是这样 查询为什么不用 MySQL 项目决定使用 PostgreSQL 而不是 MySQL因为需要强事务一致性 | distance: 0.2134 查询登录凭证怎么管理的 用户认证采用 JWTtoken 有效期设为 2 小时刷新 token 7 天 | distance: 0.3012可以看到第一个查询里我完全没提“PostgreSQL”或“MySQL”只说了“数据库选型的原因”但系统准确召回了那条架构决策。第二个查询用“登录凭证”去匹配“JWT token”语义上也对上了。这就是向量检索和关键词搜索的本质区别——它理解的是意思不是字面。如果你在 MCP 客户端里测试流程是这样的先让模型调用save_memory存一条比如“用户偏好深色主题字体大小 14px”。然后新开一个会话问“我的界面偏好是什么”模型会主动调用recall_memory把这条记忆捞出来再回答你。我实测下来从写入到跨会话召回整个链路延迟在 200ms 以内embedding 模型加载完之后单次查询基本是毫秒级。这里有个验证技巧你可以故意写一条带具体数字的记忆比如“API 限流阈值设为每分钟 120 次”然后隔一天再问“限流是多少”看它能不能精确召回。如果召回的是模糊的“有限流机制”而不是具体数字说明你的 embedding 模型对数字不敏感可以考虑换all-mpnet-base-v2这类更强的模型代价是体积和延迟会上去。5. 常见报错排查401、local proxy failed、reading choices 与 OAuth 问题这一节我把实际踩过的坑列出来你遇到报错可以直接对照。401 Unauthorized这个最常见九成是 API Key 的问题。先检查.env里的TAOTOKEN_API_KEY有没有多余空格然后确认 Key 没有过期或被删除。还有一种情况是 MCP 客户端启动时没有正确加载 env导致 Key 是空的。排查方法是在mcp_server.py开头加一行print(os.getenv(TAOTOKEN_API_KEY))看输出是不是 None。如果是 None说明客户端的 env 配置没生效检查settings.json里的env字段拼写。local proxy failed这个报错通常出现在 MCP 客户端尝试连接本地 server 时。原因一般是command或args路径写错了客户端找不到mcp_server.py。解决方法是把args里的路径改成绝对路径并且确认python命令在客户端的 PATH 里。如果你用的是虚拟环境command要指向虚拟环境里的 python比如/path/to/mempalace_env/bin/python而不是系统的 python。Error reading choices / reading choices这个报错一般来自模型接口返回格式异常。常见原因是 Base URL 配错了比如多加了/v1或者少了/api。TaoToken 的 Base URL 就是https://taotoken.net/api不要自己拼路径。另外检查一下请求的 model 名是不是你账号下有权限的用了不存在的模型名也会返回非标准结构导致解析失败。OAuth 相关报错如果你在 Claude Code 里看到 OAuth 报错通常是因为客户端在尝试用 OAuth 流程认证但你的 MCP server 是 stdio 模式不走 OAuth。检查一下是不是把 server 类型配错了。stdio 类型的 server 不需要 OAuth只要 env 里有 Key 就行。如果客户端强制走 OAuth去设置里把认证方式改成 API Key。记忆召回为空不是报错但很常见。先确认add_memory真的写进去了用collection.count()看数量。如果数量是 0说明写入失败检查DB_PATH目录权限。如果数量正常但召回为空大概率是阈值太严把threshold从 0.75 降到 0.6 试试。还有一种可能是 embedding 模型不一致——写入和查询用了不同的模型向量空间对不上这种情况只能清库重来。ChromaDB 连接超时如果你看到Failed to connect to chroma之类的错误检查是不是误用了chromadb.HttpClient()。本地场景一律用PersistentClient不要走 HTTP。另外allow_resetTrue只在测试时开生产环境关掉避免误删数据。6. 把记忆系统接进你的工作流从最小闭环到长期可用跑通最小闭环之后下一步是让它真正融入日常开发。我自己的做法是在每次重要的架构讨论或调试结束后手动触发一次save_memory把决策和原因一起存进去。不要只存结论要存“为什么”——比如不要只存“用 Redis 做缓存”要存“用 Redis 做缓存因为会话数据需要跨实例共享且能接受秒级丢失”。这样下次召回时模型拿到的是完整的决策上下文而不是一个孤立的结论。对于 Agent 场景你可以把recall_memory做成一个前置步骤每次用户提问先让模型判断是否需要检索记忆需要就调 tool把召回结果拼进 system prompt 再推理。这个判断逻辑可以用一个轻量的分类 prompt 实现成本很低。长期使用要注意记忆的清理和归档。ChromaDB 本身不会自动淘汰旧数据记忆越堆越多检索精度会下降。我的做法是每个月跑一次归档脚本把超过 90 天且从未被召回过的记忆移到冷存储集合里主集合只保留活跃记忆。这样查询延迟能稳定在低位。如果你还没开始搭建议就从这一篇的配置复制过去先跑通写入和召回再逐步加自动化。记忆系统的价值不在于技术多复杂而在于它真的能让你少重复解释几次背景。等你某天新开会话模型张口就说“上次我们决定用 PostgreSQL 是因为事务一致性”你就知道这套东西值了。需要创建 API Key 或者查看接入文档的话可以从控制台和文档入口进去模型对话入口也可以直接体验一下带记忆的对话效果。长期做编码和 Agent 的话Coding Plan 会更划算一些。