给 AI Agent 装上长期记忆:MemOS 本地部署踩坑记

写在前面

最近在研究MemOS,起因很简单——受够了每次打开 AI 对话都要从头自我介绍一遍。“我是写 Unity 的”、“项目在 E 盘”、“不要给我加一堆 emoji”——这些话我这半年说过的次数,大概比跟我妈打电话还多。

但把 MemOS 装完、跑通、接进日常工作流之后,我发现这个事远不止"让 AI 记住偏好"这么简单。

举个让我印象最深的实际用法:通过 git hook 让 AI 在每次 commit 后自动读取变更的代码、整理成结构化记忆,再写进 MemOS。一开始我只是随手试了下,想看看能不能让 AI 帮我记住"这次改了哪个模块、为什么改、引入了什么副作用"。结果用了几周下来我发现,项目的记忆库在以一种很自然的速度长大——每次提交都会沉淀一点,几百次提交之后,整个仓库的关键决策、模块现状、历史上踩过的坑,几乎都被 MemOS 记下来了。

更妙的是这种记忆是全团队共享的。前端同学想了解"后端那个结算模块最近改了什么、有没有已知问题",不用再去翻 commit 历史、也不用找人开会,直接对着 AI 问一句就行。因为 AI 身后挂着一份由全团队提交行为自动喂养的长期记忆库,它能告诉你:这个模块最近两周被 A 重构过一次、重构动机是性能问题、重构后 B 上报过一个边界 bug 已经修复、当前已知未修的问题还有两个……不论是哪个模块的人来提问,都能立刻拿到这个模块的现状和经典问题

这种体验跟过去"每次接手一个陌生模块都要重新考古"是完全两个世界的事。它让 AI 从一个"聊天工具"真的变成了团队的记忆中枢

而这一切的背后,其实牵扯到一个今年被反复讨论的概念——Harness Engineering。OpenAI 年初发的那篇博客里有句话我印象很深:Agent = Model + Harness,模型决定上限,Harness 决定底线。而记忆,正是 Harness 里最难也最关键的一层。

而这个认知并不是凭空冒出来的。我最近也在用 LangGraph 做一些 Agent 开发,被"上下文纯净"这个问题折磨过不少次——后面会专门展开聊,这里先埋个伏笔。很多时候我们以为自己在调 prompt、调模型参数,其实真正在解决的,都是记忆和上下文的问题。

所以当看到 MemTensor 开源的 MemOS 项目时,我的第一反应是:“行,这回得让它把我们记住。”

这篇文章想尽量讲人话,把它是什么、为什么这么设计、它在 Harness 里处于什么位置、怎么装、哪里容易踩坑都讲清楚。如果大家也打算本地跑一套长期记忆系统,希望能帮忙少走两个小时的弯路。

项目地址在这:https://github.com/MemTensor/MemOS


一、先聊聊"记忆"这件事到底难在哪

向量数据库加 RAG 这套方案大家应该都不陌生,网上教程一抓一大把。但真用起来就会发现,它解决的其实是"检索"问题,而不是"记忆"问题。

这两件事差别挺大的。

检索是这样的:我们有一堆文档,用户问一个问题,我们从里面捞最相关的几段拼进 prompt。它是无状态的、单次的、只关心相似度。

那记忆是什么?记忆是我们得回答一串更麻烦的问题:

这条信息值不值得记?去年用户说"我喜欢草莓",今年说"我对草莓过敏了",到底信哪条?张三的偏好能不能让李四的 Agent 看见?上周那个已经解决的 bug,要不要一直留在召回结果里污染后面的对话?用户吐槽说"你怎么还记得那个,我早换项目了",我们怎么把这条记忆优雅地淡忘掉?

如果直接往向量库里塞 chunk,上面这些问题一个都答不了。跑个一两周倒没事,跑三个月就会发现:记忆库越来越脏,召回越来越飘,用户画像里到处都是互相矛盾的片段。

MemOS 想做的事,用一句话概括就是:把"记忆"从一个存储问题,变成一个治理问题

它的设计哲学很有意思,名字就叫"Memory OS"——把记忆当成操作系统里的资源来管。操作系统管 CPU、内存、磁盘、进程;那 AI 时代也得有个东西来管用户记忆、项目记忆、Agent 经验、可复用技能。这个类比我一开始觉得有点大,用了之后才发现还挺贴切。


二、绕不开的话题:Harness Engineering

聊 MemOS 之前,得先聊一下今年(2026 年初)被反复刷屏的一个概念——Harness Engineering。因为我后来才意识到,它是理解 MemOS 价值的最佳切入点。

起因是 OpenAI 那篇《Harness Engineering: leveraging Codex in an agent-first world》。这篇博客抛出了一个很扎心的观察:Agent = Model + Harness。模型本身只是一个"文本输入输出的函数",真正让它变成一个可用的智能体,是围绕模型搭建的整套外围系统。

有个数据挺震撼:Can.ac 的实验里,同一个模型,只是换了文件编辑工具的调用方式,编码基准分数从 6.7% 直接跳到 68.3%。模型没变,变的是外围的那套系统。这让大家一下子想明白了过去两年的一个困惑——为什么我们都接同一个 GPT-4 / Claude,做出来的 Agent 体验差这么多? 瓶颈从来不在模型智能本身,而在它外面那一层骨架。

从 Prompt Engineering 到 Harness Engineering

Harness Engineering 不是凭空冒出来的,它是一条演化线的终点。业内现在把这三层关系梳理得很清楚:

graph LR
    A["<b>Prompt Engineering</b><br/>怎么写好指令<br/><i>解决:表达</i>"] --> B["<b>Context Engineering</b><br/>给模型看什么<br/><i>解决:信息</i>"]
    B --> C["<b>Harness Engineering</b><br/>整个系统怎么跑<br/><i>解决:执行</i>"]

    style A fill:#e3f2fd,stroke:#1976d2
    style B fill:#fff3e0,stroke:#f57c00
    style C fill:#fce4ec,stroke:#c2185b

三者是逐层包裹的关系,不是互相替代:

  • Prompt Engineering:塑造局部概率空间,让模型听懂意图。最早流行的"咒语工程"、思维链、Few-shot 都是这一层。它解决的是表达问题。
  • Context Engineering:确保模型在合适的时机拿到正确且必要的事实。RAG、记忆注入、Token 预算管理属于这一层。它解决的是信息问题。
  • Harness Engineering:长链路任务里系统怎么防崩、怎么量化、怎么持续运转。它解决的是执行问题。

在生产环境里真正决定成败的,是最外面那层 Harness。简单任务靠 prompt 就够,知识密集型任务靠 context,但一旦 Agent 要连续跑几十轮、甚至几小时几天,没有 Harness 顶着就是一碰就碎。

Harness 的六层架构

业内目前普遍把 Harness 切成六层。每一层解决一个独立问题,合在一起形成从"定义边界"到"兜底恢复"的完整闭环:

层级 名称 解决什么 典型组件
L1 信息边界层 Agent 该知道什么、不该知道什么 系统提示词、角色定义、任务范围界定
L2 工具系统层 怎么跟外部世界交互 MCP 工具、API、文件系统、沙箱
L3 执行编排层 多步骤任务怎么串起来 状态机、工作流、任务分解
L4 记忆与状态层 长任务中间结果怎么管、跨会话怎么记住 对话历史、进度文件、长期记忆库、上下文压缩
L5 评估与观测层 Agent 怎么知道自己做对了没 浏览器自动化验证、测试沙箱、独立 Evaluator
L6 约束、校验与恢复层 出错了怎么办 Linter、结构测试、重试、回滚

