news 2026/10/2 16:23:21

OpenClaw 记忆系统配置实战:用 launchd + Python 给 AI 装上永久记忆,告别金鱼脑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OpenClaw 记忆系统配置实战:用 launchd + Python 给 AI 装上永久记忆,告别金鱼脑

1. OpenClaw 切换会话就失忆,问题到底出在哪

OpenClaw 记忆系统配置实战要解决的核心痛点很具体:你在一个会话里跟 OpenClaw 交代了项目背景、命名规范、部署路径,聊完关掉窗口,第二天新开一个会话,它像第一次见你一样,什么都要重新讲一遍。这不是模型笨,而是 OpenClaw 默认的上下文只活在单次会话的生命周期里,会话一结束,上下文就释放了,没有任何东西被写到磁盘上。

我把它叫做「金鱼脑」:每次对话都是全新的开始。你昨天说「以后所有脚本都放 ~/Documents/A_Openclaw/scripts」,今天它照样把文件生成到别处;你上周定好的 API 端口 4010,这周它又问你端口是多少。重复解释的成本,一天两天不觉得,一个月下来就是大量被浪费的时间。

要根治这个问题,思路不是把上下文窗口开得更大,而是给 OpenClaw 外挂一套本地持久化记忆系统。核心由三部分组成:一个常驻的向量检索服务负责语义搜索,一个定时拉起的 Python 脚本负责从对话里抽取值得记住的内容,一份工作记忆文件负责在每次会话开始时把关键上下文喂回去。macOS 上让这套东西开机自启、崩溃自愈,最稳的方案是 launchd,而不是 cron 或手动跑脚本。

这篇文章面向的是已经在 macOS 上用 OpenClaw、并且受够了每次重新交代背景的开发者。下面会给出可直接复制的 launchd plist 骨架、Python 记忆读写脚本、config.toml 配置片段,以及重启后验证记忆是否真的生效的完整步骤。整套配置我实测下来大约 1 小时能跑通,包含调试时间。

先说清楚三层记忆的分工,后面所有配置都围绕它展开:

层级载体读取耗时作用
Layer 0memory/qmd/current.json<1ms当前会话工作记忆,会话开始必读
Layer 1Zvec 向量库(本地 4010 端口)<10ms语义检索历史决策、偏好、经验
Layer 2MEMORY.md~50msOpenClaw 内置兜底记忆

Layer 0 是热数据,每次会话开始直接 cat 出来塞进上下文;Layer 1 是温数据,需要回忆某件事时走向量检索;Layer 2 是冷数据,前两层都没命中时的兜底。三层按速度递减、按覆盖递增,保证任何情况下都能捞到东西。

2. TaoToken 前置:给记忆系统接上稳定的模型出口

记忆系统本身只负责「存」和「取」,真正把抽取出来的内容变成结构化记忆、把检索结果组织成自然语言回灌给 OpenClaw 的,还是模型。所以在你动手配 launchd 之前,先把模型调用这条链路理顺,否则记忆脚本跑起来会因为拿不到模型响应而静默失败。

我用的方案是通过 TaoToken 统一出口调用模型。它的作用是给你一个兼容 OpenAI 风格的 API 端点,你不用在每台机器上分别维护各家模型的 Key 和地址,记忆脚本里只认一个 Base URL 和一个 Key 就行。对记忆系统这种需要频繁、小批量调用模型的场景,统一出口能省掉大量配置切换的麻烦。

你需要准备三样东西,我把它叫做「三件套」,后面所有配置都会引用:

  • Base URL:https://taotoken.net/api
  • API Key:在控制台创建,形如sk-开头的一串
  • Model ID:比如claude-sonnet-4-5这类具体模型标识

创建 Key 的入口在这里:

控制台创建 API Key:https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=memory_system

拿到 Key 之后,先别急着写进脚本,用一条 curl 验证出口是通的:

curl -s https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "claude-sonnet-4-5", "messages": [{"role": "user", "content": "只回复两个字:通了"}], "max_tokens": 16 }'

返回里choices[0].message.content是「通了」,说明出口没问题。这一步很重要,因为后面记忆抽取脚本一旦模型调用失败,它默认是静默跳过的,你不会立刻发现记忆没写进去,等到某天发现 OpenClaw 又失忆了才回头查,成本很高。

如果你更习惯在图形界面里先试模型效果,可以走模型对话页面,把同样的 prompt 丢进去看返回:

模型对话快速验证:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=memory_system

把 Key 存到环境变量里,别硬编码进脚本。macOS 上我建议写进~/.zshrc,同时 launchd 拉起的进程读不到交互式 shell 的环境变量,所以还要在 plist 里显式声明,这一点后面 §3 会讲。

