QMD - 查询标记文档(Query Markup Documents)

一款用于记忆一切所需信息的设备端搜索引擎。为您的 Markdown 笔记、会议记录、技术文档和知识库建立索引。

支持关键词或自然语言搜索。非常适合您的智能体流程。

QMD 结合了 BM25 全文检索、向量语义检索以及 LLM 重排序——所有这些均通过 node-llama-cpp 与 GGUF 模型在本地运行。

QMD 架构图

您可以在CHANGELOG中阅读更多关于 QMD 的进展。

快速开始

# 全局安装(Node 或 Bun)
npm install -g @tobilu/qmd
# 或
bun install -g @tobilu/qmd

# 直接运行
npx @tobilu/qmd ...
bunx @tobilu/qmd ...

# 为您的笔记、文档和会议记录创建集合
qmd collection add ~/notes --name notes
qmd collection add ~/Documents/meetings --name meetings
qmd collection add ~/work/docs --name docs

# 添加上下文以帮助改善搜索结果,当匹配的子文档被返回时,每一段上下文都会被一并返回。这构成了一棵树。这是 QMD 的关键特性,因为它使 LLM 在选择文档时能够做出更好的上下文决策。不要忽视它!
qmd context add qmd://notes "个人笔记和想法"
qmd context add qmd://meetings "会议记录和笔记"
qmd context add qmd://docs "工作文档"

# 生成向量嵌入以支持语义搜索
qmd embed

# 跨所有内容进行搜索
qmd search "项目时间线"           # 快速关键词搜索
qmd vsearch "如何部署"             # 语义搜索
qmd query "季度规划流程"            # 混合检索 + 重排序(最佳质量)

# 获取特定文档
qmd get "meetings/2024-01-15.md"

# 通过 docid 获取文档(显示在搜索结果中)
qmd get "#abc123"

# 通过 glob 模式批量获取多个文档
qmd multi-get "journals/2025-05*.md"

# 在特定集合内搜索
qmd search "API" -c notes

# 为智能体导出所有匹配结果
qmd search "API" --all --files --min-score 0.3

与 AI 智能体一起使用

QMD 的 --json--files 输出格式专为智能体工作流设计:

# 获取供 LLM 使用的结构化结果
qmd search "authentication" --json -n 10

# 列出超过某个分数阈值的所有相关文件
qmd query "error handling" --all --files --min-score 0.4

# 检索完整文档内容
qmd get "docs/api-reference.md" --full

MCP 服务器

虽然您只需让智能体在命令行上使用该工具就可以完美运行,但 QMD 也提供了一个 MCP(模型上下文协议)服务器以实现更紧密的集成。

暴露的工具:

  • query — 使用类型化的子查询(lex/vec/hyde)进行搜索,通过 RRF + 重排序组合
  • get — 通过路径或 docid 检索文档(支持模糊匹配建议)
  • multi_get — 通过 glob 模式、逗号分隔列表或 docid 批量检索
  • status — 索引健康度和集合信息

Claude Desktop 配置~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "mcpServers": {
    "qmd": {
      "command": "qmd",
      "args": ["mcp"]
    }
  }
}

Claude Code — 安装插件(推荐):

claude plugin marketplace add tobi/qmd
claude plugin install qmd@qmd

或者在 ~/.claude/settings.json 中手动配置 MCP:

{
  "mcpServers": {
    "qmd": {
      "command": "qmd",
      "args": ["mcp"]
    }
  }
}

HTTP 传输

默认情况下,QMD 的 MCP 服务器使用 stdio(由每个客户端作为子进程启动)。要使用共享的、长期运行的服务器以避免重复加载模型,请使用 HTTP 传输:

# 前台运行(Ctrl-C 停止)
qmd mcp --http                    # localhost:8181
qmd mcp --http --port 8080        # 自定义端口

# 后台守护进程
qmd mcp --http --daemon           # 启动,将 PID 写入 ~/.cache/qmd/mcp.pid
qmd mcp stop                      # 通过 PID 文件停止
qmd status                        # 当激活时显示 "MCP: running (PID ...)"

HTTP 服务器暴露两个端点:

  • POST /mcp — MCP 可流式 HTTP(JSON 响应,无状态)
  • GET /health — 存活检查,包含运行时间