如果把这六层画成一张图,L4 的位置看起来就很有意思——它夹在编排和评估之间,上下四层都要从它这里读写状态:

graph TB
    subgraph Harness["Agent Harness"]
        L1["<b>L1 · 信息边界层</b><br/>角色 / 范围 / 提示词"]
        L2["<b>L2 · 工具系统层</b><br/>MCP / API / 沙箱"]
        L3["<b>L3 · 执行编排层</b><br/>状态机 / 工作流"]
        L4["<b>L4 · 记忆与状态层</b> ⭐<br/>对话历史 / 长期记忆 / 进度"]
        L5["<b>L5 · 评估与观测层</b><br/>验证 / Evaluator"]
        L6["<b>L6 · 约束与恢复层</b><br/>Linter / 重试 / 回滚"]
    end

    Model["<b>LLM Model</b><br/>(GPT / Claude / Qwen...)"]

    L1 -.依赖状态.-> L4
    L2 -.写入痕迹.-> L4
    L3 -.读写进度.-> L4
    L5 -.读取基准.-> L4
    L6 -.读写检查点.-> L4

    L1 --> Model
    L2 --> Model
    L3 --> Model

    style L4 fill:#fce4ec,stroke:#c2185b,stroke-width:3px
    style Model fill:#e8f5e9,stroke:#388e3c

这六层里,L1、L2、L3 是相对容易补的——系统提示词可以写、工具可以接 MCP、编排有现成的 LangGraph / CrewAI / AutoGen。L5、L6 也有成熟思路——测试和重试本质上是软件工程里就存在的东西,搬过来改改就能用。

但 L4 记忆与状态层,是这六层里最没有"现成轮子"的一层。

它难在哪?难在它不是一次性工程,而是需要随系统生命周期持续演化的能力。每多一次对话就产生新的记忆,每多一个用户就多一份画像,每多一条错误就可能污染后续的召回。这种"活的"状态没法用一个静态数据库解决,也没法靠几个 prompt 补上。

更麻烦的是,L4 的好坏会直接反噬其他五层(看上图的虚线)。记忆错了 → L1 的边界就飘了;长期状态丢了 → L3 的编排就断了;中间产物没管好 → L5 的评估就失去基准。L4 像 Harness 的中枢神经,其他层再整齐,中枢一出问题全身抽搐。

为什么 L4 是"记忆 + 状态"?一个关键的 40% 观察

L4 之所以叫"记忆与状态层"而不是单纯"记忆层",是因为它要管的其实是三类东西:

  1. 当前任务的中间状态(短期):这一轮里已经完成了什么、待办清单、变量值
  2. 会话内的对话历史(中期):前 N 轮说过什么、做过什么决策
  3. 跨会话的长期记忆(长期):用户偏好、项目约定、可复用的经验

这三类混在一起塞进 prompt,就是大家常说的"上下文爆炸"。Anthropic 的工程实践里有个非常关键的观察:

当上下文利用率超过约 40%,Agent 的输出质量就开始明显下降——幻觉增多、开始兜圈子、格式混乱、生成低质代码。

这条线叫"Smart Zone / Dumb Zone 分界"。听起来反直觉——现代模型号称支持 200K、1M 上下文——但实测下来,“能装下” ≠ “用得好”。模型把所有 token 读进来了,不代表它能在里面精准找到该关注的部分。

画成图大概是这样:

graph LR
    A["0%"] -->|"<b>Smart Zone</b><br/>推理聚焦<br/>工具调用准确<br/>代码质量高"| B["~40%"]
    B -->|"<b>Dumb Zone</b> ⚠️<br/>幻觉增多<br/>开始兜圈子<br/>格式混乱"| C["100%<br/>(爆炸)"]

    style A fill:#c8e6c9,stroke:#2e7d32
    style B fill:#fff9c4,stroke:#f57f17,stroke-width:3px
    style C fill:#ffcdd2,stroke:#c62828

40% 这条线是 L4 设计的北极星。所有的记忆选型、召回策略、top_k 默认值、甚至去重算法,本质上都是在为这条线服务。

Anthropic 对此给出了一个非常工程化的解法:Context Resets。当上下文接近饱和时,不做压缩,直接启动一个全新的干净 Agent,用结构化交接文档把必要状态传过去。这个做法的灵感来自程序遇到内存泄漏时的处理——不去手动释放每一个内存块,直接重启进程、从检查点恢复。

但 Context Resets 只是事后兜底。更根本的解法是事前不让垃圾进主上下文——这就是记忆系统要做的事。

回到 MemOS

说白了,L4 干不好,Agent 的下限就被按死了。模型再强,架不住每轮对话都从零开始自我介绍;工具再多,架不住用户偏好永远记不住;编排再精巧,架不住跑到第 30 轮开始胡言乱语。

而 MemOS,就是一个专门攻 L4 的系统。它不是在重复造 L2 的工具框架(那些都有了),也不是在抢 L3 的编排生态(LangGraph 之流已经站住了),它瞄准的就是 Harness 里最难、最没人啃的那块骨头

这也是为什么我说,记忆不是"锦上添花"的 feature。在 Harness 的视角下,它是整套 Agent 系统能不能从 demo 走向生产的关键开关。模型决定你能做多精彩的事,Harness 决定你能做多久而不崩——而记忆,正是 Harness 里最靠近"多久"那个字的一层。


三、MemOS 到底给了我们什么

回到 MemOS。如果把它放到 Harness 的六层架构里看,它干的就是 L4 这件事——而且是目前开源生态里,把 L4 做得最完整的一个项目

简单讲一下用下来感受最深的几个点,不照搬官方列表。

它提供了一个统一的 API,但重点不在 API,在"统一"两个字。我们不用再自己写"如何判断这条记忆该更新还是新增"这种逻辑了,MemOS 里有一整套生命周期机制。只管往里丢消息,它自己会决定抽取出什么、合并到哪里、要不要去重。

Memory Cube 是个值得琢磨的概念。一开始我以为它就是"知识库"的另一个叫法,后来发现不是。Cube 更像一个带权限的记忆盒子。我们可以按用户开一个、按项目开一个、按 Agent 开一个,然后用 user_idmem_cube_id 两个维度去组合。这在多 Agent 协作的时候特别实用——比如有一个写代码的 Agent 和一个跑测试的 Agent,我们希望它们共享"这个项目的结构约定",但各自保留自己的执行轨迹,Cube 这个抽象就能直接满足。

图 + 向量的混合存储。它后面挂着 Neo4j 和 Qdrant:向量负责语义召回,图负责关系结构。我一开始看到要装两个数据库有点劝退,但实际用下来,图数据库的价值是语义检索给不了的——"用户喜欢冰美式"和"用户讨厌苦味"之间的联系,是图能表达而 embedding 表达不了的。

MemReader 是个独立的模型角色。这个设计挺聪明。普通聊天用 qwen2.5,但"从对话里抽取长期记忆"这件事,官方专门训了一个 MemReader-4B 的小模型来做。这就像我们写代码的时候不会让同一个人既写业务又审代码,分工明确反而效率高、质量稳。

