QMD - 查询标记文档(Query Markup Documents)
一款用于记忆一切所需信息的设备端搜索引擎。为您的 Markdown 笔记、会议记录、技术文档和知识库建立索引。
支持关键词或自然语言搜索。非常适合您的智能体流程。
QMD 结合了 BM25 全文检索、向量语义检索以及 LLM 重排序——所有这些均通过 node-llama-cpp 与 GGUF 模型在本地运行。

您可以在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)与位置感知混合:
- 查询扩展:原始查询(加权 ×2)+ 1 个 LLM 变体
- 并行检索:每个查询同时搜索 FTS 和向量索引
- RRF 融合:使用
score = Σ(1/(k+rank+1))组合所有结果列表,其中 k=60 - 首位奖励:在任何列表中排名第 1 的文档获得 +0.05,第 2-3 名获得 +0.02
- 前 K 名选择:选取前 30 个候选进行重排序
- 重排序:LLM 为每个文档评分(是/否,带 logprobs 置信度)
- 位置感知混合:
- 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 | 最小断点 |
算法:
- 扫描文档中所有带分数的断点
- 当接近 900 令牌目标时,在截止点之前的 200 令牌窗口内搜索
- 计算每个断点的最终分数:
finalScore = baseScore × (1 - (distance/window)² × 0.7) - 在得分最高的断点处切分
平方距离衰减意味着在 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