LLM 模型在跨请求期间保留在显存中。嵌入/重排序上下文在空闲 5 分钟后被释放,并在下一个请求时透明地重新创建(约 1 秒的代价,模型保持加载状态)。

将任何 MCP 客户端指向 http://localhost:8181/mcp 即可连接。

SDK / 库使用

在您自己的 Node.js 或 Bun 应用程序中将 QMD 作为库使用。

安装

npm install @tobilu/qmd

快速开始

import { createStore } from '@tobilu/qmd'

const store = await createStore({
  dbPath: './my-index.sqlite',
  config: {
    collections: {
      docs: { path: '/path/to/docs', pattern: '**/*.md' },
    },
  },
})

const results = await store.search({ query: "authentication flow" })
console.log(results.map(r => `${r.title} (${Math.round(r.score * 100)}%)`))

await store.close()

创建存储(Store)

createStore() 接受三种模式:

import { createStore } from '@tobilu/qmd'

// 1. 内联配置 — 除了数据库外无需其他文件
const store = await createStore({
  dbPath: './index.sqlite',
  config: {
    collections: {
      docs: { path: '/path/to/docs', pattern: '**/*.md' },
      notes: { path: '/path/to/notes' },
    },
  },
})

// 2. YAML 配置文件 — 在文件中定义集合
const store2 = await createStore({
  dbPath: './index.sqlite',
  configPath: './qmd.yml',
})

// 3. 仅数据库 — 重新打开之前配置过的存储
const store3 = await createStore({ dbPath: './index.sqlite' })

搜索

统一的 search() 方法既可处理简单查询,也可处理预扩展的结构化查询:

// 简单查询 — 由 LLM 自动扩展,然后 BM25 + 向量 + 重排序
const results = await store.search({ query: "authentication flow" })

// 带选项
const results2 = await store.search({
  query: "rate limiting",
  intent: "API throttling and abuse prevention",
  collection: "docs",
  limit: 5,
  minScore: 0.3,
  explain: true,
})

// 预扩展查询 — 跳过自动扩展,控制每个子查询
const results3 = await store.search({
  queries: [
    { type: 'lex', query: '"connection pool" timeout -redis' },
    { type: 'vec', query: 'why do database connections time out under load' },
  ],
  collections: ["docs", "notes"],
})

// 跳过重排序以获得更快的结果
const fast = await store.search({ query: "auth", rerank: false })

直接访问后端:

// BM25 关键词搜索(快速,无需 LLM)
const lexResults = await store.searchLex("auth middleware", { limit: 10 })

// 向量相似度搜索(嵌入模型,无重排序)
const vecResults = await store.searchVector("how users log in", { limit: 10 })

// 手动查询扩展以实现完全控制
const expanded = await store.expandQuery("auth flow", { intent: "user login" })
const results4 = await store.search({ queries: expanded })

检索

// 通过路径或 docid 获取文档
const doc = await store.get("docs/readme.md")
const byId = await store.get("#abc123")

if (!("error" in doc)) {
  console.log(doc.title, doc.displayPath, doc.context)
}

// 获取文档正文并指定行范围
const body = await store.getDocumentBody("docs/readme.md", {
  fromLine: 50,
  maxLines: 100,
})

// 通过 glob 模式或逗号分隔列表批量检索
const { docs, errors } = await store.multiGet("docs/**/*.md", {
  maxBytes: 20480,
})

集合

// 添加集合
await store.addCollection("myapp", {
  path: "/src/myapp",
  pattern: "**/*.ts",
  ignore: ["node_modules/**", "*.test.ts"],
})

// 列出集合及文档统计信息
const collections = await store.listCollections()
// => [{ name, pwd, glob_pattern, doc_count, active_count, last_modified, includeByDefault }]

// 获取默认包含在查询中的集合名称
const defaults = await store.getDefaultCollectionNames()

// 移除/重命名
await store.removeCollection("myapp")
await store.renameCollection("old-name", "new-name")

上下文

上下文添加描述性元数据,可提高搜索相关性,并随结果一起返回:

// 为集合内的路径添加上下文
await store.addContext("docs", "/api", "REST API reference documentation")

