# 知识库检索架构(L0 + L1) **更新日期**: 2026-06-25 --- ## 一、概述 `LookupKnowledgeTool` 实现两阶段混合检索: - **L0 精确匹配**:基于内存索引的关键词匹配(< 10ms),索引从数据库加载 - **L1 语义检索**:基于 Milvus 向量数据库的相似度搜索(200-500ms) --- ## 二、完整流程 ``` 用户查询 │ ▼ ┌─────────────────────────────┐ │ L0: 关键词精确匹配 │ (< 10ms) │ • 从内存索引做关键词匹配 │ │ • 索引来源: ApiDocument DB│ └──────────┬──────────────────┘ │ ▼ ┌──────┴──────┐ │ matches=1 │ ← 唯一匹配(高置信度) └──────┬──────┘ │ ▼ ┌──────────────┐ ┌──────────────────┐ │ 跳过 L1 │ │ L0 返回正文摘要 │ │ 置信度: high │ │ buildCompactSummary│ └──────────────┘ └──────────────────┘ ┌──────┴──────┐ │ matches=0 │ ← 无匹配 └──────┬──────┘ │ ▼ ┌──────────────┐ ┌──────────────────┐ │ 触发 L1 │ │ L0 无结果 │ │ L1 语义检索 │ │ 仅有 L1 补充结果 │ └──────────────┘ └──────────────────┘ ┌──────┴──────┐ │ matches>=2 │ ← 多匹配 └──────┬──────┘ │ ▼ ┌──────────────┐ │ 触发 L1 │ │ L1 语义检索 │ └──────┬──────┘ │ ┌─────┴─────┐ │ ║ │ ▼ ▼ L1 有结果 L1 无结果 │ │ ▼ ▼ 元数据摘要 正文摘要 (不读文件) (读文件) ``` --- ## 三、L0 返回内容策略 根据匹配场景决定 L0 返回给 LLM 的上下文内容量。 ### 3.1 唯一匹配(高置信度,matches=1) **策略**: `buildCompactSummary()` L1 被跳过,LLM 只有 L0 信息来源,需要提供足够的正文内容。 ``` 文档: 支付网关错误码定义 摘要: 记录了支付网关所有核心错误码的含义及排查方向 章节: - 超时类错误 - 业务类错误 - 签名类错误 --- **含义**:支付网关请求超时 **常见原因**:网络延迟、第三方服务响应慢 ... ``` | 组成部分 | 说明 | 大小 | |---------|------|------| | title + summary | 从内存索引获取 | ~50-100 字符 | | 章节标题列表 | 从文件解析 `##` 标题 | ~50-200 字符 | | 正文片段 | 去 frontmatter/标题行/空行,短文档 800/长文档 500 字符截断 | ~300-800 字符 | | **总计** | | **~400-1000 字符** | ### 3.2 多匹配 + L1 有结果 **策略**: `buildMetadataOnlySummary()` L1 已有语义内容片段,L0 仅需告知 LLM 命中了哪些文档。**不读文件**,仅用内存索引。 ``` 文档: 支付网关错误码定义 摘要: 记录了支付网关所有核心错误码的含义及排查方向 关键词: ERR_TIMEOUT, 超时, 支付网关 来源: api/payment-errors.md ``` | 组成部分 | 说明 | 大小 | |---------|------|------| | title + summary + keywords | 全部从内存索引获取 | ~100-200 字符 | | **总计** | | **~100-200 字符** | ### 3.3 多匹配 + L1 无结果 **策略**: `buildCompactSummary()`(同 3.1) L1 未返回结果, L0 作为兜底提供正文内容。 --- ## 四、决策矩阵 ``` needFullContent = highConfidence || !hasL1 ``` | 场景 | matches | L1 结果 | needFullContent | L0 策略 | 是否读文件 | 上下文大小 | |------|:-------:|:--------:|:---------------:|---------|:---------:|:--------:| | 唯一匹配 | 1 | 未执行 | true | `buildCompactSummary` | 是 | ~600 字符 | | 多匹配 + L1 有结果 | 2+ | 有 | false | `buildMetadataOnlySummary` | **否** | ~150 字符 | | 多匹配 + L1 无结果 | 2+ | 无 | true | `buildCompactSummary` | 是 | ~600 字符 | | 无匹配 | 0 | 有 | — | 无 L0,仅 L1 | 否 | 0 | --- ## 五、代码结构 ``` LookupKnowledgeTool ├── lookupKnowledge(query) # 入口:编排 L0 + L1 ├── buildResult(l0, l1, confidence) # 组装结果,选择摘要策略 ├── buildCompactSummary(entry) # 元数据 + 章节 + 正文片段(读文件) ├── buildMetadataOnlySummary(entry) # 仅元数据(不读文件) ├── countMdHeadings(content) # 统计章节数(日志用) └── extractFirstMeaningfulLine(...) # 提取首个有意义文本行(日志用) ``` ### 关键逻辑(buildResult) ```java boolean needFullContent = highConfidence || !hasL1; String content = needFullContent ? buildCompactSummary(first) : buildMetadataOnlySummary(first); ``` --- ## 六、日志输出示例 ### 多匹配场景(matches=2, L1 有结果) ``` [L0 精确匹配] 完成: matches=2, time=3ms [置信度判断] highConfidence=false, reason=多个或零个匹配 [L1 语义检索] L0非唯一匹配,触发L1语义检索... [L1 语义检索] 完成: matches=1, time=245ms ---------------------------------------- <<< [工具返回] lookup_knowledge <<< [L0 主结果] 标题: 支付网关错误码定义 <<< [L0 主结果] 摘要: 记录了支付网关所有核心错误码的含义及排查方向 ← 仅元数据 <<< [L0 主结果] 内容: 126 字符, 0 个章节 ← 约150字符 <<< [L1 补充] 相似度: 0.8234 <<< [L1 补充] 内容片段: 支付网关请求超时... ← L1 提供具体内容 ``` ### 唯一匹配场景(matches=1, 跳过 L1) ``` [L0 精确匹配] 完成: matches=1, time=2ms [置信度判断] highConfidence=true, reason=唯一匹配 [L1 语义检索] L0唯一匹配,跳过L1检索 ---------------------------------------- <<< [工具返回] lookup_knowledge <<< [L0 主结果] 标题: 支付网关错误码定义 <<< [L0 主结果] 摘要: 记录了支付网关所有核心错误码的含义及排查方向 <<< [L0 主结果] 内容: 725 字符, 3 个章节 ← 约700字符 ``` --- ## 七、MVP 效率评估 & 改进方向 ### 7.1 当前效率评估 | 维度 | 评分 | 说明 | |------|:----:|------| | L0 匹配速度 | ★★★★★ | 内存索引,< 10ms,几乎没有优化空间 | | L1 检索速度 | ★★★★☆ | Milvus 向量检索,200-500ms,取决于数据量 | | L0 匹配准确率 | ★★☆☆☆ | 子串匹配,无排序无评分,匹配即返回 | | L1 检索准确率 | ★★★☆☆ | 语义相似度,但分块缺少上下文信息 | | 召回率(查全) | ★★★☆☆ | L0+L1 两阶段覆盖大多数场景,但缺乏融合重排 | | 上下文利用率 | ★★★★☆ | 根据场景动态控制 L0 内容量,已优化 | | **综合** | **★★★☆☆** | **MVP 可用,但检索质量有提升空间** | ### 7.2 关键瓶颈 #### 瓶颈 1:分块丢失上下文(✅ 已修复—见下方 7.5) 当前每个 Chunk 只记录最近的 `##` 标题: ```json { "content": "**含义**:支付网关请求超时\n**常见原因**:网络延迟", "title": "超时类错误", "chunkIndex": 2 } ``` LLM 收到这个片段时**不知道**它属于"支付网关错误码定义"这个文档,也不知道具体错误码名称是 ERR_TIMEOUT。如果同时检索了多个文档的片段,LLM 容易混淆。 #### 瓶颈 2:L0 关键词匹配过于简单 当前 `KnowledgeIndexService.matchesKeywords()` 只做子串包含匹配,没有: - 排序/评分(多个匹配时按什么顺序?) - 权重(标题匹配 > 正文匹配) - 部分匹配("timeout" 匹配 "ERR_TIMEOUT") #### 瓶颈 3:L0 和 L1 无交叉融合 两阶段检索结果只是简单的"1位L0 + 1位L1"拼接,没有: - RRF 或加权融合重排 - 重复内容去重 - 根据相关性选择 top-K --- ### 7.3 改进方向分析 #### 方向 A:面包屑导航(Chunk 携带层级上下文) **做法**:分块时记录完整的标题层级路径作为 `breadcrumb`。 当前分块 metadata: ```json { "title": "超时类错误" } ``` 改进后: ```json { "title": "超时类错误", "breadcrumb": "支付网关错误码定义 > 超时类错误 > ERR_TIMEOUT", "heading_h1": "支付网关错误码定义", "heading_h2": "超时类错误", "heading_h3": "ERR_TIMEOUT" } ``` **收益评估**: | 场景 | 无面包屑的问题 | 有面包屑的改善 | 提升幅度 | |------|---------------|---------------|:--------:| | 单文档多分块 | LLM 知道标题但不知道层级关系 | 清楚"文档>章节>条目"归属 | 中等 | | 跨文档混合结果 | 分块看不出源文档 | breadcrumb 第一段就是文档标题 | 大 | | 深层嵌套文档(3+ 级) | 分块内容难以定位 | 完整路径一目了然 | 显著 | | 向量检索相关性 | 只对 chunk content 做 embedding | breadcrumb 可拼入 content 做 embedding 或单独索引 | 中等 | **MVP 阶段价值**:当前文档结构较浅(2-3级),breadcrumb 对 LLM 理解帮助中等。但如果后续文档层级加深(像你提到的"排障指南 > 支付网关 > 502错误处理"),价值会显著提升。 **实现成本**:低。修改 `DocumentChunkService` 的分块逻辑,积累当前标题栈,写入 `DocumentChunk` 和 Milvus metadata。 #### 方向 B:混合检索 + RRF 重排 **做法**:L0 关键词和 L1 向量检索并行执行 → 结果用 Reciprocal Rank Fusion 统一排序 → 取 top-K。 ``` 用户查询 → 并行的: ├── L0 关键词匹配 → 得分向量 S₀ └── L1 向量检索 → 得分向量 S₁ ↓ RRF 融合重排 ↓ top-K 统一结果 ``` RRF 公式:对每个文档 d,`score(d) = Σ 1/(k + rank_r(d))`,其中 k=60(常数)。 **收益评估**: | 场景 | 当前的问题 | 混合 + RRF | 提升幅度 | |------|-----------|-----------|:--------:| | 精确关键词("ERR_TIMEOUT") | L0 匹配但不排序,L1 可能不匹配 | L0 高排名 → RRF 拉到顶部 | 大 | | 语义查询("支付超时如何处理") | L0 可能不匹配,全靠 L1 | L1 兜底不受影响 | 无变化 | | 混合查询("ERR_TIMEOUT 支付网关超时") | L0 匹配一个、L1 匹配一个,无融合 | RRF 统一排序,更合理 | 中等 | | 多文档匹配 | L0 返回无序列表 + L1 独立结果 | 统一排序、去重 | 大 | **MVP 阶段价值**:RRF 的实现成本和维护成本较高,而当前 MVP 数据量小(6 个文档),人工检查即可确定哪些匹配是好的。**建议数据量 > 50 个文档时引入**。 #### 方向 C:Breadcrumb + Embedding 增强 **做法**:将 breadcrumb 拼入 chunk content 后再做 embedding,让向量包含层级语义。 ```java // 当前 embeddingService.generateEmbedding(chunk.getContent()) // 改进 String augmentedContent = chunk.getBreadcrumb() + "\n" + chunk.getContent(); embeddingService.generateEmbedding(augmentedContent); ``` 这样搜索"ERR_TIMEOUT"时,"支付网关错误码定义 > 超时类错误 > ERR_TIMEOUT" 也会匹配到,而不只是 chunk 正文。 | 场景 | 当前 | Breadcrumb + Embedding | 提升 | |------|------|------------------------|:----:| | 搜索"支付网关超时" | 匹配到正文含"超时"和"支付网关"的 chunk | breadcrumb 直接含"支付网关",匹配更准 | 中等 | | 搜索"错误码定义" | 可能匹配不到具体错误内容的 chunk | breadcrumb 含"错误码定义",相关性更高 | 大 | --- ### 7.4 实施优先级建议 | 优先级 | 改进项 | 复杂度 | 收益 | 状态 | |:------:|--------|:------:|:----:|:----:| | P0 | **Breadcrumb 上下文**(方向 A) | 低 | 中 | **✅ 已实现 (2026-06-26)** | | P1 | 下个版本 | 低 | 中-大 | 待定 | | P1 | L0 排序(匹配评分 + 排序) | 低 | 中 | 待定 | | P2 | 混合检索 + RRF 重排 | 高 | 大 | 数据量 > 50 文档时引入 | ### 7.5 Breadcrumb 实现说明 已于 2026-06-26 实现。改动范围: | 文件 | 改动 | |------|------| | `DocumentChunk.java` | 新增 `breadcrumb` 字段 | | `DocumentChunkService.java` | `splitByHeadings()` 维护标题层级栈,`Section` 新增 `level`/`breadcrumb`,`chunkSection()` 和 `saveChunkAndGetNextStart()` 透传 Breadcrumb | | `VectorIndexService.java` | `buildMetadata()` 和 `buildDocumentMetadata()` 将 breadcrumb 写入 Milvus metadata | #### 层级栈算法 ```java // 在 splitByHeadings() 中,每次匹配到标题时: while (!headingStack.isEmpty() && headingStack.size() >= level) { headingStack.remove(headingStack.size() - 1); // 弹出同级或更高级 } headingStack.add(title); // 追加当前标题 currentBreadcrumb = String.join(" > ", headingStack); ``` 示例:处理 `fault-diagnosis-process.md` 的完整面包屑路径── ```json // 分块 "应急响应流程 > 1. 初步评估" { "breadcrumb": "故障诊断流程规范 > 应急响应流程 > 1. 初步评估" } // 分块 "根因分析方法 > 5-Why 分析法" { "breadcrumb": "故障诊断流程规范 > 根因分析方法 > 5-Why 分析法" } ``` #### 当前 metadata 结构(Milvus) ```json { "_source": "knowledge_base/api/payment-errors.md", "_file_name": "payment-errors.md", "category": "api", "chunkIndex": 2, "totalChunks": 5, "title": "超时类错误", "breadcrumb": "支付网关错误码定义 > 超时类错误 > ERR_TIMEOUT" } ``` 以 `fault-diagnosis-process.md` 为例: ```markdown # 故障诊断流程规范 ← heading_h1 ## 应急响应流程 ← heading_h2 ### 1. 初步评估 ← heading_h3(分块1) 内容... ### 2. 快速止血 ← heading_h3(分块2) 内容... ## 根因分析方法 ← heading_h2 ### 5-Why 分析法 ← heading_h3(分块3) 内容... ``` 改造后每个分块的 metadata: ``` 分块1: breadcrumb = "故障诊断流程规范 > 应急响应流程 > 1. 初步评估" 分块2: breadcrumb = "故障诊断流程规范 > 应急响应流程 > 2. 快速止血" 分块3: breadcrumb = "故障诊断流程规范 > 根因分析方法 > 5-Why 分析法" ``` LLM 视角受益:当检索到 "2. 快速止血" 时,LLM 立刻知道它属于"故障诊断流程规范 > 应急响应流程"体系,不需要额外读取其他分块来推断上下文。 | 文件 | 说明 | |------|------| | `LookupKnowledgeTool.java` | 检索工具入口 | | `KnowledgeIndexService.java` | L0 内存索引管理 | | `VectorSearchService.java` | L1 向量检索(Milvus) | | `KnowledgeEntry.java` | 索引条目 DTO(含 title, summary, keywords) | | `LookupResult.java` | 查询结果 DTO | | `PrimaryResult.java` | L0 结果 DTO | | `SupplementResult.java` | L1 结果 DTO |