echo 'export TAOTOKEN_API_KEY="sk-你的key"' >> ~/.zshrc source ~/.zshrc

到这里前置就绪:出口通了、Key 有了、Model ID 确定了。接下来进入真正动手的部分。

3. 可复制配置:launchd plist + Python 脚本 + config.toml

这一节是全文的核心,所有片段都可以直接复制。先建工作目录,路径按你自己的习惯改,我统一用~/Documents/A_Openclaw:

cd ~/Documents/A_Openclaw mkdir -p memory/qmd memory/subagents scripts mkdir -p ~/Library/LaunchAgents

3.1 config.toml 配置片段

先写记忆系统的配置文件,放在~/Documents/A_Openclaw/memory/config.toml。这个文件把模型出口、向量库地址、记忆策略集中管理,脚本读它就行,不用到处改常量:

# ~/Documents/A_Openclaw/memory/config.toml [model] base_url = "https://taotoken.net/api" api_key_env = "TAOTOKEN_API_KEY" model_id = "claude-sonnet-4-5" timeout_seconds = 30 [vector_store] host = "127.0.0.1" port = 4010 search_path = "/search" health_path = "/health" topk = 5 [memory] qmd_path = "~/Documents/A_Openclaw/memory/qmd/current.json" memory_md = "~/Documents/A_Openclaw/MEMORY.md" extract_decisions = true extract_preferences = true extract_lessons = true compact_after_hours = 24

注意api_key_env写的是环境变量名而不是 Key 本身,这样配置文件可以安全地进 git。

3.2 Python 记忆读写脚本

写一个memory_io.py,负责三件事:读工作记忆、调模型抽取记忆、写回向量库。放在~/Documents/A_Openclaw/scripts/memory_io.py:

#!/usr/bin/env python3 # ~/Documents/A_Openclaw/scripts/memory_io.py import os, json, sys, urllib.request, tomllib from pathlib import Path CONFIG_PATH = Path.home() / "Documents/A_Openclaw/memory/config.toml" def load_config(): with open(CONFIG_PATH, "rb") as f: return tomllib.load(f) def read_qmd(cfg): p = Path(os.path.expanduser(cfg["memory"]["qmd_path"])) if not p.exists(): return {} return json.loads(p.read_text(encoding="utf-8")) def call_model(cfg, prompt): key = os.environ.get(cfg["model"]["api_key_env"]) if not key: print("[memory_io] 缺少 API Key 环境变量", file=sys.stderr) return None body = json.dumps({ "model": cfg["model"]["model_id"], "messages": [{"role": "user", "content": prompt}], "max_tokens": 512 }).encode() req = urllib.request.Request( cfg["model"]["base_url"] + "/v1/chat/completions", data=body, headers={ "Authorization": f"Bearer {key}", "Content-Type": "application/json" } ) try: with urllib.request.urlopen(req, timeout=cfg["model"]["timeout_seconds"]) as r: data = json.loads(r.read()) return data["choices"][0]["message"]["content"] except Exception as e: print(f"[memory_io] 模型调用失败: {e}", file=sys.stderr) return None def extract_memory(cfg, text): prompt = ( "从下面这段对话里抽取值得长期记住的信息," "按 JSON 返回,字段为 decisions/preferences/lessons 三个数组。" "没有内容就返回空数组。只输出 JSON,不要解释。\n\n" + text ) raw = call_model(cfg, prompt) if not raw: return None raw = raw.strip().removeprefix("```json").removeprefix("```").removesuffix("```") try: return json.loads(raw) except json.JSONDecodeError: print("[memory_io] 模型返回不是合法 JSON", file=sys.stderr) return None def write_qmd(cfg, patch): p = Path(os.path.expanduser(cfg["memory"]["qmd_path"])) cur = read_qmd(cfg) for k, v in patch.items(): if isinstance(v, list): cur.setdefault(k, []).extend(v) else: cur[k] = v p.write_text(json.dumps(cur, ensure_ascii=False, indent=2), encoding="utf-8") print(f"[memory_io] 已写入 {p}") if __name__ == "__main__": cfg = load_config() if len(sys.argv) > 1 and sys.argv[1] == "read": print(json.dumps(read_qmd(cfg), ensure_ascii=False, indent=2)) elif len(sys.argv) > 2 and sys.argv[1] == "extract": result = extract_memory(cfg, sys.argv[2]) if result: write_qmd(cfg, result)

这个脚本的关键设计:模型调用失败时返回 None 并打 stderr,不会污染工作记忆;抽取结果强制走 JSON,方便结构化写入;read和extract两个子命令,前者给会话开始用,后者给会话中自动抽取用。

3.3 launchd plist 骨架

现在写两个 plist。第一个是常驻的向量服务,崩溃自动重启:

