文章目录
- [1 使用示例](#1 使用示例)
- [2 源码结构](#2 源码结构)
- [3 总结](#3 总结)
Model Evaluation 是指评估人工智能应用程序生成的内容,以确保人工智能模型没有产生幻觉反应。
大多数团队只在人工验收或灰度阶段"看一眼"回答质量,没有统一的、可回归的量化标准。
Spring AI 给了我们现成的评估 API,可以把"主观体验"变成可重复的自动化测试:
- 回归友好:Prompt/RAG 升级后,历史样例一键回放,立刻知道哪些场景退化了。
- 客观量化:相关性 & 事实性拆分评估,精确定位问题在"取数"还是"生成"。
- 低成本试错:评估模型可选更小/更便宜的模型,或本地模型。
1 使用示例
1_引入依赖
下面几个坐标均来自 Spring AI 官方仓库与文档,版本以 1.0.1 为例。
xml
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-advisors-vector-store</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-rag</artifactId>
</dependency>
2_配置模型信息
application.yml
示例:
yaml
spring:
ai:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode # springai会自动补全 /v1
api-key: ${API-KEY} # 来自阿里云百炼
embedding: # 向量模型
options:
model: text-embedding-v4
dimensions: 1024
chat: # 对话模型
options:
model: qwen-turbo
temperature: 0.8
3_配置客户端
在容器内部定义两个模型客户端,分别用于基本对话和模型评价:
java
/**
* @author shenyang
* @version 1.0
* 利用 qwen-max 模型生成内容,同时再利用 qwen-plus 模型进行校验幻觉
*/
@Configuration
public class AiConfiguration {
@Value("${spring.ai.openai.base-url}")
private String baseUrl;
private final String apiKey = System.getenv("API-KEY");
@Bean
public SimpleVectorStore simpleVectorStore(OpenAiEmbeddingModel openAiEmbeddingModel){
return SimpleVectorStore.builder(openAiEmbeddingModel).build();
}
@Bean(name = "baseChatClient")
public ChatClient baseChatClient(@Autowired @Qualifier("openAiChatModel") OpenAiChatModel openAiChatModel, ChatMemory chatMemory, SimpleVectorStore simpleVectorStore) {
return ChatClient.builder(openAiChatModel)
.defaultSystem("请根据提供的上下文回答问题,不要自己猜测。")
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build(), // CHAT MEMORY
new SimpleLoggerAdvisor(),
QuestionAnswerAdvisor.builder(simpleVectorStore).
searchRequest(SearchRequest.builder().similarityThreshold(0.5d).topK(10).build()).build(),
//会把检索到的文档上下文放到 ChatResponse 的 metadata 中,评估时可直接复用
RetrievalAugmentationAdvisor.builder()
.documentRetriever(VectorStoreDocumentRetriever.builder()
.vectorStore(simpleVectorStore)
.build()).build())
.build();
}
/**
* 定义评价模型 qwen-plus
*/
@Bean(name = "evalChatClient")
public ChatModel evalChatModel() {
// 设置baseurl和key
OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(baseUrl).apiKey(apiKey).build();
// 模型可选项
OpenAiChatOptions chatOptions = OpenAiChatOptions.builder().model("qwen-plus").temperature(0.8).build();
// 重试模版 retry
RetryTemplate retryTemplate = RetryTemplate.builder().maxAttempts(3) // 最多重试3次
.exponentialBackoff(1000, 2.0, 10000) // 初始1s,指数退避,最大10s
.retryOn(IOException.class) // 网络异常时重试
.build();
return OpenAiChatModel.builder()
.observationRegistry(ObservationRegistry.create())
.openAiApi(openAiApi)
.retryTemplate(retryTemplate)
.defaultOptions(chatOptions)
.build();
}
}
4_引入EVAL
在与大模型的正常交互流程中引入评估,使用一个 LLM 生成内容,再用另一个去做相关性评估。
java
@RestController
@RequestMapping
@Slf4j
public class RagEvaluationController {
@Resource
private SimpleVectorStore simpleVectorStore;
@Resource
private ChatClient baseChatClient;
@Resource(name = "evalChatClient")
private ChatModel evalChatModel;
@PostConstruct
public void init() {
log.info("start add data");
HashMap<String, Object> map = new HashMap<>();
map.put("year", 2025);
map.put("name", "王心凌");
List<Document> documents = List.of(
new Document("王心凌(Cyndi Wang),1982年9月5日出生于台湾省新竹县,祖籍山东省青岛市,华语流行乐女歌手、影视演员。"),
new Document("2022年,参加综艺节目《乘风破浪第三季》并夺得年度总冠军。2023年,凭借第13张专辑《BITE BACK》夺得台湾年度唱片销量女冠"),
new Document("2003年,王心凌发行首张专辑《Cyndi Begin》正式出道,并凭借偶像剧《西街少年》崭露头角。", Map.of("year", 2024)),
new Document("2004年,发行第2张专辑《爱你》,专辑同名歌曲《爱你》获得广泛关注;同年,主演的偶像剧《天国的嫁衣》夺得台湾年度收视冠军。", map));
simpleVectorStore.add(documents);
}
@GetMapping("/chat")
public String chat(@RequestParam(value = "query", defaultValue = "你好,请告诉我王心凌的身份信息") String query) {
// 基本会话响应
ChatResponse chatResponse = baseChatClient.prompt(query)
.call().chatResponse();
evaluateRelevancy(chatResponse, query);
return chatResponse.getResult().getOutput().getText();
}
/**
* 评估相关性
*
* @param chatResponse LLM 响应结果
* @param question 用户原始问题
*/
public void evaluateRelevancy(ChatResponse chatResponse, String question) {
if (ObjectUtils.isEmpty(chatResponse)) {
throw new RuntimeException("未知错误");
}
log.info("start eval");
EvaluationRequest evaluationRequest = new EvaluationRequest(
question,
chatResponse.getMetadata().get(RetrievalAugmentationAdvisor.DOCUMENT_CONTEXT),//从 RAG 流检索到的上下文
chatResponse.getResult().getOutput().getText()
);
log.info("eval request: {}", evaluationRequest);
// 创建相关性评估器
RelevancyEvaluator evaluator = new RelevancyEvaluator(ChatClient.builder(evalChatModel));
EvaluationResponse relevancyEvaluate = evaluator.evaluate(evaluationRequest);
boolean pass = relevancyEvaluate.isPass();
log.info("eval result: {}", pass);
}
}
5_结果
模型响应结果:

控制台验证相关性结果:

2 源码结构
1_Evaluator
Evaluator 是所有用于评估响应的统一接口:
方法名称 | 描述 |
---|---|
evaluate | 执行评估的核心逻辑 |
doGetSupportingData | 提取额外的上下文知识 |
2_EvaluationRequest
作用:封装评估请求相关内容
- String userText:用户原始输入文本;
- List dataList:额外的上下文知识(例如来自检索增强生成);
- String responseContent:AI 模型响应内容;
java
public class EvaluationRequest {
private final String userText;
private final List<Document> dataList;
private final String responseContent;
....
}
3_EvaluationResponse
作用:封装模型评估结果的标准响应。
- boolean pass:评估是否通过;
- float score:定义的内容得分(比如相关性得分);
- String feedback:自然语言形式提供评估结果的详细解释,辅助人工复核或调试;
Map<String, Object> metadata
:存储与评估相关的附加信息;
java
public class EvaluationResponse {
private final boolean pass;
private final float score;
private final String feedback;
private final Map<String, Object> metadata;
}
4_RelevancyEvaluator
相关性评估器,用于判断模型回答是否与用户问题及上下文相关。
ChatClient.Builder chatClientBuilder
:客户端的建造者;PromptTemplate promptTemplate
:相关性的提示词模版;
evaluate 方法的核心逻辑如下:
- 从 EvaluationRequest 提取响应内容和上下文数据;
- 使用 promptTemplate 渲染完整提示词;
- 通过 chatClientBuilder 构建聊天客户端并调用模型;
- 解析模型输出,判断是否为 "YES" 来决定评估结果;
java
public class RelevancyEvaluator implements Evaluator {
private static final PromptTemplate DEFAULT_PROMPT_TEMPLATE = new PromptTemplate("""
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:
""");
private final ChatClient.Builder chatClientBuilder;
private final PromptTemplate promptTemplate;
public RelevancyEvaluator(ChatClient.Builder chatClientBuilder) {
this(chatClientBuilder, null);
}
private RelevancyEvaluator(ChatClient.Builder chatClientBuilder, @Nullable PromptTemplate promptTemplate) {
Assert.notNull(chatClientBuilder, "chatClientBuilder cannot be null");
this.chatClientBuilder = chatClientBuilder;
this.promptTemplate = promptTemplate != null ? promptTemplate : DEFAULT_PROMPT_TEMPLATE;
}
@Override
public EvaluationResponse evaluate(EvaluationRequest evaluationRequest) {
var response = evaluationRequest.getResponseContent();
var context = doGetSupportingData(evaluationRequest);
var userMessage = this.promptTemplate
.render(Map.of("query", evaluationRequest.getUserText(), "response", response, "context", context));
String evaluationResponse = this.chatClientBuilder.build().prompt().user(userMessage).call().content();
boolean passing = false;
float score = 0;
if (evaluationResponse != null && evaluationResponse.toLowerCase().contains("yes")) {
passing = true;
score = 1;
}
return new EvaluationResponse(passing, score, "", Collections.emptyMap());
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private ChatClient.Builder chatClientBuilder;
private PromptTemplate promptTemplate;
private Builder() {
}
public Builder chatClientBuilder(ChatClient.Builder chatClientBuilder) {
this.chatClientBuilder = chatClientBuilder;
return this;
}
public Builder promptTemplate(PromptTemplate promptTemplate) {
this.promptTemplate = promptTemplate;
return this;
}
public RelevancyEvaluator build() {
return new RelevancyEvaluator(this.chatClientBuilder, this.promptTemplate);
}
}
}
除了相关性评估器外,还有 FactCheckingEvaluator,这款评估器的作用是根据提供的上下文评估 AI 生成内容的准确性(答案是否仅来自所给上下文,防幻觉)。
3 总结
主要场景:
- 开发和测试阶段:在集成测试中使用 FactCheckingEvaluator 验证 RAG 的质量。
- 批量质量检测:对一些历史对话进行离线评估。
- 系统监控:定期抽样评估生产环境中的对话质量,比如100次对话评估一次。
- 模型验证:当更换 AI 模型或调整 RAG 配置时,用于验证效果。
前瞻建议:
- 评测与生成用不同模型。文档也强调"选择最合适的评估模型,它不一定与生成模型相同"。这能显著降低自我偏倚。
- 高风险领域必须进行事实性幻觉定期评估。
评估优化器模式(实践):
- 把模型的输出当成"候选解",再用独立的"评估器(Evaluator)"来筛选、打分、优化、给出建议,直到得到最符合预期的结果。
- 这种模式在学术界叫 LLM-as-a-Judge,在工程实践里就是 评估 → 优化 → 再生成的闭环。
- 可以抽象为三步:
- 生成 (Generate):用业务模型给出候选答案。
- 评估 (Evaluate):用独立的评估器模型(Judge Model)检查答案的相关性、事实性、风格等。
- 优化 (Optimize):根据评估结果决定,是重试(retry with different prompt / 增加上下文)、重写(让模型改进答案)还是筛选(多个候选答案中选最优)。
评估模型的选择与成本控制:
- 分离评审与生成:评审模型可以更小、更快(如 minicheck、
gpt-4o-mini
、mistral-small
等),达到"快速 + 足够稳定"的平衡。 - 本地化优先:用 Ollama 跑评审模型,结合 Testcontainers,无需外网 即可在 CI 上跑评估(要注意 runner 的算力与镜像缓存)。
- 去随机:评估时把温度调低(0~0.2),避免判定波动。
- 双通道:对高风险样本,采用 双评审(两个不同评估模型),只要其中之一 fail 就拉红。
观测评估:
- 把评估跑过的 样本 ID、判定结果、原因、耗时、token 用量全纳入 Observability。
与 Advisors 协同:
- 评估应当覆盖顾问链(RAG、记忆、函数/工具调用等)之后的最终回答。
- 在评估用例里,使用与你生产一致的顾问链。
- 通过 metadata 读取顾问产生的上下文(例如检索文档),作为评估 dataList 输入。
常见坑位 & 规避策略
- 同模自评:生成与评估用同一模型,容易"互相偏倚"。分离评审模型。
- 上下文泄漏:没把 RAG 命中的文档传给评估器,事实核查会误判。把 dataList 填好。
- 波动大:评审模型温度过高。降温 + 固定提示。
- 阈值错配:统一阈值并不合理。为高风险样本单独拉高阈值。
- 观测缺失:没开 Observability,问题定位靠猜。接入 Actuator/Micrometer/OTel。