Files
QMDSearch/.trae/specs/add-hierarchical-rag-pipeline/spec.md
T
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

145 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 分层摘要索引 + 分类路由检索全链路 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.py](file:///Users/kplam/coding/QMDSearch/app/core/summarizer.py)、[ingestion.py](file:///Users/kplam/coding/QMDSearch/app/core/ingestion.py)、[document.py](file:///Users/kplam/coding/QMDSearch/app/models/document.py)、[config.py](file:///Users/kplam/coding/QMDSearch/app/config.py)、[main.py](file:///Users/kplam/coding/QMDSearch/app/main.py)
- 新增:`app/core/embeddings.py``app/core/sparse.py``app/core/chunker.py``app/core/classifier.py``app/core/retriever.py``app/core/ranker.py``app/core/query_parser.py``app/services/qdrant.py``app/services/redis.py``app/models/search.py``app/models/knowledge.py``app/api/v1/document.py``app/api/v1/search.py``app/api/v1/knowledge.py``scripts/eval/`(评测脚本与回归集样例)
- 基础设施:Qdrant 集合初始化(4 集合 + payload 索引 + sparse 配置),无需新增容器
## ADDED Requirements
### Requirement: taxonomy 分类判定
系统 SHALL 提供可配置的类目体系(taxonomy),入库时基于 L1 总结对文档分类,输出主类、多标签与置信度;置信度低于阈值时归入 `uncategorized` 兜底类。taxonomy 通过配置文件定义,支持环境变量指定路径,未配置时使用内置默认类目集。
#### Scenario: 正常分类
- **WHEN** 入库一篇内容明确的文档
- **THEN** 返回 `main_category`(主类)、`tags`0~3 个附加标签)、`confidence`0~1),且主类属于 taxonomy 已定义类目
#### Scenario: 低置信兜底
- **WHEN** 文档内容跨类目或无法明确归类(分类置信度低于阈值)
- **THEN** `main_category``uncategorized`,多标签保留候选类目,检索时该文档仅在全库兜底通道与标签软召回中可见
### 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_id``category``tags``section_path`chunk/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_id``title``section_path``score``doc_summary`L1,仅上下文标注)
### 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 索引过滤替代(见存储设计说明)。