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:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user