// 设置全局上下文(适用于所有集合)
await store.setGlobalContext("Internal engineering documentation")

// 列出所有上下文
const contexts = await store.listContexts()
// => [{ collection, path, context }]

// 移除上下文
await store.removeContext("docs", "/api")
await store.setGlobalContext(undefined)  // 清除全局上下文

索引

// 通过扫描文件系统重新索引集合
const result = await store.update({
  collections: ["docs"],  // 可选 — 默认所有集合
  onProgress: ({ collection, file, current, total }) => {
    console.log(`[${collection}] ${current}/${total} ${file}`)
  },
})
// => { collections, indexed, updated, unchanged, removed, needsEmbedding }

// 生成向量嵌入
const embedResult = await store.embed({
  force: false,           // true 表示重新嵌入所有内容
  chunkStrategy: "auto",  // "regex"(默认)或 "auto"(对代码文件使用 AST)
  onProgress: ({ current, total, collection }) => {
    console.log(`正在嵌入 ${current}/${total}`)
  },
})

类型

为 SDK 使用者导出的关键类型:

import type {
  QMDStore,            // 存储接口
  SearchOptions,       // search() 的选项
  LexSearchOptions,    // searchLex() 的选项
  VectorSearchOptions, // searchVector() 的选项
  HybridQueryResult,   // 搜索结果,包含分数、摘要、上下文
  SearchResult,        // searchLex/searchVector 的结果
  ExpandedQuery,       // 类型化子查询 { type: 'lex'|'vec'|'hyde', query }
  DocumentResult,      // 文档元数据 + 正文
  DocumentNotFound,    // 错误,包含 similarFiles 建议
  MultiGetResult,      // 批量检索结果
  UpdateProgress,      // update() 的回调进度信息
  UpdateResult,        // 聚合的更新结果
  EmbedProgress,       // embed() 的回调进度信息
  EmbedResult,         // 嵌入结果
  StoreOptions,        // createStore() 的选项
  CollectionConfig,    // 内联配置结构
  IndexStatus,         // 来自 getStatus()
  IndexHealthInfo,     // 来自 getIndexHealth()
} from '@tobilu/qmd'

工具导出:

import {
  extractSnippet,              // 从文本中提取相关摘要
  addLineNumbers,              // 为文本添加行号
  DEFAULT_MULTI_GET_MAX_BYTES, // multiGet 的默认最大文件大小(10KB)
  Maintenance,                 // 数据库维护操作
} from '@tobilu/qmd'

生命周期

// 关闭存储 — 释放 LLM 模型和数据库连接
await store.close()

SDK 需要显式指定 dbPath — 不假定任何默认值。这使得它可以安全地嵌入到任何应用程序中而不会产生副作用。

架构

┌─────────────────────────────────────────────────────────────────────────────┐
│                         QMD 混合搜索流程                                     │
└─────────────────────────────────────────────────────────────────────────────┘

                              ┌─────────────────┐
                              │    用户查询     │
                              └────────┬────────┘
                                       │
                        ┌──────────────┴──────────────┐
                        ▼                             ▼
               ┌────────────────┐            ┌────────────────┐
               │   查询扩展     │            │   原始查询     │
               │  (微调模型)  │            │   (×2 权重)  │
               └───────┬────────┘            └───────┬────────┘
                       │                             │
                       │ 2 个替代查询                 │
                       └──────────────┬──────────────┘
                                      │
              ┌───────────────────────┼───────────────────────┐
              ▼                       ▼                       ▼
     ┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
     │   原始查询      │     │  扩展查询 1     │     │  扩展查询 2     │
     └────────┬────────┘     └────────┬────────┘     └────────┬────────┘
              │                       │                       │
      ┌───────┴───────┐       ┌───────┴───────┐       ┌───────┴───────┐
      ▼               ▼       ▼               ▼       ▼               ▼
  ┌───────┐       ┌───────┐ ┌───────┐     ┌───────┐ ┌───────┐     ┌───────┐
  │ BM25  │       │向量   │ │ BM25  │     │向量   │ │ BM25  │     │向量   │
  │(FTS5) │       │搜索   │ │(FTS5) │     │搜索   │ │(FTS5) │     │搜索   │
  └───┬───┘       └───┬───┘ └───┬───┘     └───┬───┘ └───┬───┘     └───┬───┘
      │               │         │             │         │             │
      └───────┬───────┘         └──────┬──────┘         └──────┬──────┘
              │                        │                       │
              └────────────────────────┼───────────────────────┘
                                       │
                                       ▼
                          ┌───────────────────────┐
                          │   RRF 融合 + 奖励分   │
                          │  原始查询:×2        │
                          │  首位奖励:+0.05     │
                          │  保留前 30 名         │
                          └───────────┬───────────┘
                                      │
                                      ▼
                          ┌───────────────────────┐
                          │    LLM 重排序         │
                          │  (qwen3-reranker)     │
                          │  是/否 + logprobs     │
                          └───────────┬───────────┘
                                      │
                                      ▼
                          ┌───────────────────────┐
                          │  位置感知混合         │
                          │  前 1-3 名:  75% RRF │
                          │  前 4-10 名: 60% RRF │
                          │  前 11 名+:  40% RRF │
                          └───────────────────────┘

