# QMD 记忆搜索

QMD 是一个本地语义搜索引擎，可以替代 Openclaw-CN 的内置记忆搜索后端。它在本地运行 GGUF 模型进行查询扩展、向量嵌入和重排序，**无需任何远程 API 密钥**。

相关文档：
- 记忆概念：[记忆](/concepts/memory)
- CLI 命令：[clawdbot memory](/cli/memory)

---

## 前置要求

- **Node.js 22+**
- **Bun**（用于安装 QMD）
- **磁盘空间**：约 2.5 GB（模型文件）
- **内存**：建议 4 GB 以上可用内存（加载 GGUF 模型）

---

## 安装

### 1. 安装 Bun

如果尚未安装 Bun：

```bash
# macOS / Linux
brew install oven-sh/bun/bun

# 或使用官方安装脚本
curl -fsSL https://bun.sh/install | bash
```

### 2. 安装 QMD

```bash
bun install -g https://github.com/tobi/qmd
```

安装完成后，确认 QMD 可用：

```bash
qmd --version
```
:::tip
如果提示 `command not found`，需要将 Bun 的全局安装目录添加到 PATH：

```bash
echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
```
:::


### 3. 预下载模型（推荐）

QMD 首次运行 `query` 命令时会自动从 HuggingFace 下载模型，但这可能需要较长时间。建议提前下载：

```bash
# 进入工作区目录
cd ~/.openclaw/workspace

# 初始化索引
qmd update

# 运行一次查询来触发模型下载
# 这会下载 query-expansion (~1.3 GB) 和 reranker (~640 MB)
qmd query "test" -c memory-root --json
```
:::info
首次下载模型可能需要 10-60 分钟，取决于网络速度。模型缓存在 `~/.cache/qmd/models/` 目录下，后续启动只需几秒加载。
:::


#### 中国大陆用户

如果从 HuggingFace 下载速度慢，可以设置镜像：

```bash
export HF_ENDPOINT=https://hf-mirror.com
qmd query "test" -c memory-root --json
```

也可以将镜像配置写入 shell 配置文件：

```bash
echo 'export HF_ENDPOINT=https://hf-mirror.com' >> ~/.zshrc
source ~/.zshrc
```

---

## 配置 Openclaw-CN

### 启用 QMD 后端

```bash
openclaw-cn config set memory.backend qmd
```

### 配置作用域（推荐）

默认情况下，QMD 仅在**私聊**中启用搜索。如需在所有场景（包括群聊和 CLI）中使用：

```bash
openclaw-cn config set memory.qmd.scope.default allow
```

### 调整查询超时（按需）

默认查询超时为 4 秒。在低配设备（如 MacBook Air）上，模型加载和推理可能需要更长时间：

```bash
# 设置为 30 秒（推荐低配设备）
openclaw-cn config set memory.qmd.limits.timeoutMs 30000
```

### 完成后重启网关

```bash
# 配置修改后需重启网关才能生效
openclaw-cn gateway restart
```

### 完整配置示例

以下是 `~/.openclaw/openclaw.json` 中 memory 部分的完整配置示例：

```json5
{
  memory: {
    backend: "qmd",
    qmd: {
      // QMD 可执行文件路径（默认使用 PATH 中的 qmd）
      command: "qmd",

      // 搜索模式：query（完整语义搜索）| search（BM25）| vsearch（向量搜索）
      searchMode: "query",

      // 是否包含默认记忆文件（MEMORY.md、memory.md、memory/**/*.md）
      includeDefaultMemory: true,

      // 额外索引路径
      paths: [
        { path: "~/notes", pattern: "**/*.md", name: "my-notes" }
      ],

      // 更新配置
      update: {
        interval: "5m",          // 自动更新间隔
        debounceMs: 15000,       // 防抖时间
        onBoot: true,            // 启动时自动更新索引
        waitForBootSync: false,  // 是否等待启动更新完成
        embedInterval: "60m",    // 嵌入向量更新间隔
        commandTimeoutMs: 30000, // collection 操作超时
        updateTimeoutMs: 120000, // 索引更新超时
        embedTimeoutMs: 120000   // 嵌入更新超时
      },

      // 查询限制
      limits: {
        maxResults: 6,           // 最大返回结果数
        maxSnippetChars: 700,    // 片段最大字符数
        maxInjectedChars: 4000,  // 注入上下文最大字符数
        timeoutMs: 30000         // 查询超时（毫秒）
      },

      // 作用域控制
      scope: {
        default: "allow",        // 默认策略：allow | deny
        rules: [
          // 可以按频道和聊天类型配置规则
          // { action: "allow", match: { chatType: "direct" } }
          // { action: "deny", match: { channel: "telegram", chatType: "group" } }
        ]
      },

      // 会话索引（实验性）
      sessions: {
        enabled: false,          // 是否索引会话记录
        retentionDays: 30        // 保留天数
      }
    }
  }
}
```