支持反馈修正。这个能力说起来简单,做得好不容易。跟 Agent 说一句"这条记错了",它能真的把对应的记忆找出来改掉,而不是把这句话当成新对话再塞一遍。


四、部署选型思路

先把目标摆出来:数据留在本地、不花一分钱 API 费用、Windows 环境下能跑

基于这个目标,我的选型是这样:

推理层用 Ollama。Ollama 在本地跑模型这块儿已经很成熟了,而且自带 OpenAI 兼容接口,MemOS 不用改代码就能接上。

主对话模型选 qwen2.5:7b。7B 在本地跑速度可以接受,中文也不拉胯。显存足够的话,换 14B 当然更好。

记忆抽取模型用 hf.co/mradermacher/MemReader-4B-GGUF。这是官方指定的 MemReader 模型的 GGUF 量化版本,4B 够用,速度还挺快。

Embedding 用 bge-m3。这个模型做中文 embedding 基本是闭眼选,1024 维,和 MemOS 默认配置对得上。

图数据库 Neo4j Community 版,向量数据库 Qdrant。两个都免费,都好装。

整体架构长这样:

graph TB
    Client["<b>用户 / Agent / MCP 客户端</b>"]
    Server["<b>MemOS API Server</b><br/>localhost:8000"]

    subgraph LLM["Ollama · :11434"]
        M1["qwen2.5:7b<br/><i>主对话</i>"]
        M2["MemReader-4B<br/><i>记忆抽取</i>"]
        M3["bge-m3<br/><i>embedding</i>"]
    end

    Neo["<b>Neo4j</b> · :7687<br/>图关系存储"]
    Qdrant["<b>Qdrant</b> · :6333<br/>向量检索"]

    Client -->|HTTP| Server
    Server -->|对话/抽取/向量| LLM
    Server -->|图关系读写| Neo
    Server -->|向量读写| Qdrant

    style Client fill:#e3f2fd,stroke:#1976d2
    style Server fill:#fce4ec,stroke:#c2185b,stroke-width:2px
    style LLM fill:#fff3e0,stroke:#f57c00
    style Neo fill:#e8f5e9,stroke:#388e3c
    style Qdrant fill:#e8f5e9,stroke:#388e3c

听起来组件挺多,但装起来一个下午能搞定。


五、组件一个个装起来

5.1 Python 环境

MemOS 后端是 Python,要 3.10 以上。我用的是 3.11,没翻车。

如果机器上装了一堆版本的 Python,建议用 py -3.11 或者 venv 明确一下,不然后面 uvicorn 能不能找到是个玄学问题。

5.2 Ollama 和三个模型

Ollama 直接去 https://ollama.com/ 下一个安装包,装完默认就跑在 11434 端口。

然后依次拉模型:

1
2
3
ollama pull qwen2.5:7b
ollama pull bge-m3
ollama pull hf.co/mradermacher/MemReader-4B-GGUF

这三个加起来差不多 10 个 G,看网速慢慢等。拉完用 ollama list 确认一下都在。

小提示:Ollama 默认不会开机自启。每次重启电脑要记得手动启动它,不然后面 MemOS 一堆接口会直接连不上。我后来把它设成开机启动了。

5.3 Neo4j

https://neo4j.com/download/ 下 Community 版。装完启动 Neo4j Desktop,手动创一个本地数据库,设一个密码——这个密码后面要填进 .env 里,所以定啥都行,但要和 .env 对得上。

默认用户名是 neo4j,连接地址 bolt://localhost:7687

我第一次启动 MemOS 连不上 Neo4j,盯着日志看了十分钟才发现问题:Neo4j 装完默认是不启动数据库的,要在 Desktop 里手动点 “Start”。这个锅不在 MemOS,是 Neo4j 这些年一贯的毛病。

5.4 Qdrant

Qdrant 最舒服的地方是 Windows 下直接一个 exe 就能跑,不用装。

https://github.com/qdrant/qdrant/releasesqdrant-x86_64-pc-windows-msvc.zip,解压出来的 qdrant.exe 放到 MemOS 项目根目录下的 qdrant/ 子目录里:

1
2
3
4
5
6
MemOS/
├── qdrant/
│ └── qdrant.exe ← 放这里
├── src/
├── docker/
└── .env

路径约定是为了配合后面那个启动脚本,想放别的地方也行,脚本里改下就好。

启动后打开 http://localhost:6333/readyz 能看到响应就说明没问题。

5.5 克隆 MemOS 并装依赖

1
2
3
git clone https://github.com/MemTensor/MemOS.git
cd MemOS
pip install -r ./docker/requirements.txt

requirements.txt 放在 docker/ 目录下这个设计有点反直觉,我第一次找了半天以为没有 requirements。


六、.env 文件,重点来了

配置这块儿是最容易出问题的地方。我把实际用的 .env 贴出来,一条条解释。敏感信息都替换成占位符了,大家按自己情况改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# MemOS Environment Variables

## Base
TZ=Asia/Shanghai
MOS_CUBE_PATH=D:/Memos/MemOS/data
MEMOS_BASE_PATH=.
MOS_ENABLE_DEFAULT_CUBE_CONFIG=true
MOS_ENABLE_REORGANIZE=false
MOS_TEXT_MEM_TYPE=general_text
ASYNC_MODE=sync

## User/session defaults
MOS_TOP_K=50

## Chat LLM - using Ollama local model
MOS_CHAT_MODEL=qwen2.5:7b
MOS_CHAT_TEMPERATURE=0.8
MOS_MAX_TOKENS=2048
MOS_TOP_P=0.9
MOS_CHAT_MODEL_PROVIDER=openai
OPENAI_API_KEY=ollama
OPENAI_API_BASE=http://localhost:11434/v1

## MemReader - 记忆抽取
MEMRADER_MODEL=hf.co/mradermacher/MemReader-4B-GGUF
MEMRADER_API_KEY=ollama
MEMRADER_API_BASE=http://localhost:11434/v1
MEMRADER_MAX_TOKENS=5000

## General LLM - 合并/改写/过滤
MEMREADER_GENERAL_MODEL=qwen2.5:7b
MEMREADER_GENERAL_API_KEY=ollama
MEMREADER_GENERAL_API_BASE=http://localhost:11434/v1

## Embedding
EMBEDDING_DIMENSION=1024
MOS_EMBEDDER_BACKEND=ollama
MOS_EMBEDDER_MODEL=bge-m3
OLLAMA_API_BASE=http://localhost:11434

## Reranker
MOS_RERANKER_BACKEND=cosine_local
MOS_RERANKER_STRATEGY=single_turn

## Internet search & preference memory
ENABLE_INTERNET=false
SEARCH_MODE=fast
ENABLE_PREFERENCE_MEMORY=false

## Reader chunking
MEM_READER_BACKEND=simple_struct
MEM_READER_CHAT_CHUNK_TYPE=default
MEM_READER_CHAT_CHUNK_TOKEN_SIZE=1600
MEM_READER_CHAT_CHUNK_SESS_SIZE=10
MEM_READER_CHAT_CHUNK_OVERLAP=2

## Scheduler
MOS_ENABLE_SCHEDULER=false
API_SCHEDULER_ON=true
MEMSCHEDULER_USE_REDIS_QUEUE=false