<!-- ~/Library/LaunchAgents/com.memclawz.server.plist --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>com.memclawz.server</string> <key>ProgramArguments</key> <array> <string>/opt/homebrew/bin/python3.10</string> <string>/Users/你的用户名/Documents/A_Openclaw/memclawz/memclawz_server/server.py</string> </array> <key>WorkingDirectory</key> <string>/Users/你的用户名/Documents/A_Openclaw/memclawz</string> <key>EnvironmentVariables</key> <dict> <key>TAOTOKEN_API_KEY</key> <string>sk-你的key</string> </dict> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <dict> <key>Crashed</key> <true/> </dict> <key>StandardOutPath</key> <string>/tmp/memclawz-server.log</string> <key>StandardErrorPath</key> <string>/tmp/memclawz-server.err</string> </dict> </plist>

第二个是定时抽取的 watcher,每 60 秒跑一次:

<!-- ~/Library/LaunchAgents/com.memclawz.watcher.plist --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>com.memclawz.watcher</string> <key>ProgramArguments</key> <array> <string>/opt/homebrew/bin/python3.10</string> <string>/Users/你的用户名/Documents/A_Openclaw/scripts/memory_io.py</string> <string>read</string> </array> <key>EnvironmentVariables</key> <dict> <key>TAOTOKEN_API_KEY</key> <string>sk-你的key</string> </dict> <key>RunAtLoad</key> <true/> <key>StartInterval</key> <integer>60</integer> <key>StandardOutPath</key> <string>/tmp/memclawz-watcher.log</string> </dict> </plist>

两个 plist 里都显式写了EnvironmentVariables,这是踩过的坑:launchd 不读你的.zshrc,不显式声明的话脚本里os.environ.get拿到的是空值,模型调用直接失败。

加载服务:

launchctl load ~/Library/LaunchAgents/com.memclawz.server.plist launchctl load ~/Library/LaunchAgents/com.memclawz.watcher.plist launchctl list | grep memclawz

看到两行带com.memclawz.前缀的记录,说明注册成功。

3.4 接入 OpenClaw 的 AGENTS.md

最后一步是让 OpenClaw 在每次会话开始时读工作记忆。编辑~/Documents/A_Openclaw/AGENTS.md,在## Every Session部分加上:

## Every Session Before doing anything else: 1. Read SOUL.md — this is who you are 2. Read USER.md — this is who you're helping 3. Read memory/qmd/current.json for working memory 4. If in MAIN SESSION, also read MEMORY.md Don't ask permission. Just do it.

改之前先备份:cp AGENTS.md AGENTS.md.backup。这一步是整套系统能不能「记住」的开关,漏了它,前面所有服务都白跑。

4. 验证请求:重启后记忆到底有没有生效

配置写完不算完,必须验证。我按从底层到上层的顺序测,任何一层不通都能立刻定位。

先测向量服务健康检查:

curl -s http://127.0.0.1:4010/health # 期望:{"status":"ok","engine":"zvec","version":"0.2.0"}

再测记忆抽取,手动喂一段文本进去:

python3 ~/Documents/A_Openclaw/scripts/memory_io.py extract \ "决定使用 launchd 作为守护进程方案,端口固定 4010"

跑完看工作记忆文件有没有变化:

cat ~/Documents/A_Openclaw/memory/qmd/current.json

如果decisions数组里出现了「使用 launchd 作为守护进程方案」,说明模型抽取和写回都通了。

然后是关键的端到端验证:重启 launchd 服务,模拟一次「关机再开机」:

launchctl unload ~/Library/LaunchAgents/com.memclawz.server.plist launchctl load ~/Library/LaunchAgents/com.memclawz.server.plist sleep 3 curl -s http://127.0.0.1:4010/health

服务重新拉起后健康检查仍然返回 ok,说明RunAtLoad和KeepAlive生效。

最后验证 OpenClaw 侧。新开一个会话,直接问它:「我们之前定的向量服务端口是多少?」如果它回答 4010,而不是反问你「什么端口」,说明工作记忆被正确读进了上下文。这一步是整个验证的终点,前面所有配置都是为了这一问一答。

如果想让验证更彻底,可以故意 kill 掉服务进程,看 launchd 会不会自动拉起:

pkill -f memclawz_server sleep 5 launchctl list | grep memclawz.server

KeepAlive配置正确的话,进程会被自动重启,launchctl list里那一行的 PID 会变。

5. 本篇常见错排查:401、local proxy failed、reading choices

配置过程中最容易撞的几个报错,我按实际遇到的顺序列出来,对照着查。

401 Unauthorized。模型调用返回 401,九成是 Key 没传对。先确认环境变量在当前 shell 里存在:

echo $TAOTOKEN_API_KEY

