feat: 支持一库一工具 Agentic RAG
- 将知识库声明与 Retriever 绑定为独立 Registration 并注册到 AgentScope Toolkit - 统一检索分数、最终文档事件与长答案引用语义 - 补齐历史会话、工具类加载器和知识库调用状态回归测试
This commit is contained in:
@@ -2,14 +2,26 @@ package com.easyagents.rag.retrieval;
|
||||
|
||||
import com.easyagents.core.document.Document;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 将不同检索路径的原始分数转换为统一的零到一最终相关度。
|
||||
*/
|
||||
public final class RagScoreNormalizer {
|
||||
|
||||
/**
|
||||
* 禁止实例化工具类。
|
||||
*/
|
||||
private RagScoreNormalizer() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 按检索模式归一化文档最终分数。
|
||||
*
|
||||
* @param documents 待归一化文档
|
||||
* @param retrievalMode 检索模式
|
||||
* @param reranked 是否已经过重排模型
|
||||
*/
|
||||
public static void normalize(List<Document> documents, RetrievalMode retrievalMode, boolean reranked) {
|
||||
if (documents == null || documents.isEmpty()) {
|
||||
return;
|
||||
@@ -49,41 +61,29 @@ public final class RagScoreNormalizer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保留重排模型返回的绝对相关度,并将异常范围限制到零到一。
|
||||
*
|
||||
* @param documents 待归一化的文档
|
||||
*/
|
||||
private static void normalizeRerankScores(List<Document> documents) {
|
||||
List<Double> rawScores = new ArrayList<Double>(documents.size());
|
||||
boolean allPresent = true;
|
||||
Double min = null;
|
||||
Double max = null;
|
||||
for (Document document : documents) {
|
||||
Double rawScore = readRawScore(document, RagRetrievalMetadataKeys.RERANK_SCORE, document == null ? null : document.getScore());
|
||||
rawScores.add(rawScore);
|
||||
if (rawScore == null) {
|
||||
allPresent = false;
|
||||
continue;
|
||||
if (document != null) {
|
||||
// Rerank 适配器返回绝对相关度,按查询结果集再次缩放会把低相关第一名错误抬高到 1。
|
||||
document.setScore(clamp01(rawScore));
|
||||
}
|
||||
min = min == null ? rawScore : Math.min(min, rawScore);
|
||||
max = max == null ? rawScore : Math.max(max, rawScore);
|
||||
}
|
||||
|
||||
if (allPresent && min != null && max != null && Double.compare(max, min) != 0) {
|
||||
for (int i = 0; i < documents.size(); i++) {
|
||||
Double rawScore = rawScores.get(i);
|
||||
documents.get(i).setScore(clamp01((rawScore - min) / (max - min)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (documents.size() == 1) {
|
||||
documents.get(0).setScore(1D);
|
||||
return;
|
||||
}
|
||||
|
||||
int size = documents.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
documents.get(i).setScore(clamp01(1D - ((double) i / (double) (size - 1))));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 优先读取指定元数据中的原始分数。
|
||||
*
|
||||
* @param document 文档
|
||||
* @param metadataKey 分数元数据键
|
||||
* @param fallback 元数据不可用时的回退分数
|
||||
* @return 原始分数;文档为空时返回 null
|
||||
*/
|
||||
private static Double readRawScore(Document document, String metadataKey, Double fallback) {
|
||||
if (document == null) {
|
||||
return null;
|
||||
@@ -102,6 +102,12 @@ public final class RagScoreNormalizer {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将可空分数限制到零到一范围。
|
||||
*
|
||||
* @param value 原始分数
|
||||
* @return 有效最终分数
|
||||
*/
|
||||
private static double clamp01(Double value) {
|
||||
if (value == null || value.isNaN() || value.isInfinite()) {
|
||||
return 0D;
|
||||
|
||||
@@ -7,8 +7,14 @@ import org.junit.Test;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link RagScoreNormalizer} 回归测试。
|
||||
*/
|
||||
public class RagScoreNormalizerTest {
|
||||
|
||||
/**
|
||||
* 验证关键词分数按有界函数归一化。
|
||||
*/
|
||||
@Test
|
||||
public void shouldNormalizeKeywordScoresToZeroAndOneRange() {
|
||||
Document first = document(1, 9D, RagRetrievalMetadataKeys.KEYWORD_SCORE);
|
||||
@@ -20,6 +26,9 @@ public class RagScoreNormalizerTest {
|
||||
Assert.assertEquals(0D, second.getScore(), 0.0001D);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证混合检索 RRF 分数按理论上界归一化。
|
||||
*/
|
||||
@Test
|
||||
public void shouldNormalizeHybridFusionScoreByRrfUpperBound() {
|
||||
Document document = document(1, 2D / (RrfFusionStrategy.DEFAULT_RRF_K + 1D), RagRetrievalMetadataKeys.FUSION_SCORE);
|
||||
@@ -29,36 +38,60 @@ public class RagScoreNormalizerTest {
|
||||
Assert.assertEquals(1D, document.getScore(), 0.0001D);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证重排模型返回的绝对相关度保持不变。
|
||||
*/
|
||||
@Test
|
||||
public void shouldNormalizeRerankScoresByMinMax() {
|
||||
public void shouldPreserveRerankRelevanceScores() {
|
||||
List<Document> documents = Arrays.asList(
|
||||
document(1, 10D, RagRetrievalMetadataKeys.RERANK_SCORE),
|
||||
document(2, 20D, RagRetrievalMetadataKeys.RERANK_SCORE),
|
||||
document(3, 30D, RagRetrievalMetadataKeys.RERANK_SCORE)
|
||||
document(1, 0.1D, RagRetrievalMetadataKeys.RERANK_SCORE),
|
||||
document(2, 0.5D, RagRetrievalMetadataKeys.RERANK_SCORE),
|
||||
document(3, 0.9D, RagRetrievalMetadataKeys.RERANK_SCORE)
|
||||
);
|
||||
|
||||
RagScoreNormalizer.normalize(documents, RetrievalMode.HYBRID, true);
|
||||
|
||||
Assert.assertEquals(0.1D, documents.get(0).getScore(), 0.0001D);
|
||||
Assert.assertEquals(0.5D, documents.get(1).getScore(), 0.0001D);
|
||||
Assert.assertEquals(0.9D, documents.get(2).getScore(), 0.0001D);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证单条低分重排结果不会被抬高为高相关结果。
|
||||
*/
|
||||
@Test
|
||||
public void shouldKeepSingleLowRerankScoreLow() {
|
||||
Document document = document(1, 0.1D, RagRetrievalMetadataKeys.RERANK_SCORE);
|
||||
|
||||
RagScoreNormalizer.normalize(Arrays.asList(document), RetrievalMode.HYBRID, true);
|
||||
|
||||
Assert.assertEquals(0.1D, document.getScore(), 0.0001D);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证越界重排分数会被限制到零到一范围。
|
||||
*/
|
||||
@Test
|
||||
public void shouldClampRerankScoresToZeroAndOneRange() {
|
||||
List<Document> documents = Arrays.asList(
|
||||
document(1, -0.2D, RagRetrievalMetadataKeys.RERANK_SCORE),
|
||||
document(2, 1.2D, RagRetrievalMetadataKeys.RERANK_SCORE)
|
||||
);
|
||||
|
||||
RagScoreNormalizer.normalize(documents, RetrievalMode.HYBRID, true);
|
||||
|
||||
Assert.assertEquals(0D, documents.get(0).getScore(), 0.0001D);
|
||||
Assert.assertEquals(0.5D, documents.get(1).getScore(), 0.0001D);
|
||||
Assert.assertEquals(1D, documents.get(2).getScore(), 0.0001D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFallbackToRankBasedNormalizationWhenRerankScoresAreEqual() {
|
||||
List<Document> documents = Arrays.asList(
|
||||
document(1, 5D, RagRetrievalMetadataKeys.RERANK_SCORE),
|
||||
document(2, 5D, RagRetrievalMetadataKeys.RERANK_SCORE),
|
||||
document(3, 5D, RagRetrievalMetadataKeys.RERANK_SCORE)
|
||||
);
|
||||
|
||||
RagScoreNormalizer.normalize(documents, RetrievalMode.HYBRID, true);
|
||||
|
||||
Assert.assertEquals(1D, documents.get(0).getScore(), 0.0001D);
|
||||
Assert.assertEquals(0.5D, documents.get(1).getScore(), 0.0001D);
|
||||
Assert.assertEquals(0D, documents.get(2).getScore(), 0.0001D);
|
||||
Assert.assertEquals(1D, documents.get(1).getScore(), 0.0001D);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建携带原始分数元数据的测试文档。
|
||||
*
|
||||
* @param id 文档 ID
|
||||
* @param score 原始分数
|
||||
* @param metadataKey 分数元数据键
|
||||
* @return 测试文档
|
||||
*/
|
||||
private Document document(Object id, Double score, String metadataKey) {
|
||||
Document document = new Document();
|
||||
document.setId(id);
|
||||
|
||||
Reference in New Issue
Block a user