## Graph / vector stores
NEO4J_BACKEND=neo4j-community
NEO4J_URI=bolt://localhost:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=your_password
NEO4J_DB_NAME=neo4j
MOS_NEO4J_SHARED_DB=false

QDRANT_HOST=localhost
QDRANT_PORT=6333

几个关键点得解释一下,不然容易懵。

关于 OPENAI_API_BASE=http://localhost:11434/v1:别被变量名骗了。Ollama 提供了一套兼容 OpenAI 格式的接口,所以 MemOS 哪怕变量叫 OPENAI,实际上打的是 Ollama。OPENAI_API_KEY=ollama 这一行的 ollama 是个占位字符串,Ollama 不校验 key,写 sk-whatever 也行,但约定俗成写 ollama 比较好认。

关于 MEMRADER_MODEL 这个拼写:对,是 MEMRADER 不是 MEMREADER,官方环境变量拼错了一个字母,我看 issue 区有人吐槽过。按项目里实际的写法来就对了,别自作主张改成 MEMREADER,不然读不到。

关于 EMBEDDING_DIMENSION=1024:这个必须和用的 embedding 模型维度一致。bge-m3 是 1024 维。如果换了 embedding 模型记得改。Qdrant 的 collection 一旦建了维度就定了,改维度要么删 collection 要么建新的。

关于 ASYNC_MODE=sync:本地玩就用同步,调试方便。异步模式要配合 Redis,对新手来说是个大坑,先别碰。

关于 MOS_ENABLE_REORGANIZE=false:这个我特地关掉了。开启后 MemOS 会周期性地重组记忆图,对本地玩耍来说太重,CPU 会一直不闲着。

关于 MOS_RERANKER_BACKEND=cosine_local:原生支持本地 cosine 排序,不需要额外部署 rerank 服务。bge-reranker 效果更好,但要多一个服务,不划算。

关于 MOS_CUBE_PATH:这是本地数据落盘的路径,别放 C 盘临时目录。我就因为 Windows 清理的时候把它当缓存删了一次,心态当场崩溃。


七、启动脚本

因为要同时拉起 Qdrant 和 uvicorn,我写了个 bat 脚本,双击就能跑。代码有点长但逻辑很直白:找 Python → 检查 uvicorn → 检查 Qdrant → 启 Qdrant → 等它 ready → 启 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
@echo off
title MemOS Server
cd /d "%~dp0"

:: === Config ===
set QDRANT_DIR=%~dp0qdrant
set QDRANT_EXE=%QDRANT_DIR%\qdrant.exe

:: === Find Python ===
set PYTHON_EXE=
if exist "%LOCALAPPDATA%\Programs\Python\Python311\python.exe" (
set "PYTHON_EXE=%LOCALAPPDATA%\Programs\Python\Python311\python.exe"
goto :found_python
)
if exist "%LOCALAPPDATA%\Programs\Python\Python312\python.exe" (
set "PYTHON_EXE=%LOCALAPPDATA%\Programs\Python\Python312\python.exe"
goto :found_python
)
if exist "%LOCALAPPDATA%\Programs\Python\Python313\python.exe" (
set "PYTHON_EXE=%LOCALAPPDATA%\Programs\Python\Python313\python.exe"
goto :found_python
)
if exist "%LOCALAPPDATA%\Programs\Python\Python310\python.exe" (
set "PYTHON_EXE=%LOCALAPPDATA%\Programs\Python\Python310\python.exe"
goto :found_python
)
if exist "C:\Python311\python.exe" (
set "PYTHON_EXE=C:\Python311\python.exe"
goto :found_python
)
if exist "C:\Python312\python.exe" (
set "PYTHON_EXE=C:\Python312\python.exe"
goto :found_python
)

echo [ERROR] Python not found.
echo Please install Python 3.10+ : https://www.python.org/downloads/
pause
exit /b 1

:found_python
echo Python: %PYTHON_EXE%
"%PYTHON_EXE%" --version
echo.

:: === Check uvicorn ===
"%PYTHON_EXE%" -m uvicorn --version >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] uvicorn not installed.
echo Run: "%PYTHON_EXE%" -m pip install uvicorn
pause
exit /b 1
)

:: === Check Qdrant ===
if not exist "%QDRANT_EXE%" (
echo [ERROR] Qdrant not found: %QDRANT_EXE%
echo Download: https://github.com/qdrant/qdrant/releases/latest
echo Extract qdrant.exe to: %QDRANT_DIR%\
pause
exit /b 1
)

:: === Start Qdrant ===
echo [1/2] Starting Qdrant ...
start "Qdrant" /min "%QDRANT_EXE%"

echo Waiting for Qdrant ...
:wait_qdrant
timeout /t 2 /nobreak >nul
curl -s -o nul -w "" http://localhost:6333/readyz 2>nul
if %errorlevel% neq 0 goto wait_qdrant
echo Qdrant ready (localhost:6333)
echo.

:: === Start API server ===
echo [2/2] Starting API server ...
"%PYTHON_EXE%" -m uvicorn memos.api.server_api:app --host 0.0.0.0 --port 8000

:: === Cleanup ===
echo Stopping Qdrant ...
taskkill /f /im qdrant.exe >nul 2>&1
pause

跑之前必须手动先启动的:Ollama、Neo4j 这两个。脚本只管 Qdrant 和 API。

一切顺利的话,控制台会看到 uvicorn 的 banner 跳出来,监听 8000 端口。这时候打开浏览器访问 http://localhost:8000/docs,Swagger 文档应该就出来了。看到这个界面我第一次长舒了一口气。

如果看到 uvicorn 起得慢慢的、而且控制台在狂刷连接错误,十有八九是 Neo4j 没启动或者密码不对。


八、第一次调用:让它记住我们

服务起来了,来试试最经典的 “add 再 search” 流程。

写入一条记忆:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests, json

requests.post(
"http://localhost:8000/product/add",
headers={"Content-Type": "application/json"},
data=json.dumps({
"user_id": "user-001",
"mem_cube_id": "cube-personal",
"messages": [
{"role": "user", "content": "我平时主要写 Unity 和 C#,不喜欢回答里有一堆 emoji。"}
],
"async_mode": "sync"
}, ensure_ascii=False)
).json()

因为是 sync 模式,这个请求会等到 MemReader 抽完记忆、存进 Neo4j 和 Qdrant 才返回。我本地 7B 模型大概 5-10 秒。

这 5-10 秒里 MemOS 实际做了这些事:

sequenceDiagram
    autonumber
    participant C as 客户端
    participant S as MemOS Server
    participant R as MemReader-4B<br/>(Ollama)
    participant E as bge-m3<br/>(Ollama)
    participant N as Neo4j
    participant Q as Qdrant

    C->>S: POST /product/add<br/>原始对话
    S->>R: 从对话里抽取结构化记忆
    R-->>S: ["用户写 Unity/C#",<br/> "不喜欢 emoji"]
    S->>E: 给每条记忆算 embedding
    E-->>S: vector[1024]
    par 并行落盘
        S->>N: 写图节点 + 关系
    and
        S->>Q: 写向量 + 元数据
    end
    S-->>C: 200 OK

这条链里最慢的是 MemReader 那一步(LLM 抽取),不是数据库写入——这就是为什么后面我们会把写入改成 async_mode: "async"

