说明:文中类名 / 方法签名均在本地
spring-ai 1.1.3的 jar 中核对过;文档里几处 1.0 时代的旧写法已在「踩坑清单」标出。
1. 什么叫评估测试
普通测试断言确定值(assertEquals(2, 1 + 1)),但 AI 的输出是自然语言、每次还不一样,没法硬断言。
评估测试 = 再找一个"裁判模型",按规则对 AI 的输出打分 / 判定,把不确定的生成结果变成可断言的 true / false(或分数)。
- 目的:发现幻觉(hallucination)、答非所问、回答脱离给定上下文。
- 定位:属于集成测试,会真实调用模型(有费用、有延迟、结果不完全稳定)。
- 常见评估维度:相关性(relevancy)、事实一致性 / groundedness、答案正确性。
Spring AI 的抽象就是一个函数式接口:
java
@FunctionalInterface
public interface Evaluator {
EvaluationResponse evaluate(EvaluationRequest evaluationRequest);
}
2. 两个数据类
java
public class EvaluationRequest {
private final String userText; // 用户原始输入
private final List<Content> dataList; // 上下文数据,例如 RAG 检索到的文档
private final String responseContent; // AI 模型的回答内容
}
java
public class EvaluationResponse {
boolean isPass(); // 核心:是否通过评估
float getScore(); // 分数
String getFeedback(); // 裁判模型的反馈
Map<String, Object> getMetadata(); // 元数据
}
1.1.3 实测构造器重载有三个:
java
new EvaluationRequest(String userText, String responseContent);
new EvaluationRequest(List<Document> dataList, String responseContent);
new EvaluationRequest(String userText, List<Document> dataList, String responseContent);
3. 相关性评估器 RelevancyEvaluator
判什么:回答是否"贴合用户问题 + 检索到的上下文"。主要用于验证 RAG 流程质量(上下文有没有真被用上)。
框架内置的默认提示模板是英文的(模型只回答 YES / NO),含义如下:
Your task is to evaluate if the response for the query
is in line with the context information provided.
You have two options to answer. Either YES or NO.
Answer YES, if the response for the query
is in line with context information otherwise NO.
Query: {query}
Response:{response}
Context: {context}
Answer:
想让裁判模型用中文理解(或统一团队语言),可以换成中文版模板(占位符必须是 query / response / context 三个):
你的任务是判断下面的回答是否与提供的上下文信息一致。
你只有两个选择:YES 或 NO。
如果回答与上下文信息一致,回答 YES;否则回答 NO。
问题:
{query}
回答:
{response}
上下文:
{context}
答案:
集成测试用法(验证 RAG 流程)
java
@Test
void evaluateRelevancy() {
String question = "阿纳克莱图斯和比尔巴的冒险发生在什么地方?";
RetrievalAugmentationAdvisor ragAdvisor = RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.vectorStore(pgVectorStore)
.build())
.build();
ChatResponse chatResponse = ChatClient.builder(chatModel).build()
.prompt(question)
.advisors(ragAdvisor)
.call()
.chatResponse();
EvaluationRequest evaluationRequest = new EvaluationRequest(
question, // 原始问题
chatResponse.getMetadata().get(RetrievalAugmentationAdvisor.DOCUMENT_CONTEXT), // RAG 检索到的上下文
chatResponse.getResult().getOutput().getText() // 模型回答
);
RelevancyEvaluator evaluator = new RelevancyEvaluator(ChatClient.builder(chatModel));
EvaluationResponse evaluationResponse = evaluator.evaluate(evaluationRequest);
assertThat(evaluationResponse.isPass()).isTrue();
}
关键点:dataList 不能随便传,必须是这一次 RAG 真正检索到的上下文 (从 RetrievalAugmentationAdvisor.DOCUMENT_CONTEXT 取),否则评估没有意义。
4. 自定义模板
通过 .promptTemplate() 传入自己的 PromptTemplate;可以用任意 TemplateRenderer(默认基于 StringTemplate 的 StPromptTemplate)。
硬性要求:模板必须包含三个占位符 ------ query、response、context。
java
RelevancyEvaluator evaluator = RelevancyEvaluator.builder()
.chatClientBuilder(ChatClient.builder(chatModel))
.promptTemplate(PromptTemplate.builder()
.renderer(StTemplateRenderer.builder()
.startDelimiterToken('<').endDelimiterToken('>').build())
.template("""
问题:<query>
回答:<response>
上下文:<context>
回答是否由上下文支持?只输出 YES 或 NO。
""")
.build())
.build();
5. 事实核查评估器 FactCheckingEvaluator
判什么:回答中的"陈述(claim)"是否被给定"文档(document)"逻辑支持 ------ 用来检测幻觉。
框架内置提示模板(英文):
Document: {document}
Claim: {claim}
中文含义 / 想中文化时可用:
文档:{document}
陈述:{claim}
模型建议用更小更省的专用模型,例如 Bespoke-Minicheck(可通过 Ollama 跑),比 GPT-4 便宜得多。
java
@Test
void testFactChecking() {
OllamaApi ollamaApi = new OllamaApi("http://localhost:11434");
ChatModel chatModel = new OllamaChatModel(ollamaApi,
OllamaChatOptions.builder().model(BESPOKE_MINICHECK).numPredict(2).temperature(0.0d).build());
var factCheckingEvaluator = new FactCheckingEvaluator(ChatClient.builder(chatModel));
String context = "地球是距离太阳第三近的行星,也是目前已知唯一存在生命的天体。";
String claim = "地球是距离太阳第四近的行星。";
EvaluationRequest evaluationRequest = new EvaluationRequest(context, Collections.emptyList(), claim);
EvaluationResponse evaluationResponse = factCheckingEvaluator.evaluate(evaluationRequest);
assertFalse(evaluationResponse.isPass(), "该陈述与上下文矛盾,应当判为不通过");
}
6. ⚠ 1.1.3 踩坑清单(文档 vs 实际)
| 文档 / 教程写法 | 1.1.3 实际 |
|---|---|
org.springframework.ai.evaluation.RelevancyEvaluator |
org.springframework.ai.chat.evaluation.RelevancyEvaluator |
org.springframework.ai.evaluation.FactCheckingEvaluator |
org.springframework.ai.chat.evaluation.FactCheckingEvaluator |
EvaluationRequest(List<Content> dataList, ...) |
实际是 List<Document>;EvaluationRequest / EvaluationResponse / Evaluator 在 org.springframework.ai.evaluation(spring-ai-commons) |
new FactCheckingEvaluator(chatClientBuilder) |
该构造器是 protected,只能用 FactCheckingEvaluator.builder(builder) 或 FactCheckingEvaluator.forBespokeMinicheck(builder) |
即导入应该是:
java
import org.springframework.ai.chat.evaluation.RelevancyEvaluator; // spring-ai-client-chat
import org.springframework.ai.chat.evaluation.FactCheckingEvaluator; // spring-ai-client-chat
import org.springframework.ai.evaluation.EvaluationRequest; // spring-ai-commons
import org.springframework.ai.evaluation.EvaluationResponse; // spring-ai-commons
import org.springframework.ai.evaluation.Evaluator; // spring-ai-commons
照抄旧文档会直接 Cannot resolve symbol / 构造器不可见。
7. 落地建议
- 评估测试会真实烧 token ,建议用
@Disabled或@Tag("evaluation")隔离,别让mvn test每次都跑。 - 评估模型可以和生成模型不同("强模型生成、专/小模型评判"是常见省钱姿势)。
- 裁判模型本身也有偏差:
isPass() == false只能当信号,不能直接当结论。 - 写 JUnit 5 +
assertThat需要spring-boot-starter-test;只引入junit:junit:3.8.1的模块跑不了。 - 本模块用的是内存
SimpleVectorStore,评估前必须先灌库,否则检索上下文为空、评估必然失败。