feat: 支持一库一工具 Agentic RAG

- 将知识库声明与 Retriever 绑定为独立 Registration 并注册到 AgentScope Toolkit

- 统一检索分数、最终文档事件与长答案引用语义

- 补齐历史会话、工具类加载器和知识库调用状态回归测试
This commit is contained in:
2026-08-29 15:41:52 +08:00
parent e146653de7
commit 47a2706f11
18 changed files with 1365 additions and 427 deletions

View File

@@ -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;

View File

@@ -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);