然后查一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests, json

res = requests.post(
"http://localhost:8000/product/search",
headers={"Content-Type": "application/json"},
data=json.dumps({
"query": "用户的技术栈和输出偏好?",
"user_id": "user-001",
"mem_cube_id": "cube-personal"
}, ensure_ascii=False)
).json()

print(res)

如果一切正常,返回里会有抽出来的两条记忆:用户主要使用 Unity 和 C#;用户不喜欢回答里有 emoji。

第一次看到它真的把我说过的话整理成结构化记忆,然后用中文问题也能召回回来的时候,感觉挺魔幻的。这不只是"文本搜索",它是在对话基础上做了一层抽象。

注意 user_idmem_cube_id 这两个字段:上面用了 user-001cube-personal 这种可读的字符串,但实际上随便填什么都行。官方示例里用 UUID 是好习惯,本地玩不用那么严谨。要紧的是:同一个人的数据要保持同一个 user_id,不然 MemOS 会以为是两个不同的人


九、接 MCP:让记忆真正进入工作流

服务跑起来、脚本也能 add/search 了,但这时候离"好用"还差一大步。

因为如果每次想让 AI 记住点什么,都得手写 requests.post 那是反人类的。记忆系统真正发挥价值,是要让它以 Agent 原生的方式嵌进日常工作流里——换句话说,AI 自己决定什么时候查记忆、什么时候写记忆,我们不用管。

这就是 MCP(Model Context Protocol)存在的意义。

MemOS 官方在 apps/memos-local-plugin 里给 OpenClaw 和 Hermes Agent 提供了本地插件,底层就是通过 MCP 暴露记忆能力。但我用的 Agent 是 CodeBuddy,加上 MemOS 服务是本地部署、API 和云版有差异,我干脆基于官方的 MCP 魔改了一个只认本地 MemOS v1.0.1 的版本,项目叫 memos-api-mcp。下面这节讲讲它的设计。

9.1 设计原则:零封装、一比一对齐

MemOS 的 HTTP API 字段非常多(光 search_memory 一个接口就有 20 多个参数),很容易做成那种"我帮你包装一下,只暴露常用的几个"的 MCP。但我试过一段时间后放弃了这种思路。

原因很简单:封装层是个包袱。MemOS 在快速迭代,上游加一个字段,封装层就得跟着改;而且每次封装都意味着一次抽象泄漏——你以为"我只需要这几个常用参数",直到你真需要那个被藏起来的参数的时候。

所以魔改版的设计原则是工具入参和 MemOS Server 的 OpenAPI schema 一比一对齐。MCP 这一层做且只做三件事:

  • 把 stdio 协议翻译成 HTTP 请求
  • 从环境变量注入 user_id(避免每次都要传)
  • 用 Zod schema 给 LLM 做参数校验和描述

除此之外什么都不做。LLM 能看到的每一个字段,都是上游 API 真实存在的字段,行为和上游完全一致。

9.2 暴露的五个工具

不是我原先提的三个,实际上是五个,每个对应 MemOS 的一个 REST 端点:

MCP 工具 对应 API 用途
add_message POST /product/add 保存消息、抽取记忆
search_memory POST /product/search 检索候选记忆(回答前必调)
delete_memory POST /product/delete_memory 按 ID / filter 删除
add_feedback POST /product/feedback 对已有记忆提交反馈或修正
get_user_profile POST /product/get_memory 分页导出某个 cube 的全部记忆

这里有个设计细节我想单独说一下:get_user_profile 这个名字是故意起得不一样的

上游 API 叫 /product/get_memory,直译就是 get_memory。但我发现 LLM 看到 get_memory 这个工具名,会在很多不该用它的场景下调它——因为"获取记忆"听起来太像 search_memory 的同义词了。改成 get_user_profile 之后,LLM 会很清楚这是"把某个用户的完整画像拉出来",只有真的需要全量记忆时才调,调用频次立刻合理了。

工具名是给 LLM 看的,不是给程序员看的——这个教训可以记一下。

9.3 强制工作流:search → answer → add

光有工具不够,还得告诉 Agent 什么时候用哪个。否则你会看到各种离谱行为:回答完了才想起来查记忆、同一个偏好被重复写入十几次、或者压根忘了写。

我在 MCP 的系统提示里写死了一套强制工作流,每一轮对话都要按下图跑完:

sequenceDiagram
    autonumber
    participant U as 用户
    participant A as Agent (LLM)
    participant M as memos-api-mcp
    participant S as MemOS Server

    U->>A: 提问
    rect rgb(232, 245, 233)
        Note over A,S: ① 回答前:先查记忆
        A->>M: search_memory<br/>(query + filter)
        M->>S: POST /product/search
        S-->>M: 相关记忆 top_k 条
        M-->>A: 结构化记忆
    end

    rect rgb(255, 243, 224)
        Note over A,U: ② 结合记忆作答(只用相关的,忽略噪声)
        A->>U: 给出回答
    end

    rect rgb(227, 242, 253)
        Note over A,S: ③ 回答后:异步沉淀本轮对话
        A->>M: add_message<br/>(user+assistant, info)
        M->>S: POST /product/add
        S-->>M: 202 Accepted
        Note over S: MemReader 异步抽取<br/>写入 Neo4j/Qdrant
    end

翻译成人话:

  1. 回答前必须先调 search_memory,用 filter 精确限定本项目范围;
  2. 只使用相关的那几条记忆,无关的噪声直接忽略;
  3. 回答后必须调 add_message,把本轮的 user + assistant 消息和自定义属性写进去。

三步里最容易被 LLM 偷懒省掉的是第 ③ 步——它已经把答案给用户了,从 LLM 的视角看任务已经结束了。所以提示词里要特别强调"无论用户说什么都要执行 add",否则记忆库永远不会积累。

这条工作流看起来死板,但实测下来是让 AI 真的"学会用记忆"的最关键一步。没有它,LLM 会把记忆工具当成可选 feature;有了它,记忆变成呼吸一样的本能动作。

9.4 filter:真正让记忆可用的东西

魔改版里我花时间最多的一块,其实不是工具列表,而是 filter 参数。

MemOS 本身就支持 filter,但上游文档写得比较抽象,而且有一个反直觉的规则很容易踩坑——写入时放在 info 里的字段,查询时要当成顶层扁平字段来过滤,不能带 info. 前缀。

举个例子,所有项目记忆写入时都带上了 info 结构:

1
2
3
4
5
6
7
8
{
"info": {
"app_id": "demo_project",
"module": "Backend",
"scene": "coding",
"keywords": ["python", "deploy"]
}
}

写入后,MemOS 会把 info 里的所有键平铺到 memory 顶层。所以 search 的时候就像访问顶层字段一样过滤:

1
2
3
4
5
6
7
8
9
10
11
{
"filter": {
"and": [
{ "app_id": "demo_project" },
{ "or": [
{ "scene": "coding" },
{ "scene": "debug" }
]}
]
}
}

这里特别要注意两点:

第一,filter 必须用 and / or 结构包起来。不能传裸单字段——即使只有一个条件,也得写成 {"and": [{"project_id": "x"}]},否则后端直接 400。

