在 AI 应用系统中,检索质量直接决定了上层智能体验的下限。传统关键词搜索无法捕捉语义关联,向量检索又容易在大规模文档集上召回过多相似但不相关的结果。qmd 作为一款本地运行的 CLI 文档搜索引擎,通过巧妙的混合搜索架构,在离线和轻量化约束下实现了高质量检索。本文将深入剖析其核心设计,为构建本地 AI 搜索引警提供可落地的工程参考。
本地混合搜索的核心需求
个人知识库、会议记录、技术文档等场景的搜索引警面临独特挑战:数据规模通常在数百到数千个文档级别,不需要分布式架构;但对检索质量要求极高 —— 用户期望用自然语言提问就能获得精准结果。qmd 正是瞄准这一细分场景,全部计算在本地完成,通过 Node.js/Bun 运行时调用 GGUF 格式的轻量化模型,整个系统无需云端 API。
从技术栈来看,qmd 依赖 SQLite FTS5 实现 BM25 全文检索,使用 sqlite-vec 向量索引做语义匹配,并通过 node-llama-cpp 加载本地大模型完成查询扩展和重排序。这种架构避免了外部依赖,数据始终留在本地设备,满足隐私敏感场景的需求。系统要求 Node.js 22 及以上或 Bun 1.0 以上,macOS 环境需要通过 Homebrew 安装 SQLite 以支持扩展功能。
混合搜索 pipeline 的工程实现
qmd 的检索流程并非简单的向量 + 关键词叠加,而是精心设计的五阶段 pipeline:查询扩展、并行检索、融合排序、重排序和位置感知混合。每个阶段都有明确的工程参数,理解这些参数是优化检索效果的关键。
查询扩展阶段使用一个 1.7GB 的微调模型 qmd-query-expansion-1.7B,根据原始查询生成两个变体。原始查询权重设为 ×2,两个变体各权重 1.0。这意味着系统既保留了用户意图的精确表达,又通过 LLM 生成的多样化表述扩展召回边界。查询扩展的触发由 query 命令自动执行,而 search 和 vsearch 分别仅使用 BM25 和向量检索,不经过扩展阶段。
并行检索阶段中,每个查询(原始 + 2 个变体)同时访问 FTS5 和向量索引两个后端。BM25 的原始分数范围大约在 0 到 25 以上,需要通过取绝对值转换到标准区间;向量搜索返回的是余弦距离,通过 1 / (1 + distance) 归一化到 0 到 1。这种分数标准化是后续融合的前提,避免了不同后端量纲不一致的问题。
RRF 融合采用 Reciprocal Rank Fusion 算法,公式为 score = Σ(1/(k+rank+1)),其中 k=60 是经验调优值。k 值越大,不同排名列表的融合结果越平滑;k 越小,则越偏向保留各列表的头部结果。qmd 还引入了排名奖励机制:任何列表中排名第一的文档额外加 0.05 分,排名第二到第三的加 0.02 分。这解决了纯 RRF 可能稀释精确匹配的问题 —— 当扩展查询没有命中用户真正想要的内容时,原始查询的精确匹配仍能脱颖而出。
融合后取 Top 30 候选送入重排序阶段,这里使用 640MB 的 qwen3-reranker-0.6b 模型。模型以交叉编码方式对每个候选文档给出 0-10 的相关性评分,再除以 10 归一化。重排序的输出不仅有分数,还有 logprobs 置信度,为后续的混合策略提供依据。
位置感知混合是 qmd 最独特的设计:排名前三的结果采用 75% 检索分数 + 25% 重排分数的权重,排名四到十采用 60% + 40%,排名十一之后采用 40% + 60%。这背后的直觉是:检索阶段的排名本身就蕴含了词频、逆文档频率等统计信号,对于高度相关的文档,这些信号已经足够可靠,重排序的调整应该保守;而对于排名较低的候选,重排序的语义理解能力更值得信赖。
关键工程参数配置清单
要在生产环境中有效使用 qmd,需要关注以下可配置参数:
搜索命令选择直接影响检索路径。qmd search 仅做 BM25 检索,延迟最低,适合需要快速过滤的场景;qmd vsearch 仅做向量语义检索,适合查询与文档语义相近但词汇不同的场景;qmd query 是完整 pipeline,延迟最高但质量最好。工程实践中可以根据查询类型选择 —— 精确术语搜索用 search,概念性提问用 query。
分块策略影响向量检索的粒度。默认的 regex 模式根据 markdown 标题、代码块、水平分割线等边界切分文档,目标是每个 chunk 约 900 个 token,块之间有 15% 的重叠以保持上下文连续。对于代码文件,可以启用 --chunk-strategy auto 打开 AST 感知分块,使用 tree-sitter 解析源代码后在函数、类、导入声明等语法节点处切分,这对代码搜索效果提升显著。
模型选择决定了语义理解能力的上限。默认的 embeddinggemma-300M 模型针对英文优化,对于中文、日文、韩文等多语言内容效果有限。可以通过设置环境变量 QMD_EMBED_MODEL 切换到 Qwen3-Embedding-0.6B,该模型支持 119 种语言,包括 CJK。切换后必须用 qmd embed -f 强制重新生成所有向量索引,因为不同模型的向量空间不兼容。
输出格式方面,--json 适合程序处理,--files 适合批量导出给 Agent,--explain 会输出每个结果的详细评分轨迹,便于调试检索效果。分数阈值通过 --min-score 设置,建议在 0.3 到 0.5 之间过滤低相关度结果。
MCP 集成为 AI Agent 提供了结构化接入能力。qmd 暴露三个核心工具:query 支持子查询类型指定(lex/vec/hyde),get 按路径或 docid 获取文档,multi_get 支持 glob 模式批量检索。HTTP 传输模式下,通过 qmd mcp --http --daemon 启动常驻进程,模型在显存中保持加载状态,请求间复用仅在空闲 5 分钟后卸载上下文。
索引维护与性能考量
qmd 的索引存储在 ~/.cache/qmd/index.sqlite,包含 collections、path_contexts、documents、documents_fts、content_vectors、vectors_vec 和 llm_cache 七张表。索引更新通过 qmd update 触发增量扫描,仅处理自上次扫描后修改的文件。对于频繁更新的文档,建议定期执行 qmd cleanup 清理孤立数据。
首次运行时,系统会自动从 HuggingFace 下载三个模型,总计约 2GB,缓存在 ~/.cache/qmd/models/ 目录。如果网络环境受限,可以手动下载 GGUF 文件放到对应目录。模型下载完成后,嵌入阶段是计算最密集的步骤,900 token 每块的生成速度取决于本地硬件,通常在每秒数十到数百个块不等。
对于大规模知识库(超过一万个文档),建议关注几个性能瓶颈:向量索引的构建速度与文档数量线性相关;重排序阶段需要逐个调用 LLM 推理,延迟随候选数量增长;AST 分块依赖 tree-sitter 解析,语法树构建本身有开销。实际部署中可以通过限制 query 命令的返回数量(-n 参数)来控制重排序开销,用 --min-score 快速过滤低相关度候选避免无谓计算。
面向 Agent 的设计考量
qmd 从一开始就为 Agent 工作流做了优化设计。除了 MCP 协议集成外,其输出格式针对 LLM 消费做了专门处理:JSON 输出包含相关文档的标题、路径、上下文描述和匹配片段,可以直接作为上下文注入;--files 模式输出 docid、分数、文件路径和上下文,适合批量检索场景;--all 模式配合 --min-score 可以获取所有超过阈值的匹配,避免截断导致的遗漏。
Context 机制是另一个面向 Agent 的设计:可以为集合或路径添加描述性元数据,这些元数据会在检索时返回,帮助 Agent 理解文档的整体用途。例如,为 qmd://notes 添加 “个人笔记和创意想法” 的上下文,为 qmd://meetings 添加 “会议记录和决策” 的上下文。当检索结果返回时,Agent 不仅看到匹配的具体文档,还能获得更高层级的领域信息,这对于多文档推理场景尤为重要。
总结
qmd 展示了一种在本地设备上构建高质量搜索引警的可行路径:通过 BM25 和向量的混合检索、LLM 驱动的查询扩展与重排序、以及位置感知的分数混合,在不依赖云端 API 的前提下实现了接近商业级搜索质量。其工程价值在于全链路可控、数据隐私有保障、且对硬件资源要求相对温和。关键的成功因素包括:合理选择搜索命令适应不同延迟需求、针对代码场景启用 AST 分块、根据语言特性选择嵌入模型、以及通过 MCP 协议无缝集成到 AI Agent 生态中。
资料来源:qmd GitHub 仓库 (https://github.com/tobi/qmd)