Harden evidence trace semantics

This commit is contained in:
aruo
2026-07-04 22:36:30 +08:00
parent 246c99b954
commit dc6cd32a67
24 changed files with 1096 additions and 141 deletions
@@ -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(