第二,不要写成 info.app_id。我踩过这个坑:想当然地以为既然字段写进了 info,查询时就该加 info. 前缀——结果怎么过滤都是空结果。看了一天日志才发现 MemOS 做了平铺,查询时反而不需要前缀。

filter 的威力在多项目场景下是刚需。我在 CodeBuddy 里同时开着三四个项目的对话,如果没有 filter,Unity 项目的记忆会污染 Python 项目的召回结果,Agent 会开始胡说八道。加上 app_id 过滤之后,每个项目的 Cube 虽然物理上共享,但逻辑上完全隔离

时间范围也可以过滤(同样要包 and):

1
2
3
4
5
6
7
{
"filter": {
"and": [
{ "created_at": { "gte": "2026-01-01T00:00:00Z" } }
]
}
}

这个在"我最近一个月的偏好变了"这种场景里很有用——直接让 Agent 只看近期记忆。

9.5 MCP 客户端配置

讲完设计,说说怎么接。在 Claude Desktop、CodeBuddy 或任何支持 MCP 的客户端里,配置大概长这样:

1
2
3
4
5
6
7
8
9
10
11
12
{
"mcpServers": {
"memos-api-mcp": {
"command": "node",
"args": ["D:/memos-api-mcp/build/index.js"],
"env": {
"MEMOS_USER_ID": "demo_project",
"MEMOS_BASE_URL": "http://localhost:8000"
}
}
}
}

三个环境变量的含义:

  • MEMOS_USER_ID:用户稳定标识,作为所有请求的默认 user_id。生产环境推荐用 SHA-256(email) 或 SSO subject;本地玩随便起个稳定字符串就行。
  • MEMOS_BASE_URL:MemOS Server 地址,默认 http://localhost:8000,对应我们前面启动的 uvicorn 服务。
  • MEMOS_MEM_CUBE_ID(可选):get_user_profile 的默认 cube,不设置就回退到 MEMOS_USER_ID

配好之后客户端重启一下,就能在工具列表里看到这五个工具。

9.6 真正让它跑起来的,是那份系统提示词

装完 MCP、客户端也连上了,这时候如果直接开对话你会发现——Agent 大概率不会主动用这些工具

为什么?因为 LLM 虽然能看到工具列表,但它并不知道"每一轮都必须调"、“filter 应该怎么写”、"session_id 要保持稳定"这些规矩。工具给了它能力,但没给它纪律

所以在我的项目里,我专门写了一份规则文件(放在 .codebuddy/rules/ 下,不同 Agent 客户端有不同的规则目录机制),用系统提示词的形式强制约束 Agent 的调用行为。这份提示词我打磨了快两周,下面直接放出来,大家可以整份复制走改成自己的项目名即可。

完整提示词(可直接复制)

下面这份是我实际在用的版本,按自己项目情况把 demo_project 全部替换掉就能用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# Memos MCP 使用规则

## 用户规则
- 基于用户过往的对话内容抽取记忆,并通过记忆检索提升用户与 AI 对话的一致性和个性化水平。

## 项目标识(固定值,所有调用都必须带上)
- project_id: "demo_project" // 顶层项目隔离字段
- info.app_id: "demo_project" // 写入时放在 info 里;后端会把 info 的键平铺到 memory 顶层
- session_id: "demo_project_<stableConvId>"
(由 Agent 维护,单次会话保持稳定;建议用 md5(user_id + 会话第一条用户消息) 生成)

## 可选范围与标签字段
- info.scope_key: "demo_project:<project|team_id|personal_id>" // 推荐用于共享范围过滤
- info.team_id: "<team_id>" // 组范围
- info.module: "<模块名>" // 如 FrontEnd / Backend / DevOps
- info.keywords: ["<关键词1>", "<关键词2>"] // 稳定标签;用于过滤时放在 info

## 每轮对话的强制工作流

### 1. 回答前,调用 memos-api-mcp 的 search_memory
必填入参:
- query:当前用户问题(可做精简摘要)
- session_id:本次会话的稳定 ID
- top_k:3
- search_memory_type: "LongTermMemory" // 本项目 MCP 自动记忆只检索长期记忆
- dedup: "no" // 关闭 MMR 去重,避免 search 慢到 40s+
- include_preference: false // 当前不使用 PreferenceMemory
- search_tool_memory: false // 当前不使用 ToolSchemaMemory / ToolTrajectoryMemory
- include_skill_memory: false // 当前不使用 SkillMemory
- filter:必须限定到本项目,使用 MemOS 支持的 `and` / `or` 结构;`info` 字段写入后会平铺到顶层,过滤时直接用字段名,不要写 `info.xxx`。

推荐调用示例(项目级共享):
```json
{
"query": "<当前用户问题摘要>",
"session_id": "demo_project_<stableConvId>",
"top_k": 3,
"search_memory_type": "LongTermMemory",
"dedup": "no",
"include_preference": false,
"search_tool_memory": false,
"include_skill_memory": false,
"filter": {
"and": [
{ "project_id": "demo_project" }
]
}
}
```

推荐调用示例(组内共享):
```json
{
"query": "<当前用户问题摘要>",
"session_id": "demo_project_<stableConvId>",
"top_k": 3,
"search_memory_type": "LongTermMemory",
"dedup": "no",
"include_preference": false,
"search_tool_memory": false,
"include_skill_memory": false,
"filter": {
"and": [
{ "project_id": "demo_project" },
{ "scope_key": "demo_project:<team_id>" }
]
}
}
```

规则:仅使用与当前问题真正相关的记忆;无关或噪声忽略。

### 2. 结合检索到的相关记忆回答用户

### 3. 回答完成后,必须调用 memos-api-mcp 的 add_message 记录本轮对话摘要
无论用户说什么或问什么都要调用,否则后续 search_memory 拿不到更细的用户信息。
必填/建议入参:
- session_id:与第 1 步 search_memory 使用的同一个 ID
- messages:本轮的 user + assistant 两条消息(OpenAI Chat 格式;assistant 为最终回答)
- project_id: "demo_project" // 项目隔离,必带
- async_mode: "async" // 自动记忆异步写入,避免每轮对话阻塞
- mode: "fast" // 自动记忆使用 fast 模式;user+assistant 混合消息会保存为 LongTermMemory
- info: // 自定义属性,**所有键会被平铺到 memory 顶层属性**,写入后直接以扁平字段名过滤
```json
{
"app_id": "demo_project",
"scope_key": "demo_project:<project|team_id|personal_id>",
"team_id": "<team_id,可选>",
"module": "<FrontEnd / Backend / DevOps>",
"business_type": "<unity_client / web_app / script / build_tool>",
"biz_id": "<业务实体 ID,如类名/模块名>",
"topic": "<一句话主题>",
"keywords": ["<稳定关键词1>", "<稳定关键词2>"],
"scene": "<coding / debug / daily_chat / qa>",
"lang": "<zh / en>"
}
```

推荐调用示例(项目级共享;组内共享时填写 team_id 并将 scope_key 改为 `demo_project:<team_id>`):
```json
{
"session_id": "demo_project_<stableConvId>",
"project_id": "demo_project",
"async_mode": "async",
"mode": "fast",
"messages": [
{
"role": "user",
"content": "<用户本轮问题>"
},
{
"role": "assistant",
"content": "<助手最终回答>"
}
],
"info": {
"app_id": "demo_project",
"scope_key": "demo_project:project",
"team_id": "<team_id,可选>",
"module": "<FrontEnd / Backend / DevOps>",
"business_type": "unity_client",
"biz_id": "<类名或模块名>",
"topic": "<一句话主题>",
"keywords": ["<稳定关键词1>", "<稳定关键词2>"],
"scene": "coding",
"lang": "zh"
}
}
```

