feat(knowledge): 完成 L0+L1 混合检索集成

核心功能:
- 新增 FrontmatterParser 解析 YAML frontmatter
- 新增 KnowledgeIndexService L0 内存索引
- 新增 LookupKnowledgeTool 混合检索工具
- 增强 DocumentManagementService 文件保存和索引同步

技术实现:
- 数据库迁移 V004: api_document.metadata (TEXT)
- 依赖新增: snakeyaml 2.0
- 配置新增: knowledge.base-path
- 可观测性: requestId 追踪 + 性能日志

质量保证:
- 单元测试: 31/31 通过
- 测试覆盖: FrontmatterParser(11), KnowledgeIndexService(13), LookupKnowledgeTool(7)
- 启动验证: L0 索引正常加载

归档文档:
- OpenSpec: openspec/changes/lookup-knowledge-integration/
- devflow 档案: devflow/projects/2026-06-24-lookup-knowledge-integration/
- handoff: handoff/2026-06-24-lookup-knowledge-integration.md
This commit is contained in:
zhuyongxin
2026-06-24 16:07:10 +08:00
parent c86045b33f
commit d6229f3385
32 changed files with 5396 additions and 67 deletions
@@ -0,0 +1,138 @@
package com.superbiz.agent.tool;
import com.superbiz.agent.dto.*;
import com.superbiz.agent.service.KnowledgeIndexService;
import com.superbiz.agent.service.VectorSearchService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 知识库查询工具
* 提供给 Agent 的混合检索工具(L0 + L1)
*/
@Slf4j
@Component
public class LookupKnowledgeTool {
@Autowired
private KnowledgeIndexService knowledgeIndexService;
@Autowired
private VectorSearchService vectorSearchService;
/**
* 查询知识库文档
*
* @param query 查询关键词
* @return 查询结果
*/
@Tool(description = "查询知识库文档。优先精确匹配关键词,未命中或多个匹配时自动补充语义相关片段。" +
"参数 query: 查询关键词,例如 'ERR_TIMEOUT'、'支付网关超时'")
public LookupResult lookupKnowledge(String query) {
// 生成请求ID用于追踪
String requestId = java.util.UUID.randomUUID().toString().substring(0, 8);
long startTime = System.currentTimeMillis();
log.info("[{}] 收到知识库查询请求: query={}", requestId, query);
// Step 1: L0 精确匹配
long l0Start = System.currentTimeMillis();
List<KnowledgeEntry> l0Matches = knowledgeIndexService.exactMatch(query);
long l0Time = System.currentTimeMillis() - l0Start;
log.info("[{}] L0精确匹配完成: matches={}, time={}ms", requestId, l0Matches.size(), l0Time);
// Step 2: 判断是否高置信度(唯一匹配)
boolean highConfidence = (l0Matches.size() == 1);
log.debug("[{}] 置信度判断: highConfidence={}, reason={}",
requestId, highConfidence, highConfidence ? "唯一匹配" : "多个或零个匹配");
// Step 3: L1 条件调用
List<VectorSearchService.SearchResult> l1Results = null;
if (!highConfidence) {
log.info("[{}] L0非唯一匹配,触发L1语义检索", requestId);
long l1Start = System.currentTimeMillis();
l1Results = vectorSearchService.searchSimilarDocuments(query, 3, null);
long l1Time = System.currentTimeMillis() - l1Start;
log.info("[{}] L1语义检索完成: matches={}, time={}ms",
requestId, l1Results != null ? l1Results.size() : 0, l1Time);
} else {
log.debug("[{}] L0唯一匹配,跳过L1检索", requestId);
}
// Step 4: 组装结果
LookupResult result = buildResult(l0Matches, l1Results, highConfidence);
// 记录完整结果
long totalTime = System.currentTimeMillis() - startTime;
log.info("[{}] 查询完成: found={}, hasL0={}, hasL1={}, confidence={}, totalTime={}ms",
requestId,
result.isFound(),
result.getPrimary() != null,
result.getSupplement() != null,
result.getPrimary() != null ? result.getPrimary().getConfidence() : "N/A",
totalTime);
return result;
}
/**
* 组装查询结果
*
* @param l0Matches L0 匹配结果
* @param l1Results L1 检索结果
* @param highConfidence 是否高置信度
* @return 组装后的结果
*/
private LookupResult buildResult(
List<KnowledgeEntry> l0Matches,
List<VectorSearchService.SearchResult> l1Results,
boolean highConfidence
) {
LookupResult.LookupResultBuilder builder = LookupResult.builder();
// 构建 primary(L0 结果)
PrimaryResult primary = null;
if (l0Matches != null && !l0Matches.isEmpty()) {
KnowledgeEntry first = l0Matches.get(0);
String content = knowledgeIndexService.readDocument(first.getFilePath(), 2000);
if (content != null) {
primary = PrimaryResult.builder()
.content(content)
.source(first.getFilePath())
.matchType("exact_L0")
.confidence(highConfidence ? "high" : "low")
.availableSections(null) // MVP 返回 null
.build();
log.debug("L0结果已构建: source={}, contentLength={}", first.getFilePath(), content.length());
} else {
log.warn("L0匹配但文件读取失败: {}", first.getFilePath());
}
}
builder.primary(primary);
// 构建 supplement(L1 结果)
SupplementResult supplement = null;
boolean hasL1 = l1Results != null && !l1Results.isEmpty();
if (hasL1) {
VectorSearchService.SearchResult firstL1 = l1Results.get(0);
supplement = SupplementResult.builder()
.content(firstL1.getContent())
.source(firstL1.getMetadata())
.matchType("semantic_L1")
.build();
log.debug("L1结果已构建: source={}, score={}", firstL1.getMetadata(), firstL1.getScore());
}
builder.supplement(supplement);
// 判断是否找到结果(primary 或 supplement 至少有一个)
boolean found = (primary != null) || (supplement != null);
builder.found(found);
return builder.build();
}
}