source: Young 提供的配置文档
OpenClaw 用 QMD 作为记忆搜索引擎的配置指南
> 基于 openclaw 与 qmd 当前源码整理。openclaw 已内置 QMD backend,不用自己写集成——所有逻辑在 packages/memory-host-sdk/src/host/ 下,通过子进程方式调用 qmd CLI。
一句话版本: 一份把 QMD 记忆引擎接入 OpenClaw 的完整配置手册——一行启用、三种搜索模式、完整 Schema、中文调优到排错验证,不需写集成代码。
1. 工作原理速览
- openclaw 把 QMD 当作 本地 sidecar,通过
spawn qmd ...调子命令(不是 in-process 调 SDK)。 - 启用后 openclaw 在
~/.openclaw/agents/下建独立 QMD home,自动管理 collections、/qmd/ qmd update、qmd embed。 - 默认 collection 自动覆盖
MEMORY.md+memory/*/.md,每个 agent 有独立命名空间(collection 名字加-后缀)。 - QMD 不可用、超时或失败时,自动 fallback 到 builtin SQLite 引擎,用户无感。
- 模型/算子全程在本地(QMD 自带 BM25 + sqlite-vec + node-llama-cpp 跑 GGUF)。
关键源码入口:
| 文件 | 作用 |
|---|---|
| `packages/memory-host-sdk/src/host/backend-config.ts` | 配置解析与默认值 |
| `packages/memory-host-sdk/src/host/qmd-process.ts` | 子进程 spawn / 二进制可用性探测 |
| `packages/memory-host-sdk/src/host/qmd-query-parser.ts` | 解析 `qmd ... --json` 输出 |
| `packages/memory-host-sdk/src/host/qmd-scope.ts` | 作用域规则(哪些会话能触发搜索) |
| `docs/concepts/memory-qmd.md` | 概念文档 |
| `docs/reference/memory-config.md` | 完整 schema 参考 |
2. 前置条件
- 全局安装 QMD:
bun install -g @tobilu/qmd(或npm install -g) - macOS 需要 Homebrew 的 SQLite(带扩展):
brew install sqlite - macOS / Linux 原生支持,Windows 走 WSL2
- 确保
qmd在 openclaw gateway 进程的PATH里。如果作为 service 运行,PATH 可能与 shell 不同,建议建 symlink:
`sh
sudo ln -s ~/.bun/bin/qmd /usr/local/bin/qmd
`
或在配置里写绝对路径:memory.qmd.command: "/usr/local/bin/qmd"
3. 最小配置:一行启用
在 openclaw 配置(顶层 memory 段):
{
memory: {
backend: "qmd",
},
}
只需这一行,openclaw 会自动:
- 创建 agent 隔离的 QMD home
- 注册默认的
MEMORY.md+memory/*/.md两个 collection - 启动周期更新(默认 5 分钟)
- 必要时跑
qmd embed(仅vsearch/query模式)
4. 三种搜索模式
通过 memory.qmd.searchMode 切换,性能/质量取舍:
| 模式 | QMD 命令 | 说明 |
|---|---|---|
| `search`(默认) | `qmd search` | 纯 BM25,最快;不触发向量就绪检查/embed 维护 |
| `vsearch` | `qmd vsearch` | 向量语义搜索,需先跑过 `qmd embed` |
| `query` | `qmd query` | 混合 + LLM 重排,质量最高;CPU 上慢;首次会下载约 2 GB GGUF |
> openclaw 有兜底:配的模式失败时会自动用 qmd query 重试一次。
> ⚠ 中文/CJK 警示:QMD 的 FTS5 tokenizer 在 src/store.ts:837 硬编码为 porter unicode61,不切分中文——一段连续汉字会被当成一个 token,BM25 排序基本失效。中文内容请使用 vsearch 或 query,并换 Qwen3 Embedding(见 §8)。
5. 完整配置 schema
字段全部来源于 config-utils.ts 中的 MemoryQmdConfig 类型。默认值见 backend-config.ts。
{
memory: {
backend: "qmd",
citations: "auto", // auto | on | off — 片段是否追加 Source: path#line
qmd: {
command: "qmd", // 可写绝对路径,例如 "/usr/local/bin/qmd"
searchMode: "search", // search | vsearch | query
includeDefaultMemory: true, // 自动索引 MEMORY.md + memory/**
// 额外索引目录
paths: [
{ name: "docs", path: "~/notes", pattern: "**/*.md" },
{ name: "specs", path: "~/work/docs", pattern: "**/*.md" },
],
// 历史会话转录索引(跨会话回忆)
sessions: {
enabled: false,
exportDir: "~/.openclaw/transcripts",
retentionDays: 90,
},
// 增量/重建调度
update: {
interval: "5m", // 周期更新频率
debounceMs: 15000,
onBoot: true, // manager 打开时立即刷新
startup: "off", // off | idle | immediate — 网关启动时预热
startupDelayMs: 120000,
waitForBootSync: false,
embedInterval: "60m",
commandTimeoutMs: 30000,
updateTimeoutMs: 120000,
embedTimeoutMs: 120000,
},
// 注入到 prompt 的限额
limits: {
maxResults: 4,
maxSnippetChars: 450,
maxInjectedChars: 2200,
timeoutMs: 4000, // 单次搜索超时;CPU 慢时调到 120000
},
// 哪些会话能触发 QMD 搜索(同 session.sendPolicy schema)
scope: {
default: "deny",
rules: [
{ action: "allow", match: { chatType: "direct" } },
],
},
// 可选:把 QMD 以 MCP server 暴露给 agent
mcporter: {
enabled: false,
serverName: "qmd",
startDaemon: true,
},
},
},
}
6. 多 agent / 跨 agent 共享集合
每个 agent 默认有独立的 collection 命名空间(自动加 - 后缀)。需要让多个 agent 共享同一索引时,用 agent 级配置而不是顶层 memory.qmd.paths:
{
agents: {
defaults: {
memorySearch: {
qmd: {
extraCollections: [
{ name: "shared-handbook", path: "~/team/handbook", pattern: "**/*.md" },
],
},
},
},
list: [
{
id: "alice",
memorySearch: {
qmd: {
extraCollections: [
{ name: "alice-private", path: "~/alice/notes" },
],
},
},
},
],
},
}
合并顺序:agents.defaults.memorySearch.qmd.extraCollections → 各 agent 的 extraCollections → memory.qmd.paths。同一 path 重复时保留首条。
7. 模型覆盖(不在 openclaw 配置里)
QMD 的 GGUF 模型用 环境变量覆盖,openclaw 透传给子进程。中文/多语料库强烈建议换 embedding 模型:
export QMD_EMBED_MODEL="hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf"
export QMD_RERANK_MODEL="/abs/path/reranker.gguf"
export QMD_GENERATE_MODEL="/abs/path/generator.gguf"
# 换嵌入模型后必须重建索引
qmd embed -f
8. 中文场景:必须走向量
为什么不能用 `search`
QMD 在 qmd/src/store.ts:837 硬编码了 FTS5 tokenizer:
CREATE VIRTUAL TABLE ... USING fts5(filepath, title, body, tokenize='porter unicode61')
unicode61按空白/标点切分,中文里没有空格 → 整段连续汉字变一个 token。porter是英文词干化,对中文无效。- BM25 IDF 在"超长 token + 文档间几乎无重叠"下退化,排序基本只看子串包含。
而且 QMD 没有 trigram 切换的开关(grep -rn 'trigram\|jieba\|cjk' src/ 全空)。结论:中文内容上 QMD,只能走 vsearch 或 query。
推荐配置:vsearch + Qwen3 Embedding
第 1 步:换 embedding 模型(环境变量,全局生效)
加进 ~/.zshrc 或 gateway 的 systemd/launchd unit:
export QMD_EMBED_MODEL="hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf"
> Qwen3-Embedding-0.6B 是 QMD README 明确推荐的多语言(含 CJK)模型,MTEB 排名靠前;约 600 MB,首次 qmd embed 时自动从 HuggingFace 下载到 ~/.cache/qmd/models/。
第 2 步:openclaw 配置切到 `vsearch`
{
memory: {
backend: "qmd",
citations: "auto",
qmd: {
searchMode: "vsearch", // 纯向量;中文最稳的选择
includeDefaultMemory: true,
paths: [
{ name: "notes", path: "~/notes", pattern: "**/*.md" },
],
limits: {
maxResults: 6,
timeoutMs: 30000, // 向量编码 + 检索慢,放宽
},
update: {
interval: "5m",
embedInterval: "60m", // 单独的 embed 节奏
startup: "idle", // 启动后空闲时再预热
startupDelayMs: 60000,
},
},
},
}
需要更高质量召回时,把 searchMode 换成 query——会启用 LLM 重排和查询扩展,但 CPU 机器上单次可能 5–30 秒。
第 3 步:预热索引(手动执行一次)
> 重要:openclaw 的 CLAUDE.md 明确禁止它自动跑 qmd embed。第一次切换 embedding 模型必须手动重建。
# 1. 先确认 collections 已就位
qmd status
# 2. 拉取/更新所有 collection 的文件元数据
qmd update
# 3. 用 Qwen3 重建全量向量(-f 强制重嵌入)
qmd embed -f
# 4. 跑一次中文查询确认链路
qmd vsearch "项目时间线" -n 5
之后 openclaw 启动时会自己按 update.embedInterval 周期增量 embed。
第 4 步:验证
openclaw memory status --deep
输出里应能看到 backend = qmd、searchMode = vsearch、collections 列出来、vector_ready: true。
中英混合内容怎么办
英文段落 BM25 仍然好用,混合内容把 searchMode 设为 query:
{ memory: { backend: "qmd", qmd: { searchMode: "query" } } }
query 模式同时跑 BM25 + 向量并用 RRF 融合(见 QMD README 的 Hybrid Search Pipeline 图):英文走 BM25 拿精确匹配,中文走向量拿语义召回,最后 LLM 重排。代价是首次需要下额外的 ~640 MB rerank 模型 + ~1.1 GB query expansion 模型。
索引质量进一步优化(可选)
QMD 1.x 之后的 chunking 默认按 markdown 标题/段落切,900 token/块、15% overlap。中文一个 token 通常对应 0.5–1 个汉字,所以一块约 600–900 字,对中文长文档基本够用。代码文件想按函数/类边界切,加:
qmd embed -f --chunk-strategy auto
(仅 .ts/.tsx/.js/.jsx/.py/.go/.rs 生效;中文 markdown 不受影响)
9. 验证与排错
# openclaw 视角检查 backend / collections / 二进制可用性
openclaw memory status --deep
# QMD 自身的索引健康
qmd status
# 确认是否支持多 -c 过滤(影响 openclaw 是否能合并子进程)
qmd --help | grep -i collection
常见问题(汇总自 docs/concepts/memory-qmd.md Troubleshooting):
| 现象 | 处理 |
|---|---|
| `spawn qmd ENOENT` | gateway 的 PATH 与 shell 不同。配 `memory.qmd.command` 写绝对路径,或建 symlink |
| 首次搜索极慢 | `qmd query` 在下载 ~2 GB GGUF。提前在 shell 里跑一次 `qmd query "test"` 预热 |
| 搜索超时 | 调高 `memory.qmd.limits.timeoutMs`(默认 4000,慢机器用 120000) |
| BM25-only 也试图编译 llama.cpp | 显式设 `searchMode: "search"`,openclaw 会跳过向量就绪检查 |
| group chat 永远空结果 | 默认 scope 仅 direct + channel,按需放开 group |
| 子进程数量过多 | 升级 QMD 到支持多 `-c` 过滤的版本,openclaw 会自动用单进程多 collection |
10. 三段式上手路径
按从轻到重推荐:
1. 试水:只配 memory.backend: "qmd",用 searchMode: "search",体感 BM25 召回。
2. 加额外目录:memory.qmd.paths 把笔记/文档/会议纪要纳入索引;按需开 sessions.enabled。
3. 上语义/重排:换中文 embedding 模型 → qmd embed -f → searchMode: "query",并放宽 limits.timeoutMs。
附录 A:trigram tokenizer 速览
§4 提到 QMD 硬编码 porter unicode61,§8 提到 builtin backend 支持 trigram。这里解释下 trigram 是什么、为什么能救中文 BM25。
定义
trigram = 3-gram = 用 3 个字符的滑动窗口把字符串切成重叠片段,每段当一个索引 token。
"hello" → hel, ell, llo
"项目时间线" → 项目时, 目时间, 时间线
查询时 query 也走同样规则,最后取 trigram 集合交集做匹配,BM25 在交集大小上排序。
为什么对中文比 `unicode61` 好
| 维度 | `unicode61`(QMD 当前) | `trigram` |
|---|---|---|
| 切分依据 | 空白 + 标点 | 固定 3 字符滑窗 |
| 中文一段汉字的 token 数 | 1(整段视为一个词) | N − 2(N 是字符数) |
| 不同文档间是否共享 token | 几乎不共享 | 共享大量 trigram |
| BM25 IDF 是否有效 | 退化为子串包含 | 重新有效 |
| 任意子串能否命中 | 仅整段匹配 | 任意 ≥ 3 字符子串 |
核心:trigram 让"不同文档共享 token"重新成立,BM25 的 TF/IDF 才有意义。
代价
1. 索引膨胀:一篇 1000 字中文文档约产生 998 个 trigram,索引体积通常是源文 3–5×。
2. 没有语义:"汽车" 与 "轿车" 没有共享 trigram,仍互不召回——这是 vsearch/query 的位置。
3. 查询长度门槛:< 3 字符走不了索引,会退化为 LIKE 全扫;避免 1–2 字关键词。
4. 跨语言混搭:英文上 trigram 不如 porter unicode61 精准,更适合 CJK / 子串 / 代码片段。
一行验证
需要 SQLite ≥ 3.34(macOS Homebrew 版都满足):
sqlite3 :memory: <<'SQL'
CREATE VIRTUAL TABLE t USING fts5(body, tokenize='trigram');
INSERT INTO t VALUES('公司项目时间线安排'), ('项目周报');
SELECT body FROM t WHERE t MATCH '项目时间';
SQL
应返回 公司项目时间线安排——trigram 项目时 和 目时间 同时命中。
在选型矩阵里的位置
中文 BM25
├─ unicode61(QMD 当前硬编码) → 基本不可用
├─ trigram(openclaw builtin 可切换) → 可用,索引大、无语义
└─ 向量 embedding(QMD vsearch/query) → 最准、有语义、慢且需模型
trigram 是"穷人的中文分词"——无分词器、无词典、无 GPU,靠暴力滑窗换可用性。解不了同义词,但能让 BM25 在中文上重新工作。
> 跟踪项:QMD PR #623(kechol 提,2026-05-04 OPEN)正是给 store.ts:837 加 QMD_FTS_TOKENIZER 环境变量并把 trigram 加入白名单。合并后,本文档 §4 的 CJK 警示和 §8 的"必须走向量"结论都会松动——search 模式可重新进入中文场景的可选集。
附录 B:trigram vs 词级分词
附录 A 把 trigram 和 unicode61 比,是想说服你"中文 BM25 起码要 trigram 起步"。但很多人会问:为什么不直接上 jieba 这种真正的中文分词?这里把 trigram 和分词路线放在一起对比。
核心区别
- trigram:不懂中文,靠 3 字符滑窗暴力覆盖。
- 分词:懂中文(词典 + HMM/CRF),按"词"边界切。
维度对比
| 维度 | trigram | 词级分词(jieba 类) |
|---|---|---|
| 是否需要语言知识 | 否 | 是(词典 + 统计模型) |
| token 粒度 | 3 字符滑窗(重叠) | 中文词,平均 2 字 |
| N 字汉字产生的 token 数 | N − 2 | ≈ N / 2 |
| 索引膨胀(vs 源文 byte) | ~3–5× | ~1.5–2× |
| 召回完整性 | 高(任意 ≥3 字符子串) | 取决于分词质量 |
| 精度 | 低("机器学习" 与 "机器人" 共享 `机器`) | 高(边界对了就不会乱召回) |
| 短查询(1–2 字)走索引 | ❌(退化 LIKE 全扫) | ✅ |
| 未登录词 / 新词 | ✅ 天然支持 | ❌ 词典外切错 |
| 同义词 / 近义词 | ❌ | ❌(BM25 范畴外,靠 query expansion 或向量) |
| 部署复杂度 | 零依赖(SQLite 自带) | 需 SQLite 扩展或预分词 pipeline |
| 词典维护 | 无 | 持续维护(领域词、人名、产品名) |
| 中英混合 | 二者一视同仁,英文上不如 porter | 英文需另走 unicode61 / porter |
| 查询时延 | 慢一档(token 多) | 快 |
一个能看出区别的例子
文档:机器学习正在改变世界
| 查询 | trigram | jieba 默认(精确模式) |
|---|---|---|
| `机器学习` | trigram `机器学` + `器学习` 命中 | token `机器学习` 命中 |
| `机器人` | 文档没有 `机器人` trigram → ❌ 不误召 | 词不存在 → ❌ |
| `学习正在` | `学习正` + `习正在` 命中 | `[学习, 正在]` 短语查询命中 |
| `机器` | < 3 字符走 LIKE,仍可命中 | 被吞进 `机器学习` → ❌ **召回不到** |
| `深度学习` | 共享 `度学习` 1 个 trigram → 弱命中(**误召回**) | 词典里独立词,文档没有 → ❌ 不误召 |
真实场景下方向相反:trigram 偏召回,分词偏精度。jieba 的"最大粒度"特性反而让 2 字子词搜不到(除非用 cut_for_search 模式)。
QMD 当前状态下各路线的成本
| 路线 | 实现成本 | 状态 |
|---|---|---|
| trigram | env var 切换,零改造 | [PR #623](https://github.com/tobi/qmd/pull/623) OPEN |
| 分词 + SQLite FTS5 自定义 tokenizer | 加载 C 扩展(如 `wangfenjin/simple-tokenizer`、`signalapp/Signal-FTS5-Extension`),改 QMD 的 `Database` 初始化 + `getFtsTokenizer` 放开扩展名校验 | 无人做 |
| 预分词 pipeline | 在 QMD indexer 前注入 jieba,token 间用空格连接,仍用 `unicode61`;查询端也要同样分词 | 无 hook,需 fork |
| 向量 (vsearch) | 换 Qwen3-Embedding,env var + `qmd embed -f` | 现成可用 |
结论:QMD 上没有"开关式"分词选项。想要词级分词,要么写 SQLite 扩展,要么 fork QMD 改 indexer。trigram 路线(PR #623)是几乎零成本的折中。
决策建议
中文检索目标
├─ 只要搜得到(召回 > 精度) → trigram(PR #623 合并后开 env var)
├─ 精确语义匹配 + 同义词 → vsearch + Qwen3-Embedding(QMD 现成)
├─ 精确词边界 + 零误召回 → 分词,但 QMD 上要自写扩展;
│ 此时不如换 Elasticsearch + IK / Meilisearch
└─ 召回与精度都要 → query 模式 = trigram BM25 ⊕ 向量 ⊕ rerank
(PR #623 合并后才真正成立)
实战上最常见的最佳组合是 trigram + 向量混合:
- trigram 解决"我记得里面有 XX 字"的精确召回;
- 向量解决"我问的是什么意思"的语义召回;
- rerank 把两路合并。
这正是 QMD query 模式的设计意图。但因为 BM25 lane 当前对中文失效,混合检索还没真正发挥——PR #623 合并后这套架构才完整。真要给中文召回质量做一次跃迁,等这个 PR 比换 embedding 模型更值得跟进。
完整字段权威来源仍以 packages/memory-host-sdk/src/host/config-utils.ts 与 backend-config.ts 为准;行为权威来源以 docs/concepts/memory-qmd.md 与 docs/reference/memory-config.md 为准。
评分
| 维度 | 评分 | 说明 |
|---|---|---|
| 实用性 | ⭐⭐⭐⭐⭐ | 操作指令从配到排错都覆盖了 |
| 深度 | ⭐⭐⭐⭐⭐ | 源码级分析 + trigram 附录深入 |
| 中文适用性 | ⭐⭐⭐⭐⭐ | 专门花了 §8 + 两个附录讲中文 |
| 独特性 | ⭐⭐⭐⭐ | 同类配置指南很少见 |
| **综合** | **4.8/5** | 这份 OpenClaw + QMD 的记忆配置指南是目前最完整的 |