## 关键规则
- 按上面的 search/add 示例调用;`session_id` 单次会话保持稳定。
- 搜索默认只查 `LongTermMemory`,并使用 `dedup: "no"`、关闭 Preference/Tool/Skill 记忆分支。
- filter 必须使用 `and` / `or` 结构;不要传裸单字段 filter,也不要使用 `info.<key>`
- 共享范围用 `scope_key`:项目级 `demo_project:project`,团队级 `demo_project:<team_id>`
- 不传 `custom_tags`;稳定标签写入 `info.keywords`,模块归类写入 `info.module` / `info.biz_id`

复制完之后直接丢到 .codebuddy/rules/memos.md(或你 Agent 客户端约定的规则目录)就能生效。

整份规则看起来挺长,但它的内在结构其实就五块:项目标识 → search 规范 → 回答规范 → add 规范 → 关键规则。下面把每一块单独拎出来讲讲背后的设计决策——如果只是想拿来用,看到这里就可以了;想理解为什么这么写,继续往下读。

(1) 项目标识:三个细节值得说

project_idinfo.app_id 为什么要写两遍? 因为 MemOS 会把 info 里的键平铺到 memory 顶层属性。也就是说写入时放进 info.app_id,查询时就能直接用 app_id 去 filter,不用写 info.app_id。两个字段都带上是为了兼容两种过滤写法。

session_id 为什么要稳定? MemOS 的召回相关性会受 session_id 影响。每次新对话生成一个随机 ID 会导致同一会话内的记忆互相召不到;但如果全局只用一个 ID,又会让跨会话的上下文互相污染。折中方案是按会话稳定——一次对话内保持不变,新对话换新的。

md5 生成 session_id 的小技巧:用 md5(user_id + 第一条用户消息)。同一个人说同一句话会得到同一个 ID,天然幂等。

(2) 搜索规范:每个参数都是踩坑换来的

top_k: 3:默认是 10。实测 10 条记忆拼进 prompt,对 Agent 的决策反而有干扰——回到前面讲过的"上下文纯净"问题。3 条是我调出来的甜点值,宁少勿多。

search_memory_type: "LongTermMemory":MemOS 里其实有 11 种记忆类型,全查一遍又慢又乱。本地部署场景下 90% 有用的都是长期记忆,其他的按需开。

dedup: "no":这个参数坑过我一次。默认是 "mmr"(Maximal Marginal Relevance 去重),听起来很美好,但本地 7B 模型跑 MMR 一次搜索能到 40 秒+,整个对话体验崩塌。关掉它,靠 top_k=3 自己保证多样性。

include_preference / search_tool_memory / include_skill_memory 全关:这三类记忆在自动记忆场景下用不到,默认开着会拖慢搜索。只保留 LongTermMemory 一条主线最干净。

filter 必须用 and/or 结构:MemOS 的 filter 不接受裸单字段(比如直接 {"project_id": "x"}),必须包一层 and。这个是规范要求,不遵守直接报 400。

(3) 回答规范:只用相关记忆,无关的丢掉

仅使用与当前问题真正相关的记忆;无关或噪声忽略。

没有这条,LLM 会倾向于把所有召回的记忆都想办法"塞进答案里"——哪怕跟问题无关,也要生硬地提一嘴,美其名曰"体现连贯性"。这是 LLM 的一个通病,必须用提示词显式压制。

(4) 写入规范:看似官僚的字段全是未来的弹药

async_mode: "async" + mode: "fast" 的组合很关键。同步模式 + fine 模式虽然质量最高,但每次 add 要等 10 秒以上,严重影响对话体验。异步 + fast 把写入延迟压到几百毫秒,用户无感,质量也够用。

info 里那些"看起来很官僚"的字段是故意的modulebusiness_typetopickeywords 这些字段在写入时看起来很啰嗦,但它们是未来 filter 搜索的弹药库。当记忆库里攒到几千条之后,没有这些元数据就根本没法精准召回。Agent 在写入时稍微费点力气打好标签,查询时才有东西可过滤。

特别是 scope_key,我用它做共享范围的控制:

  • demo_project:project:项目级共享(所有团队成员都能看到)
  • demo_project:<team_id>:团队级共享(只有本组能看到)
  • demo_project:<personal_id>:个人私有

这样同一个 MemOS 实例可以服务多人,靠 scope_key filter 就能实现软性权限隔离。

(5) 关键规则:六条铁律的意义

提示词最后那个 check list 看起来像啰嗦的总结,但它的作用是容错。LLM 在长对话中会开始偷懒、简化、漏参数——check list 就是写给它的"每次调用前再自查一遍"的最后一道闸门。

这套提示词的本质是什么?

回头看这份规则,我觉得它干的事情,其实就是把"MemOS 的 API 设计直觉"翻译成 LLM 能遵守的行为约束

MemOS 本身设计得很灵活——20 多个参数、11 种记忆类型、任意自定义 info。但灵活性的反面是需要有人做选择。如果让 LLM 自己选,它会每次都选不一样的;如果让用户每次手动填,谁也受不了。所以必须有一层规则来做默认收敛——把 80% 场景下的最佳参数固化下来,让 Agent 照着抄就行。

这份规则大概 150 行,但它的价值抵得上前面所有代码

工具给能力,提示词给纪律。缺了任何一个,记忆系统都运转不起来。

9.7 跑起来的效果

讲了这么多,实际用起来什么感觉?

每次在 CodeBuddy 里开一个新对话,它第一件事会调 search_memory,把我的技术栈、项目偏好、当前项目的结构约定都捞出来。然后整个对话里它不会再问"你用什么框架"、"路径是哪"这种问题。

对话过程中,每次轮次结束它会悄悄调 add_message,把刚才这一轮有长期价值的信息沉淀进去。第二天打开新对话,昨天那条"用户希望 MemOS 的 Qdrant 放在项目根下的 qdrant/ 里"就直接能召回。

偶尔我会说一句"上次那个记错了,不是 8001 是 8000",Agent 会调 add_feedback 修正。这些动作全程不用我介入,MCP 协议 + 强制工作流让这一切变成了 Agent 的肌肉记忆。

用了一个月最大的感受是:AI 不再是金鱼脑。它不会再问"你的项目路径是什么"、“你用的是什么框架”、“你喜欢简洁还是详细”——这些它都已经知道了。而且更重要的是,它还在持续变得更懂我


十、踩过的坑汇总

按遇到的顺序排:

Neo4j 装了但没启动。Neo4j Desktop 装完默认只是启动了"管理器",数据库本身要手动 Start。这个坑我吃了两次。

MEMRADERMEMREADER 拼写不统一。记忆抽取模型的环境变量前缀是 MEMRADER_,少了一个 E。千万别手贱改成正确拼写,那样反而读不到。

Ollama 模型拉了但 MemOS 连不上。确认一下 Ollama 服务是真的在跑(任务管理器里有 ollama app 进程),curl http://localhost:11434/api/tags 能返回模型列表。有一次我的 Ollama 进程悄悄挂了自己都没发现。

