最近在折腾 OpenClaw,本来只想拿它当个聊天的 AI 助手,结果用着用着发现一个很要命的问题——每次对话一长,它就把前面聊的关键决策忘得干干净净。明明是刚定好的方案,换个话题再回来,它就跟失忆了一样重新问一遍。不光我踩了这个坑,身边好几个折腾本地 AI 代理的朋友也在抱怨同样的事。后来我花了两天时间把 OpenClaw 的源码翻了一遍,才发现它其实自带一套可以在会话之间保留“长期记忆”的机制,只是默认配置基本没用到,很多人根本不知道还有这个玩法。这篇就把我调试源码、写 memory skill、让助手真正“记住事”的完整过程写出来,给同样被 AI 失忆折磨的朋友一个能直接抄作业的方案。
OpenClaw 这类 AI 代理的核心思路,是把大模型接进来,再给它配一堆工具和技能,让它能读文件、调 API、执行命令。但模型本身是“无状态”的,每个请求都相当于一个刚睡醒的新人。要想让它“记住”,就得从源码层面找到记忆的注入点和保存点,把重要的决策信息显式地写到外部存储里,下一次对话再重新加载。这套方案不挑具体模型,只要你能在 OpenClaw 里正常调用工具,基本都能复现。
1. 内容整体设计与思路拆解
1.1 为什么 AI 助手总是“失忆”
先说清楚“失忆”到底是怎么发生的。模型本身不保存任何对话历史,OpenClaw 每次调用大模型时,都是把当前会话的消息列表一股脑塞进上下文窗口(context window)里。聊到一半,之前的消息还在上下文里,所以看起来“记得”;但一旦超过窗口上限,最老的消息就被截断了,或者你新开一个会话,整个消息列表清空,它就啥也不记得了。
更隐蔽的问题是“重要决策”和“普通闲聊”在模型眼里没有区别。你在对话中订了一个规则,比如“以后所有回复都用中文、邮件模板用第二版、外部 API 的 key 放在 config 目录”,这些信息只是混在历史消息里,并没有被结构化地保存下来。等上下文被截断或新会话开启,这些关键的“决策”就跟着普通聊天记录一起丢了。
所以要让 AI 助手不“失忆”,核心思路不是无限加大上下文窗口,而是把值得长期保留的信息从对话流中“抽”出来,存到一个独立、可持续读取的地方。等下一次对话开始时,再把这份记忆重新注入到上下文里。OpenClaw 的源码里已经预留了这条链路,只是需要我们自己把它激活。
1.2 OpenClaw 里的“短期记忆”与“长期记忆”
我在源码里把记忆相关的模块翻了一遍,OpenClaw 的记忆体系大致分两层。
短期记忆就是当前会话的 message history。源码里对应的是每个 session 底下维护的消息列表,OpenClaw 在每次运行代理循环时,会把这段历史交给上下文构建器,最终拼成给模型的 prompt。这部分数据用完就丢,除非你显式做会话持久化,否则重启进程就没了。
长期记忆则是我们需要下手的地方。OpenClaw 支持一种叫 Active Memory 的机制,允许用户或 agent 通过工具调用,把一些持久化的信息写到外部存储里。这些信息会在后续的上下文构建阶段被重新读取并注入。也就是说,只要我们把关键决策写进 Active Memory,即使开新会话,模型也能在系统提示词里看到这些历史决策记录。
这里有一个容易踩的误区:OpenClaw 本身也会自动保存对话记录到本地文件,但那只相当于日志,并不会在每次请求时自动回填给模型。如果只依赖自动保存的聊天记录,Agent 该忘记还是会忘记。我们要写的是“主动记忆”,不是“日志存档”。
1.3 让记忆“结构化”而不是堆文本
我在设计和调试记忆方案时,一直提醒自己:记忆不是把对话不分青红皂白全记下来,那样很快会把上下文窗口撑爆。真正的长期记忆应该是结构化的、简短实在的决策条目。
举个例子,你在聊天里定了一个规则:“文档输出统一用 Markdown,代码块带语言标记。”如果直接存整段聊天记录,模型每次都要从那堆对话里自己翻重点,费 token 且效果不稳定。更好的方式是把这条规则浓缩成一条结构化记录:
json复制{
"type": "decision",
"category": "output_format",
"content": "输出文档统一使用 Markdown,代码块必须标注语言",
"created_at": "2025-06-10T10:00:00Z",
"status": "active"
}
这样每次注入到上下文时,模型能一眼看懂之前定了什么规则。Active Memory 的设计理念也正是如此:高密度、低冗余、按时间或类别组织。后面我会具体讲如何通过一个自定义 skill 来实现这种结构化记忆。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 源码中记忆模块的入口位置
拿到 OpenClaw 源码后,不要急着到处翻,先看 src/ 或 claw/ 目录下的模块划分。我本地的版本里,跟记忆最相关的是 memory/ 和 skills/ 两个目录。memory/ 里定义了 Active Memory 的数据结构、存储后端和上下文注入逻辑;skills/ 里则是一些可被模型调用的工具函数,其中就包含了记忆读写相关的配置。
如果你是从 GitHub 拉的最新分支,建议先搜索 active memory 或 active_memory 这两个关键字。源码里通常会有类似 ActiveMemoryManager、MemoryItem 这样的定义。它的作用很简单:维护一个条目列表,每个条目有内容、标签和过期策略,并提供 add、search、remove 这些基础方法。
找到了这个入口,接下来的思路就清楚了:我们不是要魔改源码,而是借助它预留的工具调用接口,在对话中触发“把当前决策写入记忆”的操作。基础版可以不做复杂向量化,直接存成文本/JSON 文件;后续如果要支持语义检索,可以把文件换成向量数据库。
2.2 记忆写入与读取的完整链路
从源码层面看,一次记忆写入大致走三步。
第一步,模型在对话过程中决定调用某个记忆工具。这个决定来自系统提示词里对 Active Memory 的说明。OpenClaw 默认会把一段“如果你发现用户提出了重要决策,请调用 memory.add 记下来”之类的指令拼进系统提示词。
第二步,工具调用被路由到对应的处理函数。OpenClaw 用的是 tool calling 机制,记忆功能对应一个 Skill 或内置工具。处理函数会接收模型传来的参数,比如 content、category、tags,然后调用存储后端的写入方法。
第三步,存储后端把数据持久化到文件或数据库。默认实现一般是把记忆数据写到 ~/.openclaw/memory/ 下的一个 JSON 或 SQLite 文件里。下次会话启动时,上下文构建器会读取这个文件,把记忆条目作为系统提示词的一部分注入进去。
读取链路相对简单:会话初始化时,ActiveMemoryManager 会加载全部(或按条件筛选)记忆条目,格式化成一串文本,插到 system prompt 的末尾。OpenClaw 源码里通常会有类似 load_recent_memories 或 get_context_memories 的函数,就是干这件事的。
2.3 一个最小可行的决策记忆实现
如果你不想用太复杂的数据库,直接从文件存储开始是最稳妥的。我在实操中写了一个不到 100 行的记忆 skill,核心逻辑只有两个函数:读取记忆文件和追加记忆条目。结构大概是这样的:
python复制import json
import os
from pathlib import Path
MEMORY_DIR = Path.home() / ".openclaw" / "memory"
MEMORY_FILE = MEMORY_DIR / "decisions.json"
def ensure_memory_file():
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
if not MEMORY_FILE.exists():
MEMORY_FILE.write_text(json.dumps({"decisions": []}, ensure_ascii=False, indent=2))
def load_decisions():
ensure_memory_file()
data = json.loads(MEMORY_FILE.read_text(encoding="utf-8"))
return data.get("decisions", [])
def add_decision(content, category="general", tags=None):
ensure_memory_file()
decisions = load_decisions()
decisions.append({
"type": "decision",
"category": category,
"content": content,
"tags": tags or [],
"created_at": datetime.utcnow().isoformat()
})
# 简单去重:如果前面已经有相似度高且 active 的同类决策,就覆盖
MEMORY_FILE.write_text(json.dumps({"decisions": decisions}, ensure_ascii=False, indent=2), encoding="utf-8")
这样的实现没有任何外部依赖,JSON 文件方便人工查看和修改,也不容易遇到数据库锁问题。当然,如果记忆条目非常多,可以考虑改成 SQLite 或直接用 OpenClaw 内置的 Active Memory API,但文件方案更适合理解和调试。
3. 实操过程与核心环节实现
3.1 环境准备:确认版本与模型支持
在开始写记忆 skill 之前,先确认三件事。
第一,OpenClaw 的版本不能太老。建议直接拉最新 release,或者用官方的一键部署脚本更新到最新版,否则可能没有 Active Memory 相关的内置接口。第二,当前接的模型必须支持工具调用(function calling)。无论是 OpenAI 系、Claude 系,还是本地部署的 Qwen、DeepSeek 等模型,都需要确认 API 配置里启用了 tool 支持。如果你模型没有工具调用能力,后面“模型决定调用记忆工具”这一步根本走不通。第三,确认工作目录有写入权限。OpenClaw 默认把配置和数据放在用户主目录下的 .openclaw 文件夹,如果之前部署在 root 用户下,后面切换普通用户运行时要重新初始化。
我在 Mac mini 上用 Docker 部署过一次,后来又换到 Linux 服务器上跑裸机版本。两种方式在写记忆文件时没有本质差别,Docker 部署需要额外注意把 ~/.openclaw 挂载到宿主机,否则容器一删记忆就全没了。
3.2 编写一个 memory skill 的完整步骤
我习惯用 OpenClaw 的 skill 机制来扩展功能,因为它不用改核心源码,单独一个文件就能被模型识别调用。具体来说,在 .openclaw/skills/ 下新建一个目录,比如 memory_skills/,里面放两个文件:SKILL.md 和 memory_tool.py。
SKILL.md 的作用是向模型描述这个技能的功能和调用方法。我写的版本是:
markdown复制# Memory Skill
## 功能
保存和读取长期决策记忆。当用户提出重要规则、偏好、决策时,主动调用 add_decision 保存。
在用户询问“你还记得吗”或需要历史决策时,调用 get_decisions 查询。
## 工具方法
- add_decision(content: str, category: str = "general", tags: list = []): 新增一条决策记忆。
- get_decisions(category: str = None): 返回全部或指定分类下的决策记忆。
memory_tool.py 就是上一节里的最小实现,再加一个 get_decisions 查询函数。把这两个文件放进 skill 目录后,重启 OpenClaw 或重新初始化会话,让模型重新加载 skill 列表。
这里有个细节:模型能否正确调用 skill,很大程度上取决于 description 写得够不够清楚。我一开始写的是“memory related”,模型调用得很犹豫。后来改成“保存和读取长期决策记忆,当用户提出重要规则或偏好时调用”,命中率一下子提高了很多。如果你想让模型更积极地记住信息,可以在系统提示词里加一句:“在对话中检测到关键决策时,必须调用 add_decision 保存。”
3.3 配置主动保存与自动触发的策略
记忆 skill 写好之后,还面临一个选择:到底让模型自动保存,还是由用户手动触发?
我建议分两级。第一级是模型自动识别。当对话中出现“以后都按这个来”“记住这条规则”“统一使用 X 方案”等明确表达时,模型应该主动调用 add_decision。第二级是用户手动命令。当用户说“记住:数据库连接串统一走配置中心”时,模型必须把后面的内容当作记忆内容保存。
为了达到这个效果,我在 OpenClaw 的 system prompt 里追加了一段指令,大致内容:
text复制请保持一个 Active Memory 列表,记录用户的长期决策、偏好和规则。
当出现以下情况时,调用 memory_tool.add_decision:
1. 用户用“记住/以后/总是/不要”等词提出明确要求。
2. 用户确认了某个方案、配置或规则,并且语气带有长期约束。
写入时请把内容压缩成一句简洁的话,分类和标签也要填准确。
通过这种配置,模型的行为稳定了很多。实测下来,它不再把每句闲聊都写进记忆,也不会漏掉真正的规则。
3.4 验证记忆是否真正跨会话生效
配置完这些,如何验证确实生效了?我的测试方法很简单。
第一步,开一个会话,和 OpenClaw 说:“记住,以后输出 HTML 邮件时,纯文本和 HTML 两个版本都要生成。”然后等模型调用工具,确认 decisions.json 里多了一条记录。
第二步,结束会话,清掉上下文,重新开一个新会话。第一句话直接问:“我之前有没有提过 HTML 邮件有什么要求?”如果 OpenClaw 能说出“需要同时生成纯文本和 HTML 版本”,说明记忆已经成功注入到了新会话的上下文中。
如果答案是否定的,就得一步步排查:先看 decisions.json 里有没有记录;再看系统提示词里有没有注入该记录;最后确认 skill 是否被正确加载。不要一上来怀疑模型能力,大部分问题出在上下文构建阶段没把记忆文件读进来。
3.5 进阶:在记忆里保存“全部重要决策”的上下文
如果你的场景比“记住几条规则”更复杂,比如要管理一个长期项目,重要的决策有几十条,那简单 JSON 文件读写依然够用,但需要增加两个能力:摘要和分类。
我在项目里把记忆分成几个 category:database、ui、api、workflow。每次写入新决策时,除了存原始内容,还会让模型顺手补一个 summary 字段,比如“用户决定统一走 MySQL,不用 PostgreSQL”。查询时按 category 过滤,注入上下文时只取当前项目相关的部分,避免把无关记忆也塞进去。
这样做了之后,上下文中注入的记忆量比较稳定,不会随着使用时间无限膨胀。如果你和 OpenClaw 的对话特别多,还可以加一个定时清理逻辑:超过 90 天且 status 为 inactive 的决策自动归档。
4. 常见问题与排查技巧实录
4.1 模型报错“unknown model: deepsee...”这样的问题
很多人在 OpenClaw 里切换模型后会遇到 agent 启动失败,报错信息类似 agent failed before reply: unknown model: deepseek...。这个八成不是记忆功能的问题,而是模型别名没有在配置里注册。OpenClaw 的模型配置通常维护在 ~/.openclaw/config.yaml 里,需要把模型名称改成 API 服务商实际接受的模型 ID。
比如你后端用的是 DeepSeek,配置里 model 字段写 deepseek-chat 或 deepseek-reasoner,不要写 deepseek。切换模型后,需要做两件事:确认 API key 有对应模型的权限,然后重启 OpenClaw 让配置重新加载。这个问题和记忆功能叠加时特别容易误导人,你会误以为是新会话没加载记忆导致模型回答异常,实际只是模型根本没起来。
4.2 “OneClaw node runtime not found”环境依赖问题
Windows 上手动安装 OpenClaw 时,偶尔会遇到 oneclaw node runtime not found 或 OpenClaw node runtime not found 的提示。这是因为 OpenClaw 依赖 Node.js 运行时,但安装脚本没有自动找到。解决办法是先装一个 LTS 版本的 Node.js,并在系统环境变量里把 Node 的安装路径加到 PATH 中。装完最好重新打开终端,让环境变量生效。
如果已经在 PATH 里还是报错,可以手动指定 Node 路径。OpenClaw 的配置文件里通常有 runtime.node_path 之类的选项,指向具体的 node 可执行文件路径。我踩过一次坑是装了 nvm 管理多版本 Node,结果默认版本切到了老版本,导致 OpenClaw 不识别,切回 LTS 就好了。
4.3 写记忆文件时报 “EBUSY resource busy or locked”
当你在 Windows 上运行 OpenClaw,如果同时用记事本或编辑器打开了 ~/.openclaw/memory/decisions.json,那么模型调用 add_decision 写入时,可能会报 error: ebusy: resource busy or locked, unlink ...。这是因为文件被别的进程锁住了,Node.js 的写文件操作无法替换原有文件。
解决办法很简单:编辑记忆文件时先退出或使用支持热重载的编辑器,或者干脆不要手动编辑,通过 tool 来改。如果已经报了文件锁错误,重启 OpenClaw 进程,或者删掉临时文件后重新初始化记忆文件都行。为了减少这种问题,我后来把记忆存储改成了 SQLite 而不是 JSON,并发写入和手动查询都更稳定。但一开始调试用 JSON 文件更直观,看得到内容才能确认写入是否正确。
4.4 上下文里记忆越来越多,token 开销变大
用了一段时间后,记忆文件里的决策条数会增多。如果把所有记忆都注入到每次请求里,token 开销会明显变大,而且大量低价值记忆反而可能干扰模型判断。我建议只注入最近 30 条或最近 7 天内更新的活跃决策,老数据保留在文件里但默认不读取。
具体做法是在读取逻辑里加一个时间过滤:
python复制def load_recent_decisions(days=7, limit=30):
decisions = load_decisions()
cutoff = datetime.utcnow() - timedelta(days=days)
recent = [d for d in decisions if d.get("created_at") and d["created_at"] > cutoff.isoformat()]
return recent[:limit]
如果做了摘要,也可以只注入 summary 字段,原始内容用户需要时再通过工具查询。这样既能保证模型对关键决策有感知,又不会把上下文窗口撑爆。
4.5 模型不主动调用记忆工具怎么办
这是最容易让人沮丧的坑。明明 skill 写好了,配置文件也改了,但模型就是不调用 add_decision。我试过几次之后,总结出三个原因。
第一,模型版本不支持工具调用,或者 API 配置里没开 tools 参数。这种情况哪怕系统提示词写得再清楚,模型也无法触发工具调用。解决办法是换一个支持 function calling 的模型。第二,skill 的 description 不够明确,模型没有把它和“保存决策”联系起来。我后来把描述改成非常直白的话:“这是一个长期记忆工具。当用户说出任何希望以后仍然有效的规则时,你必须立即调用 add_decision 保存。”效果立刻改善。第三,当前上下文里的系统提示词被其他内容覆盖或压缩了。有些服务商对超长 system prompt 会截断,如果记忆指令写在很靠后的位置,可能根本没被模型看到。把记忆指令提到系统提示词的前面,并保持简短。
5. 从源码到日常使用的几个心得
这套方案我从最开始在 JSON 文件里手动加记录,到后来封装成 skill,再到接入 SQLite,差不多花了一个周末。回头来看,OpenClaw 的“失忆”并没有想象中那么难解决。它的源码已经把 Active Memory 的存储和注入框架搭好了,我们要做的只是在合适的位置填上自己的业务逻辑。
我个人最大的收获是:不要指望模型自动理解“什么值得记住”。你必须在系统提示词和 skill 描述里把规则写清楚,甚至要给出明确的触发词。模型本质上是一个遵循指令的引擎,你把“如何记忆”的边界划得越清楚,它的表现越稳定。
如果你只是想要一个“聊天的时候不忘事”的助手,那这套记忆 skill 已经足够了。但如果你是想让 OpenClaw 承担更长期的项目管理工作,我的建议是再往前走一步:把记忆条目按项目拆目录,或者加上标签和优先级。后续我在自己项目里正在尝试把记忆和任务清单联动,比如决策触发后自动生成待办事项,目前看效果还不错。这个方向后续可以继续深入,也欢迎有条件的朋友一起把记忆模块玩出更多花样。