分数归一化与融合

搜索后端

后端 原始分数 转换方式 范围
FTS (BM25) SQLite FTS5 BM25 Math.abs(score) 0 到 ~25+
向量 余弦距离 1 / (1 + distance) 0.0 到 1.0
重排序器 LLM 0-10 评分 score / 10 0.0 到 1.0

融合策略

query 命令使用倒数排名融合(RRF)与位置感知混合:

  1. 查询扩展:原始查询(加权 ×2)+ 1 个 LLM 变体
  2. 并行检索:每个查询同时搜索 FTS 和向量索引
  3. RRF 融合:使用 score = Σ(1/(k+rank+1)) 组合所有结果列表,其中 k=60
  4. 首位奖励:在任何列表中排名第 1 的文档获得 +0.05,第 2-3 名获得 +0.02
  5. 前 K 名选择:选取前 30 个候选进行重排序
  6. 重排序:LLM 为每个文档评分(是/否,带 logprobs 置信度)
  7. 位置感知混合
    • RRF 排名 1-3:75% 检索分数 + 25% 重排序分数(保留精确匹配)
    • RRF 排名 4-10:60% 检索分数 + 40% 重排序分数
    • RRF 排名 11+:40% 检索分数 + 60% 重排序分数(更信任重排序器)

为什么采用这种方法:当扩展查询与原始查询不匹配时,纯 RRF 可能会稀释精确匹配的结果。首位奖励机制保留了在原始查询中排名第 1 的文档。位置感知混合防止重排序破坏高置信度的检索结果。

分数解读

分数 含义
0.8 - 1.0 高度相关
0.5 - 0.8 中度相关
0.2 - 0.5 部分相关
0.0 - 0.2 低相关性

系统要求

系统要求

  • Node.js >= 22
  • Bun >= 1.0.0
  • macOS:Homebrew SQLite(用于扩展支持)
    brew install sqlite
    

GGUF 模型(通过 node-llama-cpp)

QMD 使用三个本地 GGUF 模型(首次使用时自动下载):

模型 用途 大小
embeddinggemma-300M-Q8_0 向量嵌入(默认) ~300MB
qwen3-reranker-0.6b-q8_0 重排序 ~640MB
qmd-query-expansion-1.7B-q4_k_m 查询扩展(微调) ~1.1GB

模型从 HuggingFace 下载,并缓存在 ~/.cache/qmd/models/ 中。

自定义嵌入模型

通过 QMD_EMBED_MODEL 环境变量覆盖默认的嵌入模型。这对于多语言语料库(例如中文、日文、韩文)特别有用,因为 embeddinggemma-300M 在这些语言上的覆盖范围有限。

# 使用 Qwen3-Embedding-0.6B 以获得更好的多语言(中日韩)支持
export QMD_EMBED_MODEL="hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf"

# 更改模型后,重新嵌入所有集合:
qmd embed -f

支持的模型系列:

  • embeddinggemma(默认)— 针对英语优化,占用空间小
  • Qwen3-Embedding — 多语言(包括中日韩在内的 119 种语言),MTEB 排名前列