Qdrant collection 维度不匹配。之前试过换 embedding 模型,结果新模型是 768 维,旧 collection 是 1024 维,报错报得很隐晦。删掉 Qdrant 数据目录重建就好。

端口冲突。8000 是 MemOS API,11434 是 Ollama,7687 是 Neo4j Bolt,6333 是 Qdrant。我本地正好有个 docker 服务占了 8000,MemOS 起不来但日志里没明说,查了半天。

第一次 add 巨慢。本地 7B 模型抽记忆本来就慢,再加上第一次冷启动 Ollama 要加载模型权重到显存,头几次请求能到 30 秒。第一次跑别以为是卡死了,耐心等。

.env 不小心被提交到 git。示例里虽然把密码都脱敏了,但真实的 .env 一定要加到 .gitignore。对外分享项目的时候,密码、内网地址、MCP 地址都得先清一遍。


十一、MemOS vs 普通 RAG:什么时候用它

聊了这么多,我还想说句实话:不是所有项目都需要 MemOS

如果只是做一个 FAQ 问答、一个文档助手、一个代码搜索工具,普通 RAG 就够了。MemOS 相对来说是"重"的——要装图数据库、向量数据库、还要跑个记忆抽取模型。

它的价值只有在以下场景才体现得出来:

Agent 需要跨会话记住用户(个人助手、长期陪伴类应用);我们要做多 Agent 协作且 Agent 之间需要共享经验(工程团队的开发 Agent、测试 Agent、审查 Agent);系统需要对记忆做治理(企业合规、可审计、可删除);系统会长期运行,需要自然淘汰过时信息、修正错误记忆。

换句话说,RAG 解决的是"有知识要查",MemOS 解决的是"有经验要沉淀"。


十二、回到 Harness:记忆到底有多关键

装完 MemOS、接上 MCP、写完规则文件,这时候再回头看开篇那句"Agent = Model + Harness",我的体会完全不一样了。

第二节讲 Harness 六层的时候我说过 L4 是最难啃的一层,那还只是理论判断。真正动手搭了一个月之后,我想把这句话说得更具体:在 Harness 的六层里,L4 是唯一需要"长期陪跑"的一层

L1(边界)、L2(工具)、L3(编排)、L5(评估)、L6(约束)这五层有一个共同特点——它们都是"写好就放那"的静态设施。提示词写完就是那个样子,工具接上就不用管,测试脚本写完能跑多少年。但 L4 不一样,它是一个"今天的 Agent 比昨天更懂你"的动态过程。每多一次对话、每一条反馈、每一次纠错,它都在悄悄变化。

这种"活的"特性让 L4 成为 Harness 里最像真实产品的一层,也是它最容易被低估的原因——很多团队把记忆当成"附加功能"做,结果跑到几千条记忆之后系统就崩了:重复记忆互相打架、错误记忆无法清除、不同项目的记忆互相污染。补 L4 的代价不是写代码的工作量,而是设计出一套能自我演化的治理机制

一段来自 LangGraph 的真实体会

说到这个我特别想插一段自己的实战体会。最近我也在用 LangGraph 做一些 Agent 开发,折腾下来感触最深的一件事,可以总结成四个字——上下文纯净

LangGraph 的 State 机制让节点之间传状态非常方便,但也很容易养成一个坏习惯:什么都往 state 里塞。工具调用的完整返回、中间推理的草稿、历史轮次的原始对话、还有一堆"万一后面会用到"的字段。一开始跑得好好的,跑到后面你会发现——Agent 开始做一些莫名其妙的决策,因为它的上下文里混了太多早就过期的噪音,LLM 的注意力被稀释了。这正是第二节里讲的 Smart Zone → Dumb Zone 分界,一旦越过 40% 就开始退化。

解决这个问题有很多做法:在图的边上做 state 裁剪、用 summarization 节点压缩历史、或者分层架构把细节隔离在子图里。但这些本质上都是"事后清理"——垃圾已经产生了,我们在想办法把它们扫出去

而记忆系统干的是更前置的一件事:从源头上就不让垃圾进主上下文

具体来说,当 Agent 有了一个像 MemOS 这样的外挂 L4 层之后,工作流大概是这样的:

  • 历史对话不再原样拼进 prompt,而是被 MemReader 抽取成结构化记忆存进 Cube;
  • 每次请求时,根据当前任务做语义召回,只把相关的那几条记忆拼进来(上面规则文件里 top_k: 3 就是这个意思);
  • 工具调用的中间结果该记录的记录、该丢弃的丢弃,主 state 只保留决策所需的精简字段;
  • 跨会话的用户偏好、项目约定这些"稳定记忆",不占当前轮的上下文预算。

结果就是:主上下文里永远只有"和当前这一步最相关的信息"。Agent 的注意力不会被分散,第 50 轮和第 1 轮的决策质量基本一致。

所以记忆看似是在解决"记住"这件事,但它带来的最大副作用其实是"忘掉"——忘掉那些不该出现在当前上下文里的东西。这种"忘掉"不是真的遗忘,是把它们从热数据转成冷数据,需要的时候还能精准召回,不需要的时候不来打扰。

对我来说,这是用了 MemOS 之后最打动我的一点。它让我重新理解了一件事:好的记忆系统,和好的遗忘机制,其实是同一件事的两面

Context Resets vs MemOS:两条路径的取舍

回到上下文崩坏这个问题,业内目前有两种主流解法:

一种是 Anthropic 推的 “context resets”——干脆另起一个新 Agent,用结构化的交接文档把状态传过去。思路干脆,但交接文档本身就是一种记忆形式,你还是得有地方存它。

另一种就是 MemOS 这条路——不等到崩了再重置,而是从一开始就把"该记什么、记成什么结构、什么时候调出来"做成系统能力。每次请求只召回真正相关的记忆,上下文利用率始终压在 40% 以下,Agent 就不会退化。

所以说,记忆其实很重要。它不只是一个让 AI 记住你生日的小功能,而是决定 Agent 能不能跑到第 100 轮、第 1000 轮还不崩的基础设施


十三、一些不成熟的想法

用了 MemOS 一段时间,我开始觉得,长期记忆可能会成为 AI 应用的分水岭

现在大家做 AI 产品,能力上其实差不多——都是接个大模型加点 prompt engineering。真正拉开差距的点,往往不在模型本身,而在 Harness 那一套外围骨架上:有没有积累用户画像、有没有沉淀项目知识、有没有让 Agent 从历史任务里学到东西、会话跑到第 50 轮还能不能保持一致性。

MemOS 提供的,正是让普通开发者也能构建这种"长期能力"的基础设施。我们不用自己去想怎么设计记忆 schema、怎么做去重、怎么处理冲突、怎么按用户隔离——这些它都替我们想好了。把 Harness 的 L4 层外包给 MemOS,我们就能把精力放回在真正差异化的业务上。

当然它现在还年轻,文档和稳定性都有进步空间。装的过程里我也吐槽了不少地方。但方向我是认的。

如果大家也想试试,我的建议是:别想着一口气搞完,先把 Ollama 和 MemOS API 跑通,能完成 add/search,就算赢了一半。剩下的 MCP 集成、多 Agent 协作、反馈修正,都可以等这个基础跑稳了再慢慢加。


十四、参考资源