---

## 准备记忆文件

QMD 默认索引工作区（`~/.openclaw/workspace`）中的以下文件：

| Collection | 路径 | 匹配模式 |
|---|---|---|
| `memory-root` | `~/.openclaw/workspace/` | `MEMORY.md` |
| `memory-alt` | `~/.openclaw/workspace/` | `memory.md` |
| `memory-dir` | `~/.openclaw/workspace/memory/` | `**/*.md` |

确保工作区目录和记忆文件存在：

```bash
# 创建工作区（如果不存在）
mkdir -p ~/.openclaw/workspace/memory

# 创建一个测试记忆文件
cat > ~/.openclaw/workspace/MEMORY.md << 'EOF'
# 个人记忆

## 偏好
- 喜欢简洁的代码风格
- 倾向使用 TypeScript

## 项目笔记
- 当前在开发 Openclaw-CN 项目
EOF
```

---

## 验证

### 1. 检查 QMD 安装

```bash
which qmd && qmd --version
```

### 2. 检查索引状态

```bash
# 手动更新索引
cd ~/.openclaw/workspace && qmd update

# 查看状态
qmd status
```

预期输出应显示已注册的 collection 和已索引的文件数。

### 3. 直接测试 QMD 搜索

```bash
cd ~/.openclaw/workspace && qmd query "偏好" -c memory-root --json
```

预期输出为包含匹配结果的 JSON 数组，score 大于 0。

### 4. 通过 CLI 测试

```bash
openclaw-cn memory search "偏好"
```

预期输出类似：

```
0.930 MEMORY.md:1-5
# 个人记忆
## 偏好
- 喜欢简洁的代码风格
- 倾向使用 TypeScript
```

### 5. 检查记忆状态

```bash
openclaw-cn memory status
openclaw-cn memory status --deep
```

---

## 工作原理

### 搜索流程

当用户发起搜索时（通过 `memory_search` 工具或 `openclaw-cn memory search`），QMD 执行以下步骤：

1. **查询扩展**：使用本地 LLM（qwen3-0.6B）将查询扩展为多个搜索变体（同义词、关键词、假设文档）
2. **候选检索**：通过 BM25（关键词）和向量搜索（语义）检索候选文档
3. **重排序**：使用本地 reranker（qwen3-reranker-0.6b）对候选文档进行精确排序
4. **返回结果**：返回排名最高的文档片段

### 模型文件

QMD 使用以下 GGUF 模型（首次使用时自动下载）：

| 模型 | 用途 | 大小 |
|---|---|---|
| `embeddinggemma-300M-Q8_0` | 文本向量嵌入 | ~330 MB |
| `qmd-query-expansion-1.7B` | 查询扩展 | ~1.3 GB |
| `qwen3-reranker-0.6b-q8_0` | 结果重排序 | ~640 MB |

模型缓存路径：`~/.cache/qmd/models/`

### 索引隔离

网关运行时，QMD 使用隔离的 XDG 环境避免与其他进程冲突：

- 索引路径：`~/.openclaw/agents/main/qmd/xdg-cache/qmd/index.sqlite`
- 模型会通过 symlink 共享 `~/.cache/qmd/models/`，避免重复下载

---

## 常见问题

### 搜索返回空结果

- **检查索引**：运行 `cd ~/.openclaw/workspace && qmd status` 确认文件已索引
- **检查作用域**：确认 `memory.qmd.scope.default` 设置为 `allow`
- **检查超时**：如果日志显示 `timed out`，增大 `memory.qmd.limits.timeoutMs`

