## 改动内容 ### 1. ChatService - Agent 执行日志 在 `executeChat` 方法中添加: ``` ======================================== ========== Agent 执行开始 ========== ======================================== 📝 用户问题: 支付为什么会失败? ---------------------------------------- 🚀 执行 ReactAgent.call() - 自动处理工具调用 ======================================== ========== Agent 执行完成 ========== ======================================== ⏱️ 执行耗时: 1523 ms 📏 最终输出长度: 456 字符 ---------------------------------------- 📤 最终输出内容: 根据知识库的记录,支付失败的主要原因是... ======================================== ``` **关键信息**: - 用户问题 - 执行耗时 - 最终输出长度和内容 --- ### 2. LookupKnowledgeTool - 工具调用详细日志 ``` ======================================== >>> [工具调用] lookup_knowledge >>> 参数: query = "支付为什么会失败?" >>> RequestId: a3b4c5d6 ---------------------------------------- [L0 精确匹配] 完成: matches=0, time=3ms [置信度判断] highConfidence=false, reason=多个或零个匹配 [L1 语义检索] L0非唯一匹配,触发L1语义检索... [L1 语义检索] 完成: matches=1, time=245ms [L1 语义检索] 找到文档: - [1] 文档ID: doc-123, 相似度得分: 0.82 ---------------------------------------- <<< [工具返回] lookup_knowledge <<< 结果: found=true, matchType=semantic_L1, confidence=medium <<< 总耗时: 248ms (L0=3ms, L1=245ms) <<< 返回内容长度: 1234 字符 <<< 内容预览: ## 支付网关错误码定义... ======================================== ``` **关键信息**: - 工具名称和参数 - L0/L1 执行时间和结果 - 匹配文档列表 - 返回结果摘要 --- ## 日志格式说明 ### 符号约定 - `>>>` - 工具调用(入参) - `<<<` - 工具返回(出参) - `***` - Agent 思考过程(暂未实现) - `📝` - 用户输入 - `📤` - Agent 输出 - `⏱️` - 性能指标 ### 日志级别 - `INFO` - 关键节点和结果 - `DEBUG` - 详细的中间状态(已设置但默认不显示) --- ## 使用场景 ### 1. 调试工具调用 ```bash # 查看工具调用详情 grep "工具调用\|工具返回" logs/application.log # 输出示例 >>> [工具调用] lookup_knowledge >>> 参数: query = "ERR_TIMEOUT" <<< [工具返回] lookup_knowledge <<< 结果: found=true, matchType=exact_L0, confidence=high ``` ### 2. 性能分析 ```bash # 查看执行耗时 grep "执行耗时\|总耗时" logs/application.log # 输出示例 ⏱️ 执行耗时: 1523 ms <<< 总耗时: 248ms (L0=3ms, L1=245ms) ``` ### 3. L0/L1 验证 ```bash # 查看检索路径 grep "L0精确匹配\|L1语义检索" logs/application.log # 示例 - L0 命中 [L0 精确匹配] 完成: matches=1, time=3ms [L0 精确匹配] 找到文档: - [1] 标题: 支付网关错误码定义, 路径: api/payment-errors.md [L1 语义检索] L0唯一匹配,跳过L1检索 # 示例 - L1 命中 [L0 精确匹配] 完成: matches=0, time=2ms [L1 语义检索] L0非唯一匹配,触发L1语义检索... [L1 语义检索] 完成: matches=1, time=245ms ``` --- ## 后续优化 ### 可能的增强(未实现) 由于阿里云 ReactAgent 不支持内置监听器,以下功能暂时无法实现: - ❌ Agent 思考过程实时监听(`onStateUpdate`) - ❌ 工具调用前拦截(`onToolCall`) - ❌ 工具返回后拦截(`onToolResponse`) 如需这些功能,需要: 1. 包装每个工具,统一添加日志 2. 或使用支持监听器的 Agent 框架 当前实现已满足基本可观测需求。 --- ## 验证 ```bash # 1. 启动应用 mvn spring-boot:run # 2. 发起对话 curl -X POST http://localhost:9900/api/chat \ -H "Content-Type: application/json" \ -d '{"id":"test","question":"支付为什么会失败?"}' # 3. 查看日志 tail -f logs/application.log | grep -E "Agent|工具|输出" ```
173 lines
7.6 KiB
Java
173 lines
7.6 KiB
Java
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 = "查询内部知识库文档,获取错误码定义、接口文档、排障步骤、配置说明等背景信息。" +
|
||
"采用两阶段检索:L0 精确匹配关键词(< 10ms),L1 语义检索补充(200-500ms)。" +
|
||
"IMPORTANT: 遇到错误码、接口名、配置项、排障问题时,优先使用此工具。" +
|
||
"支持的查询场景:" +
|
||
"1) 错误码定义 - 查询错误码的含义和处理方法,例如 'ERR_TIMEOUT'、'ERR_CONNECTION_REFUSED';" +
|
||
"2) 接口文档 - 查询 API 接口定义、参数说明、返回格式,例如 'payment-gateway'、'/api/v1/orders';" +
|
||
"3) 排障步骤 - 查询故障诊断流程、最佳实践,例如 '支付超时排查'、'数据库连接池配置';" +
|
||
"4) 配置说明 - 查询系统配置、中间件参数,例如 'HikariCP'、'Redis 集群配置'。" +
|
||
"参数 query: 查询关键词或描述")
|
||
public LookupResult lookupKnowledge(String query) {
|
||
// 生成请求ID用于追踪
|
||
String requestId = java.util.UUID.randomUUID().toString().substring(0, 8);
|
||
long startTime = System.currentTimeMillis();
|
||
|
||
log.info("========================================");
|
||
log.info(">>> [工具调用] lookup_knowledge");
|
||
log.info(">>> 参数: query = \"{}\"", query);
|
||
log.info(">>> RequestId: {}", requestId);
|
||
log.info("----------------------------------------");
|
||
|
||
// Step 1: L0 精确匹配
|
||
long l0Start = System.currentTimeMillis();
|
||
List<KnowledgeEntry> l0Matches = knowledgeIndexService.exactMatch(query);
|
||
long l0Time = System.currentTimeMillis() - l0Start;
|
||
log.info("[L0 精确匹配] 完成: matches={}, time={}ms", l0Matches.size(), l0Time);
|
||
if (!l0Matches.isEmpty()) {
|
||
log.info("[L0 精确匹配] 找到文档:");
|
||
for (int i = 0; i < Math.min(3, l0Matches.size()); i++) {
|
||
KnowledgeEntry entry = l0Matches.get(i);
|
||
log.info(" - [{}] 标题: {}, 路径: {}", i+1, entry.getTitle(), entry.getFilePath());
|
||
}
|
||
}
|
||
|
||
// Step 2: 判断是否高置信度(唯一匹配)
|
||
boolean highConfidence = (l0Matches.size() == 1);
|
||
log.info("[置信度判断] highConfidence={}, reason={}",
|
||
highConfidence, highConfidence ? "唯一匹配" : "多个或零个匹配");
|
||
|
||
// Step 3: L1 条件调用
|
||
List<VectorSearchService.SearchResult> l1Results = null;
|
||
if (!highConfidence) {
|
||
log.info("[L1 语义检索] L0非唯一匹配,触发L1语义检索...");
|
||
long l1Start = System.currentTimeMillis();
|
||
l1Results = vectorSearchService.searchSimilarDocuments(query, 3, null);
|
||
long l1Time = System.currentTimeMillis() - l1Start;
|
||
log.info("[L1 语义检索] 完成: matches={}, time={}ms",
|
||
l1Results != null ? l1Results.size() : 0, l1Time);
|
||
if (l1Results != null && !l1Results.isEmpty()) {
|
||
log.info("[L1 语义检索] 找到文档:");
|
||
for (int i = 0; i < Math.min(3, l1Results.size()); i++) {
|
||
VectorSearchService.SearchResult result = l1Results.get(i);
|
||
log.info(" - [{}] 文档ID: {}, 相似度得分: {}", i+1, result.getId(), result.getScore());
|
||
}
|
||
}
|
||
} else {
|
||
log.info("[L1 语义检索] L0唯一匹配,跳过L1检索");
|
||
}
|
||
|
||
// Step 4: 组装结果
|
||
LookupResult result = buildResult(l0Matches, l1Results, highConfidence);
|
||
|
||
// 记录完整结果
|
||
long totalTime = System.currentTimeMillis() - startTime;
|
||
log.info("----------------------------------------");
|
||
log.info("<<< [工具返回] lookup_knowledge");
|
||
log.info("<<< 结果: found={}, matchType={}, confidence={}",
|
||
result.isFound(),
|
||
result.getPrimary() != null ? result.getPrimary().getMatchType() : "N/A",
|
||
result.getPrimary() != null ? result.getPrimary().getConfidence() : "N/A");
|
||
log.info("<<< 总耗时: {}ms (L0={}ms, L1={}ms)",
|
||
totalTime, l0Time, l1Results != null ? (totalTime - l0Time) : 0);
|
||
if (result.isFound() && result.getPrimary() != null) {
|
||
String content = result.getPrimary().getContent();
|
||
log.info("<<< 返回内容长度: {} 字符", content != null ? content.length() : 0);
|
||
if (content != null && content.length() > 200) {
|
||
log.info("<<< 内容预览: {}", content.substring(0, 200) + "...");
|
||
}
|
||
}
|
||
log.info("========================================");
|
||
|
||
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();
|
||
}
|
||
}
|