feat(knowledge): Executor 行动记忆 + 归一化质量等级解决 ISS-002 重复检索
- RetrievedDocTracker 升级为域级+文档级双层记录(Map<sessionId, Map<domain, Set<filePath>>>) - LookupKnowledgeTool 新增 Min-Max 归一化层(BGE-M3 L2 距离→[0,1] similarity) - 三等级 relevanceLevel:PRECISE / HIGHLY_RELEVANT / REFERENCE + completenessHint 兜底信号 - LookupResult 新增 relevanceLevel、completenessHint、retrievedDomainsThisSession - Executor prompt 重写:4 条检索约束 + 合法出口不查全不追责,重复检索才惩罚 - 入库可观测性:V010 迁移 + retrieval_details JSON 扩展 - 归档 executor-action-memory-relevance change
This commit is contained in:
@@ -3,34 +3,87 @@ package com.superbiz.agent.tool;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* session 级已召回文档追踪器
|
||||
* 防止同一 session 内重复召回相同文档
|
||||
* 支持文档级去重 + 域级行动记忆
|
||||
*
|
||||
* 数据结构:sessionId → { domain → Set<filePath> }
|
||||
* - 域级:控制"不要重复查同域",提供行动记忆给 LLM
|
||||
* - 文档级:控制"不要重复召回同文档"(替代原有单层结构)
|
||||
*/
|
||||
@Component
|
||||
public class RetrievedDocTracker {
|
||||
|
||||
// key: sessionId, value: 已召回文档的 filePath 集合
|
||||
private final ConcurrentHashMap<String, Set<String>> retrieved = new ConcurrentHashMap<>();
|
||||
// key: sessionId, value: { domain → Set<filePath> }
|
||||
private final ConcurrentHashMap<String, Map<String, Set<String>>> sessionRetrievals = new ConcurrentHashMap<>();
|
||||
|
||||
public boolean isAlreadyRetrieved(String sessionId, String filePath) {
|
||||
if (sessionId == null || filePath == null) return false;
|
||||
Set<String> docs = retrieved.get(sessionId);
|
||||
return docs != null && docs.contains(filePath);
|
||||
}
|
||||
|
||||
public void markRetrieved(String sessionId, String filePath) {
|
||||
/**
|
||||
* 记录一次检索(域级 + 文档级)
|
||||
*/
|
||||
public void markRetrieved(String sessionId, String domain, String filePath) {
|
||||
if (sessionId == null || filePath == null) return;
|
||||
retrieved.computeIfAbsent(sessionId,
|
||||
k -> Collections.newSetFromMap(new ConcurrentHashMap<>()))
|
||||
|
||||
sessionRetrievals.computeIfAbsent(sessionId,
|
||||
k -> new ConcurrentHashMap<>())
|
||||
.computeIfAbsent(domain != null ? domain : "_unknown",
|
||||
d -> Collections.newSetFromMap(new ConcurrentHashMap<>()))
|
||||
.add(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档级去重:检查 filePath 是否已在本会话中检索过
|
||||
*/
|
||||
public boolean isDocRetrieved(String sessionId, String filePath) {
|
||||
if (sessionId == null || filePath == null) return false;
|
||||
Map<String, Set<String>> domains = sessionRetrievals.get(sessionId);
|
||||
if (domains == null) return false;
|
||||
return domains.values().stream().anyMatch(docs -> docs.contains(filePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* 域级检查:检查 domain 是否已在本会话中检索过
|
||||
*/
|
||||
public boolean isDomainRetrieved(String sessionId, String domain) {
|
||||
if (sessionId == null || domain == null) return false;
|
||||
Map<String, Set<String>> domains = sessionRetrievals.get(sessionId);
|
||||
return domains != null && domains.containsKey(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本次会话已检索的域列表(行动记忆,返回给 LLM)
|
||||
*/
|
||||
public List<String> getRetrievedDomains(String sessionId) {
|
||||
if (sessionId == null) return List.of();
|
||||
Map<String, Set<String>> domains = sessionRetrievals.get(sessionId);
|
||||
if (domains == null) return List.of();
|
||||
return List.copyOf(domains.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 向后兼容:文档级去重(委托给 isDocRetrieved)
|
||||
*/
|
||||
public boolean isAlreadyRetrieved(String sessionId, String filePath) {
|
||||
return isDocRetrieved(sessionId, filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向后兼容:旧版 markRetrieved(domain 设为 null,归入 _unknown)
|
||||
*/
|
||||
public void markRetrieved(String sessionId, String filePath) {
|
||||
markRetrieved(sessionId, null, filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理会话
|
||||
*/
|
||||
public void clearSession(String sessionId) {
|
||||
if (sessionId == null) return;
|
||||
retrieved.remove(sessionId);
|
||||
sessionRetrievals.remove(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user