Files
kplam 51dc8dc4f6 Initial commit: QMDSearch 分层信息检索服务
- FastAPI + Qdrant + Redis + Ollama 技术栈
- L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合)
- 文档三级总结与 2.5 级回退
- query 解析路由与分类
- /admin 管理页面
2026-07-29 21:24:40 +08:00

10 KiB
Raw Permalink Blame History

分层摘要索引 + 分类路由检索全链路 Spec

Why

当前 QMDSearch 仅实现了三级总结器(summarizer),入库链路的分类/向量化/写入为 TODO,检索侧整体缺失,无法兑现 CLAUDE.md 中「面向 AI Agent 的分层信息检索服务」定位。本变更按「分层预摘要索引(Hierarchical Summarization Index+ 分类路由 + 自顶向下剪枝检索」架构补齐全链路:离线侧把 Agentic RAG 中 router/planner 需要的结构信息预先算好,在线侧用「分类过滤 + 摘要树逐层剪枝 + hybrid 检索」实现大规模知识库下又省又准的检索,并附评测脚本骨架守护摘要质量与剪枝召回。

What Changes

  • 入库链路补全:taxonomy 驱动的分类判定(主类 + 多标签 + 置信度,uncategorized 兜底)、dense + sparseBM25)双向量生成、Qdrant 四层集合写入(L1 总结 / L2 大纲节点 / L3 内容大纲节点 / 原文 chunk)
  • summarizer 改造:L2 大纲优先解析文档原生标题树(Markdown 标题/编号标题),无结构文本回退 LLM 生成;新增 chunk 切分器,chunk 关联所属 section 路径
  • 分层检索引擎(新增)query 解析(Ollama 小模型 JSON 约束输出:意图分类 + query rewrite + 关键词)→ 分类路由(主类硬过滤 + 多标签软召回 + 低置信全库兜底)→ L1→L2→L3 摘要树逐层剪枝 → chunk 层 dense+sparse hybrid 召回(RRF 融合)→ 重排返回原文 chunk
  • 检索边界:摘要仅用于路由与上下文标注,返回给调用方 Agent 的答案语料只含原文 chunk(防摘要幻觉进入生成)
  • 降级路径:分类低置信跳过路由;任一层召回为空回退上一层范围直搜 chunk;2.5 级文档 L1 命中后直进 L3/chunk 层
  • API(新增)POST /api/v1/documents 入库、POST /api/v1/search 分层检索、GET /api/v1/knowledge/categories 类目查询
  • Redis 缓存:query 解析结果与检索结果缓存(短 TTL)
  • 评测脚本骨架(新增):离线回归集 + Entity Recall / Hallucination Rate / Routing F1 / Pruning Loss / Precision@5 指标,支持与平铺 chunk baseline 对比

存储设计说明:不按类目建集合,采用全局四层集合 + category payload 索引过滤(Qdrant keyword payload index)。理由:支持多标签软召回与 uncategorized 兜底跨类检索,taxonomy 调整无需迁库;此设计取代 CLAUDE.md 中「按分类映射写入对应集合」的设想(该设想从未实现,非破坏性变更)。

命名对齐:用户方案中 L0/L1/L2 对应代码库既有命名 L1 总结 / L2 大纲 / L3 内容大纲(+ L2.5 回退),本 spec 沿用代码库命名。

Impact

  • Affected specs: 文档入库(ingestion)、文档三级总结(summarizer)、分层检索(新增)、评测(新增)
  • Affected code:
    • 修改:summarizer.pyingestion.pydocument.pyconfig.pymain.py
    • 新增:app/core/embeddings.pyapp/core/sparse.pyapp/core/chunker.pyapp/core/classifier.pyapp/core/retriever.pyapp/core/ranker.pyapp/core/query_parser.pyapp/services/qdrant.pyapp/services/redis.pyapp/models/search.pyapp/models/knowledge.pyapp/api/v1/document.pyapp/api/v1/search.pyapp/api/v1/knowledge.pyscripts/eval/(评测脚本与回归集样例)
  • 基础设施:Qdrant 集合初始化(4 集合 + payload 索引 + sparse 配置),无需新增容器

ADDED Requirements

Requirement: taxonomy 分类判定

系统 SHALL 提供可配置的类目体系(taxonomy),入库时基于 L1 总结对文档分类,输出主类、多标签与置信度;置信度低于阈值时归入 uncategorized 兜底类。taxonomy 通过配置文件定义,支持环境变量指定路径,未配置时使用内置默认类目集。

Scenario: 正常分类

  • WHEN 入库一篇内容明确的文档
  • THEN 返回 main_category(主类)、tags03 个附加标签)、confidence01),且主类属于 taxonomy 已定义类目

Scenario: 低置信兜底

  • WHEN 文档内容跨类目或无法明确归类(分类置信度低于阈值)
  • THEN main_categoryuncategorized,多标签保留候选类目,检索时该文档仅在全库兜底通道与标签软召回中可见

Requirement: 双向量索引(dense + sparse

系统 SHALL 为 L1 总结、L2 大纲节点、L3 内容大纲节点、原文 chunk 生成 dense 向量;并至少为原文 chunk 与 L1 总结生成 sparseBM25)向量。dense 向量提供方可配置(openai / local),sparse 向量使用本地分词 + 特征哈希 BM25 实现,不依赖外部模型下载。

Scenario: 四层集合写入

  • WHEN 文档完成总结与分类
  • THEN L1/L2/L3 各层节点与原文 chunk 分别写入对应集合,每条数据携带 doc_idcategorytagssection_pathchunk/L2/L3 层)payloadchunk 同时携带 dense 与 sparse 向量