如果为空,说明.zshrc没 source。如果 shell 里有值但 launchd 拉起的脚本报 401,那就是 plist 里EnvironmentVariables没写或写错。检查 plist 里 Key 的字符串有没有多余空格,改完必须unload再load才生效。

local proxy failed。这个报错通常出现在脚本里配了代理但代理没起来,或者系统代理设置和脚本里的地址冲突。记忆脚本走的是直连https://taotoken.net/api,不需要任何本地代理。检查config.toml里base_url是不是被误改成了127.0.0.1开头的地址,改回官方出口即可。

reading choices 报错。形如KeyError: 'choices'或list index out of range,说明模型返回的结构和你解析的字段对不上。多半是模型调用失败返回了错误对象,但脚本没判断就直接取choices。在call_model里加一层判断:

if "choices" not in data: print(f"[memory_io] 返回异常: {data}", file=sys.stderr) return None

这样错误会打到 stderr,而不是让脚本崩在半路。

OAuth 相关报错。如果你在 OpenClaw 侧看到 OAuth 字样,那是 OpenClaw 自身的登录态问题,和记忆系统无关。记忆系统只依赖TAOTOKEN_API_KEY这一个凭证,不涉及 OAuth 流程。两者别混在一起排查。

服务加载了但健康检查不通。先看日志:

tail -50 /tmp/memclawz-server.err

常见原因是 Python 路径不对。plist 里写的是/opt/homebrew/bin/python3.10,如果你用的是系统 Python 或别的版本,路径要改。用which python3.10确认实际路径。

记忆写进去了但 OpenClaw 读不到。检查AGENTS.md里的路径是不是相对路径,OpenClaw 的工作目录和你的预期是否一致。最稳的写法是在AGENTS.md里用绝对路径,避免工作目录漂移导致读不到文件。

排查完这些,整套系统基本就稳了。日常维护只需要偶尔看一眼/tmp/memclawz-server.log,确认服务没在反复重启。

6. 长期编码与 Agent 场景,把记忆系统用起来

记忆系统配好只是起点,真正让它产生价值的是日常使用习惯。我自己的做法是:凡是涉及项目约定、命名规范、部署路径、接口端口的对话,都让 OpenClaw 走一遍自动抽取,这样下次新开会话它直接就有背景,不用我重复。

如果你经常跑长时间的编码任务或者多步骤 Agent 流程,记忆系统的价值会更明显。一个跨天甚至跨周的任务,中间断了好几次会话,每次都能从工作记忆里恢复上下文,不用从头交代。这种场景下,除了本地记忆系统,模型出口的稳定性也很关键,频繁的长任务调用建议走 Coding Plan,配额和稳定性更适合持续编码:

Coding Plan 长期编码方案:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=memory_system

如果你更想先把接入细节和 API 用法摸清楚,接入文档在这里,里面有完整的端点和参数说明:

接入文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=memory_system

回到记忆系统本身,有几个后续可以做的优化方向。一是给工作记忆加定期压缩,current.json里的数组会越滚越长,超过一定条数后让模型做一次归并,把重复的决策合并掉。二是把向量库的检索结果也回灌进会话开始流程,这样 Layer 0 和 Layer 1 能联动,不只是读热数据。三是给抽取脚本加一个 dry-run 模式,先看模型会抽什么再决定写不写,避免把噪音写进长期记忆。

最后提醒一个实操细节:current.json是纯文本,你可以随时手动编辑。如果发现某条记忆是错的,直接改文件比让模型重新抽取更快。改完不用重启服务,下次会话开始读到的就是新内容。这套系统的好处就在于它足够透明,所有记忆都是你能看见、能改的本地文件,而不是藏在某个黑盒里。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 16:22:58

AI 采集器配置实战:Claude Code、OpenAI、LiteLLM 监控接入 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 16:20:38

Linux基本管理及命令(上):从命令行到实战排障

说实话&#xff0c;搜索“linux常用命令”的人&#xff0c;大部分并不缺一份命令列表&#xff0c;缺的是把这些命令串联起来的管理思路。我带过的实习生里&#xff0c;凡是能稳定处理线上故障的&#xff0c;无一例外都是把基础命令练成了肌肉记忆&#xff1b;反过来&#xff0c…

作者头像 李华
网站建设 2026/10/2 16:20:04

自己的人生态度(从ai到职业生涯,再到健康)

从初中开始&#xff0c;生长和心智处在中等偏上的状态&#xff01;为什么这样&#xff0c;我也不知道&#xff01;可能和活过来了有关系吧&#xff01;六年级之前&#xff0c;小命差点不保&#xff01;或许只有经历过&#xff0c;才能激活人生状态&#xff01;前半生&#xff0…

作者头像 李华