### 模型下载失败

- 检查网络连接
- 中国大陆用户设置 `HF_ENDPOINT=https://hf-mirror.com`
- 模型下载支持断点续传，失败后重试即可

### 搜索速度慢

- 首次查询需加载模型到内存，后续查询会显著加快
- 确保没有其他进程同时占用大量内存
- 低配设备可考虑使用 `searchMode: "search"`（仅 BM25，不使用 LLM）

### QMD 与内置搜索的区别

| 特性 | 内置搜索 | QMD |
|---|---|---|
| 依赖 | 无额外依赖 | 需安装 QMD + 下载模型 |
| API 密钥 | 需要嵌入提供商密钥 | 不需要 |
| 搜索质量 | BM25 + 向量（远程嵌入） | 查询扩展 + BM25 + 向量 + 重排序 |
| 离线可用 | 仅 BM25（无远程嵌入时） | 完全离线 |
| 磁盘占用 | 极小 | ~2.5 GB（模型） |
| 首次查询 | 快 | 较慢（加载模型） |

---

## 配置参考

### `memory.backend`

记忆搜索后端。设为 `"qmd"` 启用 QMD。

| 值 | 说明 |
|---|---|
| `"builtin"` | 默认内置搜索 |
| `"qmd"` | 使用 QMD 本地语义搜索 |

### `memory.qmd.command`

QMD 可执行文件路径。默认：`"qmd"`（从 PATH 查找）。

### `memory.qmd.searchMode`

搜索模式。默认：`"query"`。

| 值 | 说明 |
|---|---|
| `"query"` | 完整语义搜索（查询扩展 + BM25 + 向量 + 重排序） |
| `"search"` | 仅 BM25 关键词搜索 |
| `"vsearch"` | 仅向量搜索 |

### `memory.qmd.scope`

控制哪些聊天场景可以触发记忆搜索。

```json5
{
  default: "deny",  // 默认策略
  rules: [
    { action: "allow", match: { chatType: "direct" } },
    { action: "deny", match: { channel: "telegram", chatType: "group" } }
  ]
}
```

`match` 支持的字段：
- `chatType`：`"direct"` | `"group"` | `"channel"`
- `channel`：频道名称（如 `"telegram"`、`"feishu"`、`"slack"`）
- `keyPrefix`：会话键前缀匹配

### `memory.qmd.limits`

| 字段 | 默认值 | 说明 |
|---|---|---|
| `maxResults` | `6` | 最大返回结果数 |
| `maxSnippetChars` | `700` | 每个结果片段最大字符数 |
| `maxInjectedChars` | `4000` | 注入上下文窗口的最大总字符数 |
| `timeoutMs` | `4000` | 查询超时（毫秒），低配设备建议设为 `30000` |

### `memory.qmd.update`

| 字段 | 默认值 | 说明 |
|---|---|---|
| `interval` | `"5m"` | 自动更新索引间隔（支持 `s`/`m`/`h` 后缀） |
| `debounceMs` | `15000` | 文件变更后的防抖等待时间 |
| `onBoot` | `true` | 网关启动时自动更新索引 |
| `waitForBootSync` | `false` | 是否阻塞启动直到索引更新完成 |
| `embedInterval` | `"60m"` | 嵌入向量更新间隔 |
| `commandTimeoutMs` | `30000` | collection 操作超时 |
| `updateTimeoutMs` | `120000` | 索引更新超时 |
| `embedTimeoutMs` | `120000` | 嵌入更新超时 |

### `memory.qmd.paths`

添加额外的索引路径（除默认记忆文件外）：

```json5
paths: [
  { path: "~/notes", pattern: "**/*.md", name: "my-notes" },
  { path: "./docs", pattern: "*.md", name: "project-docs" }
]
```

- `path`：目录路径（绝对路径或相对于工作区）
- `pattern`：glob 匹配模式（默认 `**/*.md`）
- `name`：collection 名称（可选，自动生成）

### `memory.qmd.sessions`

实验性功能：索引会话记录。

| 字段 | 默认值 | 说明 |
|---|---|---|
| `enabled` | `false` | 是否启用 |
| `exportDir` | 自动 | 会话导出目录 |
| `retentionDays` | 无限制 | 保留天数 |