注意: 切换嵌入模型时,必须使用 qmd embed -f 重新索引,因为向量在不同模型之间不兼容。每种模型系列的提示格式会自动调整。

安装

npm install -g @tobilu/qmd
# 或
bun install -g @tobilu/qmd

开发

git clone https://github.com/tobi/qmd
cd qmd
npm install
npm link

使用说明

集合管理

# 从当前目录创建集合
qmd collection add . --name myproject

# 使用显式路径和自定义 glob 掩码创建集合
qmd collection add ~/Documents/notes --name notes --mask "**/*.md"

# 列出所有集合
qmd collection list

# 移除集合
qmd collection remove myproject

# 重命名集合
qmd collection rename myproject my-project

# 列出集合中的文件
qmd ls notes
qmd ls notes/subfolder

生成向量嵌入

# 嵌入所有已索引的文档(900 令牌/块,15% 重叠)
qmd embed

# 强制重新嵌入所有内容
qmd embed -f

# 对代码文件启用 AST 感知分块(TS、JS、Python、Go、Rust)
qmd embed --chunk-strategy auto

# 也可与 query 一起使用以保持一致的分块选择
qmd query "auth flow" --chunk-strategy auto

AST 感知分块--chunk-strategy auto)使用 tree-sitter 在函数、类和导入边界处对代码文件进行分块,而不是在任意文本位置进行分块。这可以产生更高质量的块,并为代码库提供更好的搜索结果。Markdown 和其他文件类型无论策略如何都始终使用基于正则表达式的分块。

默认值为 regex(原有行为)。使用 --chunk-strategy auto 选择启用。运行 qmd status 以验证哪些语法库可用。

注意: Tree-sitter 语法库是可选依赖项。如果未安装,--chunk-strategy auto 会自动回退到仅使用正则表达式的分块。已在 Node.js 和 Bun 上测试。

上下文管理

上下文为集合和路径添加描述性元数据,帮助搜索引擎理解您的内容。

# 为集合添加上下文(使用 qmd:// 虚拟路径)
qmd context add qmd://notes "个人笔记和想法"
qmd context add qmd://docs/api "API 文档"

# 从集合目录内添加上下文
cd ~/notes && qmd context add "个人笔记和想法"
cd ~/notes/work && qmd context add "工作相关笔记"

# 添加全局上下文(适用于所有集合)
qmd context add / "我的项目知识库"

# 列出所有上下文
qmd context list

# 移除上下文
qmd context rm qmd://notes/old

搜索命令

┌──────────────────────────────────────────────────────────────────┐
│                        搜索模式                                   │
├──────────┬───────────────────────────────────────────────────────┤
│ search   │ 仅 BM25 全文检索                                      │
│ vsearch  │ 仅向量语义搜索                                        │
│ query    │ 混合:FTS + 向量 + 查询扩展 + 重排序                   │
└──────────┴───────────────────────────────────────────────────────┘
# 全文搜索(快速,基于关键词)
qmd search "authentication flow"

# 向量搜索(语义相似度)
qmd vsearch "how to login"

# 带重排序的混合搜索(最佳质量)
qmd query "user authentication"

选项

# 搜索选项
-n <num>           # 结果数量(默认:5,对于 --files/--json 则为 20)
-c, --collection   # 将搜索限制在特定集合
--all              # 返回所有匹配结果(与 --min-score 一起使用以过滤)
--min-score <num>  # 最低分数阈值(默认:0)
--full             # 显示完整文档内容
--line-numbers     # 为输出添加行号
--explain          # 包含检索分数追踪(query、JSON/CLI 输出)
--index <name>     # 使用指定名称的索引

# 输出格式(用于 search 和 multi-get)
--files            # 输出:docid,score,filepath,context
--json             # JSON 输出,包含摘要
--csv              # CSV 输出
--md               # Markdown 输出
--xml              # XML 输出

# get 选项
qmd get <file>[:line]  # 获取文档,可选择从某行开始
-l <num>               # 返回的最大行数
--from <num>           # 从指定行号开始

# multi-get 选项
-l <num>           # 每个文件的最大行数
--max-bytes <num>  # 跳过大于 N 字节的文件(默认:10KB)

输出格式

