Harden evidence trace semantics
This commit is contained in:
@@ -14,6 +14,7 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -309,131 +310,30 @@ public class LookupKnowledgeTool {
|
||||
String sessionId = SessionContextHolder.getSessionId();
|
||||
if (sessionId == null) return;
|
||||
|
||||
boolean hasL0 = l0Matches != null && !l0Matches.isEmpty();
|
||||
boolean hasL1 = l1Results != null && !l1Results.isEmpty();
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
double l1TopSimilarity = (l1Results != null && !l1Results.isEmpty())
|
||||
? normalizeL2(l1Results.get(0).getScore())
|
||||
: -1;
|
||||
|
||||
String layer;
|
||||
String outputPreview = null;
|
||||
int outputLength = 0;
|
||||
int l0Count = 0;
|
||||
int l1Count = 0;
|
||||
boolean truncated = false;
|
||||
|
||||
if (hasL0 && !highConfidence) {
|
||||
layer = "L0+L1";
|
||||
l0Count = l0Matches.size();
|
||||
l1Count = l1Results.size();
|
||||
} else if (hasL0) {
|
||||
layer = "L0";
|
||||
l0Count = l0Matches.size();
|
||||
} else if (hasL1) {
|
||||
layer = "L1";
|
||||
l1Count = l1Results.size();
|
||||
} else {
|
||||
layer = null;
|
||||
}
|
||||
|
||||
// output_preview
|
||||
if (result != null && result.getPrimary() != null && result.getPrimary().getContent() != null) {
|
||||
String content = result.getPrimary().getContent();
|
||||
outputLength = content.length();
|
||||
if (content.length() > 500) {
|
||||
outputPreview = content.substring(0, 500) + "...";
|
||||
truncated = true;
|
||||
} else {
|
||||
outputPreview = content;
|
||||
}
|
||||
} else if (l1Results != null && !l1Results.isEmpty() && l1Results.get(0).getContent() != null) {
|
||||
String content = l1Results.get(0).getContent();
|
||||
outputLength = content.length();
|
||||
if (content.length() > 500) {
|
||||
outputPreview = content.substring(0, 500) + "...";
|
||||
truncated = true;
|
||||
} else {
|
||||
outputPreview = content;
|
||||
}
|
||||
}
|
||||
|
||||
// L1 top score + similarity
|
||||
float l1TopScore = (hasL1) ? l1Results.get(0).getScore() : -1;
|
||||
double l1TopSimilarity = (hasL1) ? normalizeL2(l1TopScore) : -1;
|
||||
|
||||
// 构建检索明细 JSON(扩展版)
|
||||
StringBuilder details = new StringBuilder("{");
|
||||
if (hasL0) {
|
||||
details.append("\"l0_match_count\":").append(l0Count).append(",");
|
||||
details.append("\"l0_titles\":[");
|
||||
for (int i = 0; i < Math.min(3, l0Matches.size()); i++) {
|
||||
if (i > 0) details.append(",");
|
||||
details.append("\"").append(escapeJson(l0Matches.get(i).getTitle())).append("\"");
|
||||
}
|
||||
details.append("],");
|
||||
}
|
||||
if (hasL1) {
|
||||
details.append("\"l1_top_score\":").append(String.format("%.4f", l1TopScore)).append(",");
|
||||
details.append("\"l1_top_similarity\":").append(String.format("%.4f", l1TopSimilarity)).append(",");
|
||||
details.append("\"l1_match_count\":").append(l1Count).append(",");
|
||||
details.append("\"l1_scores\":[");
|
||||
for (int i = 0; i < Math.min(3, l1Results.size()); i++) {
|
||||
if (i > 0) details.append(",");
|
||||
details.append(String.format("%.4f", l1Results.get(i).getScore()));
|
||||
}
|
||||
details.append("],");
|
||||
}
|
||||
// 归一化信息
|
||||
if (result != null && result.getRelevanceLevel() != null) {
|
||||
details.append("\"relevance_level\":\"").append(result.getRelevanceLevel()).append("\",");
|
||||
details.append("\"completeness_hint\":\"").append(escapeJson(result.getCompletenessHint())).append("\",");
|
||||
}
|
||||
// 域信息
|
||||
if (domain != null) {
|
||||
details.append("\"retrieved_domains\":[\"").append(escapeJson(domain)).append("\"],");
|
||||
}
|
||||
// 去重原因
|
||||
if (dedupReason != null) {
|
||||
details.append("\"dedup_reason\":\"").append(dedupReason).append("\",");
|
||||
}
|
||||
// 移除末尾逗号
|
||||
if (details.charAt(details.length() - 1) == ',') {
|
||||
details.setLength(details.length() - 1);
|
||||
}
|
||||
details.append("}");
|
||||
|
||||
ToolInvocation inv = ToolInvocation.builder()
|
||||
.sessionId(sessionId)
|
||||
.toolName("lookup_knowledge")
|
||||
.inputParams("{\"query\":\"" + escapeJson(query) + "\"}")
|
||||
.outputPreview(outputPreview)
|
||||
.outputLength(outputLength)
|
||||
.retrievalLayer(layer)
|
||||
.l0MatchCount(hasL0 ? l0Count : null)
|
||||
.l1MatchCount(hasL1 ? l1Count : null)
|
||||
.isTruncated(truncated)
|
||||
.retrievalDetails(details.toString())
|
||||
.relevanceLevel(result != null ? result.getRelevanceLevel() : null)
|
||||
.dedupReason(dedupReason)
|
||||
.durationMs((int) duration)
|
||||
.success(true)
|
||||
.build();
|
||||
|
||||
toolInvocationRecorder.save(inv);
|
||||
ToolInvocationRecorder.LookupKnowledgeRecord record = ToolInvocationRecorder.LookupKnowledgeRecord.from(
|
||||
query,
|
||||
l0Matches,
|
||||
l1Results,
|
||||
highConfidence,
|
||||
result,
|
||||
domain,
|
||||
dedupReason,
|
||||
(int) duration,
|
||||
l1TopSimilarity
|
||||
);
|
||||
toolInvocationRecorder.recordLookupKnowledge(record);
|
||||
log.debug("tool_invocation 已保存: sessionId={}, layer={}, relevanceLevel={}, duration={}ms",
|
||||
sessionId, layer, result != null ? result.getRelevanceLevel() : null, duration);
|
||||
sessionId, record.retrievalLayer(), record.relevanceLevel(), duration);
|
||||
} catch (Exception e) {
|
||||
log.error("保存 tool_invocation 失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String escapeJson(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t");
|
||||
}
|
||||
|
||||
// ==================== 结果组装 ====================
|
||||
|
||||
private LookupResult buildResult(
|
||||
|
||||
Reference in New Issue
Block a user