十一、评估测试

说明:文中类名 / 方法签名均在本地 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,评估前必须先灌库,否则检索上下文为空、评估必然失败。
相关推荐
东方佑1 小时前
从《事件发生与智能》的框架看中国古代命理学
人工智能·深度学习·语言模型·自然语言处理·架构
szial1 小时前
网络爬虫与 CDP 实战(二):浏览器能访问,Python 却被拒绝?拆开登录态与 CSRF
爬虫·python·csrf
天远API1 小时前
零信任架构实战:基于天远运营商三要素简版V即时版查询构建自动化司乘安全绑定网关
人工智能·安全·架构·自动化
Henry-SAP1 小时前
SAP PP核心引擎计划策略业务解析
人工智能·云原生·sap·erp
左手の明天1 小时前
SLIP 协议封装与解析详解:原理、C 代码与实战案例
linux·c语言·开发语言·c++
DP DPharness2 小时前
拆开 dsh-knowledge 的检索链路,看 RRF 融合与锚点续读
人工智能·dpharness
海上小飞龙2 小时前
【KMP算法-下篇】同一道题:Java 库函数 2600 微秒,手写 KMP 16 微秒
java·开发语言·算法
I'm a winner2 小时前
《AI 赋能嵌入式开发:从 0 到全栈工程师》模块1|第4课时
人工智能
johnsong2 小时前
当决策成为免费商品:思维成本崩溃背后的治理真空
大数据·人工智能