默认输出为彩色 CLI 格式(遵循 NO_COLOR 环境变量)。

当 stdout 是 TTY 时,结果路径会作为可点击的终端超链接(OSC 8)发出。点击路径将使用编辑器 URI 模板在您的编辑器中打开文件。

当 stdout 不是 TTY 时(例如通过管道传给另一个命令或重定向到文件),QMD 会输出纯文本路径,不带转义序列。

TTY 示例:

docs/guide.md:42 #a1b2c3
标题:软件工匠精神
上下文:工作文档
分数:93%

本节涵盖了构建高质量软件时需关注的**工匠精神**。
另见:工程原则


notes/meeting.md:15 #d4e5f6
标题:Q4 规划
上下文:个人笔记和想法
分数:67%

讨论开发过程中的代码质量和工匠精神。

使用 QMD_EDITOR_URI(或配置文件中的 editor_uri)配置编辑器链接目标:

# VS Code(默认)
export QMD_EDITOR_URI="vscode://file/{path}:{line}:{col}"

# Cursor
export QMD_EDITOR_URI="cursor://file/{path}:{line}:{col}"

# Zed
export QMD_EDITOR_URI="zed://file/{path}:{line}:{col}"

# Sublime Text
export QMD_EDITOR_URI="subl://open?url=file://{path}&line={line}"

