1. 项目概述
在Spring应用中集成大语言模型(LLM)时,如何评估模型返回答案的质量是一个关键问题。本文将分享如何通过单元测试和评估器来验证LLM回答的相关性和准确性。
作为一名长期从事Spring开发的工程师,我发现直接调用LLM API进行测试存在几个痛点:响应不可预测、消耗Token资源、测试速度慢。本文将介绍如何通过WireMock模拟LLM响应,并构建完整的评估体系来解决这些问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搭建Mock测试环境
2.1 为什么需要Mock测试
LLM返回的内容具有不可预测性,这给单元测试带来了挑战:
- 相同问题可能得到不同回答
- 每次调用都会消耗Token
- 网络延迟影响测试速度
使用WireMock可以:
- 预设固定响应,确保测试可重复
- 避免真实API调用,节省成本
- 提高测试执行速度
2.2 WireMock集成步骤
首先添加依赖到pom.xml:
xml复制<dependency>
<groupId>org.wiremock.integrations</groupId>
<artifactId>wiremock-spring-boot</artifactId>
<version>3.10.0</version>
<scope>test</scope>
</dependency>
然后在test/resources下创建模拟响应文件,例如test-openapi-response-usa.json:
json复制{
"id": "chatcmpl-yDUbJwsur69ZLTSGiBpCUvL7QAAQ",
"object": "chat.completion",
"created": 1771113600,
"model": "qwen3.5-plus",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "华盛顿",
"refusal": null,
"annotations": []
},
"finish_reason": "stop"
}]
}
2.3 编写Mock测试类
java复制@EnableWireMock(@ConfigureWireMock(baseUrlProperties = "openai.base.url"))
@SpringBootTest(properties = "spring.ai.openai.base-url=${openai.base.url}")
public class ChatServiceMockTest {
@Value("classpath:/test-openapi-response-usa.json")
Resource responseResourceUSA;
@Autowired
ChatClient.Builder chatClientBuilder;
@Test
public void testAsk() throws IOException {
var cannedResponse = responseResourceUSA.getContentAsString(Charset.defaultCharset());
var mapper = new ObjectMapper();
var responseNode = mapper.readTree(cannedResponse);
WireMock.stubFor(WireMock.post("/v1/chat/completions")
.willReturn(ResponseDefinitionBuilder.okForJson(responseNode)));
var instance = new OpenAIChatServiceImpl(chatClientBuilder);
var chatAnswer = instance.ask(new ChatQuestion("美国的首都是哪里?"));
Assertions.assertThat(chatAnswer.answer()).isEqualTo("华盛顿");
}
}
关键点说明:
- @EnableWireMock注解启用WireMock服务器
- 通过baseUrlProperties指定模拟的API地址
- WireMock.stubFor定义请求匹配规则和响应
3. 答案评估实现
3.1 相关性评估(Relevancy)
Spring AI提供了RelevancyEvaluator来评估回答与问题的相关性:
java复制@SpringBootTest
public class ChatServiceTest {
@Autowired
private ChatService chatService;
@Autowired
private ChatClient.Builder chatClientBuilder;
private RelevancyEvaluator relevancyEvaluator;
@BeforeEach
public void setup() {
this.relevancyEvaluator = new RelevancyEvaluator(chatClientBuilder);
}
@Test
public void evaluateRelevancy() {
String userText = "Why the sky is blue?";
ChatAnswer chatAnswer = chatService.ask(new ChatQuestion(userText));
EvaluationRequest evaluationRequest = new EvaluationRequest(userText, chatAnswer.answer());
EvaluationResponse evaluationResponse = relevancyEvaluator.evaluate(evaluationRequest);
Assertions.assertThat(evaluationResponse.isPass())
.withFailMessage("答案不相关")
.isTrue();
}
}
常见问题:
- 评估器对回答格式敏感
- 中文问题可能需要添加"是"等连接词
- 评估过程较慢,建议Mock评估器
3.2 正确性评估(Factual Accuracy)
Spring AI的FactCheckingEvaluator目前对中文支持有限,可以自定义实现:
java复制private EvaluationResponse factCheckingEvaluateWithQwen(EvaluationRequest evaluationRequest) {
var client = chatClientBuilder.build();
String prompt = String.format("""
你是一个事实核查助手。
问题:%s
回答:%s
请判断上述回答是否符合客观事实。
请仅返回一个 JSON 对象:
{"pass": true/false, "score": 1.0或0.0, "feedback": "简短的理由"}
""", evaluationRequest.getUserText(), evaluationRequest.getResponseContent());
try {
String content = client.prompt(prompt).call().content();
var responseNode = new ObjectMapper().readTree(content);
return new EvaluationResponse(
responseNode.get("pass").asBoolean(),
(float) responseNode.get("score").asDouble(),
responseNode.get("feedback").asText(),
null
);
} catch (JsonProcessingException e) {
return new EvaluationResponse(false, 0.0f, e.getMessage(), null);
}
}
4. 自动重试机制
当评估不通过时,可以使用Spring Retry自动重新生成回答:
java复制@Override
@Retryable(retryFor = AnswerNotRelevantException.class, maxAttempts = 3)
public Answer askQuestion(Question question) {
var answerText = chatClient.prompt()
.user(question.question())
.call()
.content();
evaluateRelevancy(question, answerText);
return new Answer(answerText);
}
private void evaluateRelevancy(Question question, String answerText) {
var evaluationResponse = evaluator.evaluate(
new EvaluationRequest(question.question(), answerText));
if (!evaluationResponse.isPass()) {
throw new AnswerNotRelevantException(question.question(), answerText);
}
}
5. 性能优化建议
- 对评估器也使用Mock测试
- 缓存常见问题的评估结果
- 批量评估时使用异步处理
- 对评估结果建立本地数据库
6. 实际应用中的经验
- 评估标准需要根据业务调整
- 中文问题需要特殊处理
- 评估Prompt需要不断优化
- 结合人工审核提高准确率
通过这套方案,我们可以在Spring应用中构建可靠的LLM答案评估体系,确保生成内容的质量和可靠性。