Requirement: chunk 切分与 section 关联

系统 SHALL 将原文按结构(标题树)优先、长度兜底切分为 chunk,每个 chunk 记录所属 section 路径,与 L2/L3 节点可互相定位。

Scenario: 结构化文档切分

  • WHEN 文档含标题结构
  • THEN chunk 按 section 边界切分,section_path 与 L2 大纲节点对应;超长 section 内部按长度二次切分

Requirement: query 解析与分类路由

系统 SHALL 在检索前用 Ollama 小模型以 JSON 约束输出解析 query:意图命中的类目集合 + 置信度、rewrite 后 query、关键词。解析失败或分类置信度低时 SHALL 跳过路由进入全库检索。

Scenario: 明确意图路由

  • WHEN query 意图明确命中 1~2 个类目且置信度达标
  • THEN 后续检索仅在命中类目 payload 过滤范围内进行

Scenario: 跨类/模糊兜底

  • WHEN 分类置信度低于阈值或命中类目数超过上限
  • THEN 放弃类目过滤,全库检索(不丢召回)

Requirement: 自顶向下摘要树剪枝检索

系统 SHALL 按 L1→L2→L3→chunk 顺序逐层缩小检索范围:L1 层检索落候选文档(top-N),候选文档内检索 L2 落候选 section,候选 section 内检索 L3 定位 chunk 范围,最终在范围内 hybrid 检索 chunk。2.5 级文档无 L2 层,L1 命中后 SHALL 直进 L3/chunk 层。任一层召回为空时 SHALL 回退到上一层范围直接检索 chunk。

Scenario: 三级文档逐层命中

  • WHEN query 在路由范围内有匹配文档
  • THEN 检索沿 摘要树逐层收敛,最终 chunk 候选仅来自 L3 命中的 section 范围

Scenario: 剪枝为空回退

  • WHEN L2 或 L3 层在候选范围内召回为空
  • THEN 回退到上一层候选文档范围直接 hybrid 检索 chunk,不返回空结果

Requirement: hybrid 召回与重排

chunk 层 SHALL 同时执行 dense 与 sparse 检索,使用 RRF 融合排序,按 retrieval_top_k 召回、retrieval_final_k 返回。

Scenario: RRF 融合

  • WHEN chunk 层执行检索
  • THEN dense 与 sparse 两路结果经 RRF 融合后排序,返回 final_k 个原文 chunk

Requirement: 检索结果只含原文

检索 API SHALL 仅返回原文 chunk 及引用元数据(doc_id、标题、section_path、score、所属文档 L1 总结作为上下文标注),摘要不作为可答题语料返回字段的主体。

Scenario: 结果结构

  • WHEN 检索成功
  • THEN 每个 hit 含 text(原文)、doc_idtitlesection_pathscoredoc_summaryL1,仅上下文标注)

Requirement: 检索缓存

系统 SHALL 使用 Redis 缓存 query 解析结果与检索结果(短 TTL,可配置),缓存键包含 query 文本与路由类目;缓存不可用时不影响主流程。

Requirement: 入库与检索 API

系统 SHALL 提供 POST /api/v1/documents(入库,返回 document_id/分类/总结层级)、POST /api/v1/search(分层检索)、GET /api/v1/knowledge/categories(taxonomy 类目列表),统一响应格式 {"code": 0, "data": {...}, "message": "ok"},错误码 0=成功、1xxx=客户端错误、2xxx=服务端错误。

Requirement: 评测脚本骨架

系统 SHALL 提供离线评测脚本:回归集格式(文档 + golden query 含应检出/不应检出 + golden doc/section/chunk 标注),实现 Entity Recall、Hallucination RateLLM-as-judge 反查)、Routing F1、Pruning Loss、Precision@5 / Recall@10 指标,并支持同一 query 集对「平铺 chunk baseline」与本方案做 A/B 对比输出报告。

Scenario: 摘要质量门禁

  • WHEN 对回归集运行评测脚本
  • THEN 输出各指标:L1 Entity Recall ≥ 0.85、L3 ≥ 0.9、Hallucination Rate < 2%、Pruning Loss < 8% 作为参考门槛,不达标项在报告中显式标红

MODIFIED Requirements

Requirement: L2 大纲生成(原 LLM 自由生成)

L2 大纲 SHALL 优先解析文档原生标题树(Markdown #/##/### 及常见编号标题模式),直接以标题层级作为大纲节点;仅当文档无可解析结构时回退为 LLM 生成大纲。L2 限定为「结构导航」,不写入结论性压缩语句,避免与 L1 总结语义重叠。原文无标题结构且为短文本时仍走既有 2.5 级回退。

理由:用户方案评审结论——LLM 自由生成的三级粒度易出现层级语义重叠、检索两头命中同一段;原生标题树更稳且零成本。

Requirement: 分类判定(原 stub 返回 "default")

Ingester._classify 占位实现 SHALL 替换为独立 classifier 模块:基于 L1 总结调用 Ollama 小模型 JSON 约束输出主类 + 多标签 + 置信度,对齐 taxonomy;不再硬编码返回 default

REMOVED Requirements

Requirement: 按分类映射写入对应集合(CLAUDE.md 设想)

Reason:固定 taxonomy 的按类目分集合会使新业务线/跨类目问题被路由砍死,且多标签软召回与 uncategorized 兜底无法跨集合实现;taxonomy 变更需迁库。 Migration:从未实现,无需迁移;以全局四层集合 + category payload 索引过滤替代(见存储设计说明)。