模板占位符:

  • {path} 绝对文件系统路径(URI 编码)
  • {line} 基于 1 的行号
  • {col}{column} 基于 1 的列号

  • 路径:相对于集合的路径(例如 docs/guide.md
  • Docid:短哈希标识符(例如 #a1b2c3)— 使用 qmd get #a1b2c3
  • 标题:从文档中提取(第一个标题或文件名)
  • 上下文:通过 qmd context add 配置的路径上下文
  • 分数:颜色编码(绿色 >70%,黄色 >40%,否则为暗色)
  • 摘要:匹配项周围的上下文,查询词高亮显示

示例

# 获取 10 个结果,最低分数 0.3
qmd query -n 10 --min-score 0.3 "API design patterns"

# 以 markdown 格式输出,供 LLM 上下文使用
qmd search --md --full "error handling"

# 用于脚本的 JSON 输出
qmd query --json "quarterly reports"

# 检查每个结果的得分方式(RRF + 重排序混合)
qmd query --json --explain "quarterly reports"

# 为不同的知识库使用独立的索引
qmd --index work search "quarterly reports"

索引维护

# 显示索引状态以及带上下文的集合
qmd status

# 重新索引所有集合
qmd update

# 先执行 git pull 再重新索引(适用于远程仓库)
qmd update --pull

# 通过文件路径获取文档(支持模糊匹配建议)
qmd get notes/meeting.md

# 通过 docid 获取文档(来自搜索结果)
qmd get "#abc123"

# 从第 50 行开始获取文档,最多 100 行
qmd get notes/meeting.md:50 -l 100

# 通过 glob 模式批量获取多个文档
qmd multi-get "journals/2025-05*.md"

# 通过逗号分隔列表批量获取多个文档(支持 docid)
qmd multi-get "doc1.md, doc2.md, #abc123"

# 将 multi-get 限制在 20KB 以下的文件
qmd multi-get "docs/*.md" --max-bytes 20480

# 以 JSON 格式输出 multi-get 结果,供智能体处理
qmd multi-get "docs/*.md" --json

# 清理缓存和孤立数据
qmd cleanup

数据存储

索引存储位置:~/.cache/qmd/index.sqlite

模式

collections     -- 已索引的目录,包含名称和 glob 模式
path_contexts   -- 按虚拟路径(qmd://...)的上下文描述
documents       -- Markdown 内容,包含元数据和 docid(6 字符哈希)
documents_fts   -- FTS5 全文索引
content_vectors -- 嵌入块(hash, seq, pos,每块 900 令牌)
vectors_vec     -- sqlite-vec 向量索引(hash_seq 键)
llm_cache       -- 缓存的 LLM 响应(查询扩展、重排序分数)

环境变量

变量 默认值 描述
XDG_CACHE_HOME ~/.cache 缓存目录位置

工作原理

索引流程

集合 ──► Glob 模式 ──► Markdown 文件 ──► 解析标题 ──► 哈希内容
    │                                                   │              │
    │                                                   │              ▼
    │                                                   │         生成 docid
    │                                                   │         (6 字符哈希)
    │                                                   │              │
    └──────────────────────────────────────────────────►└──► 存入 SQLite
                                                                       │
                                                                       ▼
                                                                  FTS5 索引

嵌入流程

文档被分成约 900 令牌的块,块之间有 15% 的重叠,并使用智能边界检测:

文档 ──► 智能分块(~900 令牌)──► 格式化每个块 ──► node-llama-cpp ──► 存储向量
                │                          "标题 | 文本"        embedBatch()
                │
                └─► 存储的块包含:
                    - hash: 文档哈希
                    - seq: 块序号(0, 1, 2...)
                    - pos: 在原文档中的字符位置

智能分块

QMD 不使用硬性的令牌边界切分,而是使用评分算法来寻找自然的 Markdown 断点。这样可以保持语义单元(章节、段落、代码块)的完整性。

断点评分:

模式 分数 描述
# Heading 100 H1 - 主要章节
## Heading 90 H2 - 子章节
### Heading 80 H3
#### Heading 70 H4
##### Heading 60 H5
###### Heading 50 H6
` ``` ` 80 代码块边界
--- / *** 60 水平线
空行 20 段落边界
- item / 1. item 5 列表项
换行 1 最小断点

算法:

  1. 扫描文档中所有带分数的断点
  2. 当接近 900 令牌目标时,在截止点之前的 200 令牌窗口内搜索
  3. 计算每个断点的最终分数:finalScore = baseScore × (1 - (distance/window)² × 0.7)
  4. 在得分最高的断点处切分

平方距离衰减意味着在 200 令牌前的标题(分数约 30)仍然优于目标位置的简单换行(分数 1),但更近的标题会胜过远处的标题。

代码块保护: 代码块内部的断点会被忽略——代码保持在一起。如果代码块超过块大小,则尽可能保持完整。

AST 感知分块(代码文件):

对于受支持的代码文件,QMD 还会使用 tree-sitter 解析源代码,并添加从 AST 派生的断点,这些断点与上述正则表达式分数合并:

AST 节点 分数 语言
类 / 接口 / 结构体 / impl / trait 100 所有
函数 / 方法 90 所有
类型别名 / 枚举 80 所有
导入 / 使用声明 60 所有

支持 .ts.tsx.js.jsx.py.go.rs 文件。使用 --chunk-strategy auto 启用。Markdown 和其他文件类型始终使用基于正则表达式的分块。

查询流程(混合)

查询 ──► LLM 扩展 ──► [原始, 变体 1, 变体 2]
                │
      ┌─────────┴─────────┐
      ▼                   ▼
   对于每个查询:       FTS (BM25)
      │                   │
      ▼                   ▼
   向量搜索            排名列表
      │
      ▼
   排名列表
      │
      └─────────┬─────────┘
                ▼
         RRF 融合(k=60)
         原始查询 ×2 权重
         首位奖励:第 1 名 +0.05,第 2-3 名 +0.02
                │
                ▼
         前 30 个候选
                │
                ▼
         LLM 重排序
         (是/否 + logprob 置信度)
                │
                ▼
         位置感知混合
         排名 1-3:  75% RRF / 25% 重排序
         排名 4-10: 60% RRF / 40% 重排序
         排名 11+:  40% RRF / 60% 重排序
                │
                ▼
         最终结果

模型配置

模型在 src/llm.ts 中配置为 HuggingFace URI:

const DEFAULT_EMBED_MODEL = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
const DEFAULT_RERANK_MODEL = "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf";
const DEFAULT_GENERATE_MODEL = "hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf";

EmbeddingGemma 提示格式

// 查询
"task: search result | query: {query}"

// 文档
"title: {title} | text: {content}"

Qwen3-Reranker

使用 node-llama-cpp 的 createRankingContext()rankAndSort() API 进行交叉编码器重排序。返回按相关性分数(0.0 - 1.0)排序的文档。

Qwen3(查询扩展)

用于通过 LlamaChatSession 生成查询变体。

许可证

MIT

参考资料