
1. 概述
借助Spring AI,我们可以将大型语言模型(LLM)集成到 Spring 应用程序中。例如,这有助于处理维基页面、电子邮件或聊天消息等非结构化数据,通过提取结构化信息并将其存储在数据库中。然而,LLM 的响应并非确定性的,因此无法通过应用程序代码进行测试或处理。为LLM响应实现某种质量把关的最佳方式,是再次使用LLM进行评估。这种模式被称为“LLM 作为评审员”(LLM-as-a-Judge)。
在本教程中,我们将学习如何在 Spring AI 中实现“LLM 作为评审员”模式。
2. Maven 依赖项
我们在 pom.xml 中添加 Spring AI OpenAI starter:
org.springframework.ai spring-ai-starter-model-openai ${spring-ai.version}
spring-ai-starter-model-openai 的最新版本可在 Maven Central 上获取。
3. “LLM 作为评审员”模式
“LLM 作为评审员”模式类似于代码审查。开发人员(生成者)编写代码,资深工程师(评审员)对其进行审查并给出结构化的反馈。如果评审员认为代码不符合要求,开发人员需要进行改进,随后评审员需要再次审查该代码。
应用于 LLM 时,流程如下:
-
生成器针对用户的问题生成初步答案
-
评审员根据评分标准评估该答案,并返回数值评分和文字反馈
-
如果评分过低,生成器将收到原始问题及评审员的点评,并生成优化后的答案。
-
该过程将循环进行,直至答案符合要求。为避免在无法达到所需质量时陷入无休止的循环,需要限制循环次数。
该模式无需对模型进行微调、无需人工干预、也无需修改任何调用代码,即可提升输出质量。
当质量较差的回答会产生有问题的实际后果时,该模式的价值尤为突出,例如客户支持机器人——若对账单问题给出含糊的回答,将直接损害用户信任。在这种情况下,该模式在模型与用户之间充当了一道无形的质量把关机制。
3.1. 为何递归顾问是理想选择
Spring AI中的顾问是拦截器,用于封装ChatClient的请求/响应循环。CallAroundAdvisor可以在请求到达模型之前对其进行检查,并在响应到达调用者之前对其进行修改,但每次调用仅会发起一次模型调用。
递归顾问(Recursive Advisors)则更进一步:它们持有内部的ChatClient引用,并能从顾问自身内部发起额外的模型调用。
我们可以将这一能力应用于一个具体问题:将第二次模型调用用作“裁判”,而不仅仅是扩展。“生成-判断-优化”循环由此成为一个对调用者完全透明的、独立的组件。
调用方只需像往常一样调用 chatClient.prompt().user(“…”).call().content()。评判器的逻辑对调用方完全不可见。
4. 实现“LLM 作为评审员”的顾问
让我们分三个部分来构建该功能:用于裁决结果的值对象、用于评判器的提示模板,以及顾问本身。
4.1. 评判记录
评审员会给出结构化的评判结果。我们将它建模为一个简单的 Java 记录:
public record Verdict(double score, String feedback) {}
在我们的示例中,评分值介于 0.0(差)和 1.0(优秀)之间。反馈部分说明了哪些内容缺失或薄弱,这也是我们在精炼过程中将反馈给生成器的内容。
4.2. 评审员提示模板
评审员需要一个清晰、结构化的系统提示。为此,让我们将其定义为顾问类内部的一个常量:
private static final String JUDGE_SYSTEM_PROMPT = """
You are a strict quality evaluator for AI-generated answers.
Given a user question and an AI-generated answer, rate the answer quality.
Use this rubric:
- 1.0: Complete, accurate, and clearly explained
- 0.7: Mostly correct but missing details or clarity
- 0.4: Partially correct or overly vague
- 0.0: Incorrect, irrelevant, or harmful
Respond ONLY with a valid JSON object. Do not add any explanation outside the JSON.
Format: {"score": <0.0 to 1.0>, "feedback": ""}
""";
声明明确的评分标准和严格的JSON输出格式非常重要,这样才能从评委模型中获得可解析且一致的答案。
4.3. 顾问类及其依赖项
递归顾问的关键设计决策在于,它持有自己的ChatClient实例,以便独立于顾问链进行额外的模型调用。我们还注入了可配置的质量阈值:
public class LlmJudgeAdvisor implements CallAdvisor {
private final ChatClient judgeClient;
private final double scoreThreshold;
private final int maxRefinements;
public LlmJudgeAdvisor(
ChatClient judgeClient,
double scoreThreshold,
int maxRefinements
) {
this.judgeClient = judgeClient;
this.scoreThreshold = scoreThreshold;
this.maxRefinements = maxRefinements;
}
@Override
public int getOrder() {
return 0;
}
@Override
public String getName() {
return "LlmJudgeAdvisor";
}
// ...
}
ChatClient.Builder接收与应用程序其余部分相同的自动配置模型设置。我们可以在此处接入一个不同的、专门的评判模型。Spring AI官方文档特别建议这样做,以避免模型对自己输出的评判过于宽松。
4.4. 评估和优化响应
adviseCall() 方法是顾问链调用的唯一入口点。我们并非仅转发一次请求,而是调用 chain.copy(this).nextCall(request),这会创建一个包含我们顾问的子链。正是这一点使得循环能够在多次尝试中重新评估和重新生成响应:
@Override
public ChatClientResponse adviseCall(ChatClientRequest request, CallAdvisorChain chain) {
for (int attempt = 1; attempt <= maxRefinements + 1; attempt++) {
ChatClientResponse response = chain.copy(this).nextCall(request);
if (attempt > maxRefinements) {
return response;
}
Verdict verdict = evaluate(request, response);
if (verdict.score() >= scoreThreshold) {
return response;
}
request = addFeedback(request, verdict.feedback());
}
return chain.copy(this).nextCall(request);
}
该循环需要明确的上限以避免无限递归。在最后一次允许的尝试中,该方法会立即返回响应,跳过评估步骤。对于无法付诸行动的答案,进行评分毫无意义。否则,如果评分达到scoreThreshold,则提前返回;若未达到,则将评判模型的反馈补充到请求中,并继续进行下一次迭代。
4.5. 答案评估
evaluate() 方法将原始问题和生成的答案发送给评判模型。我们可以使用 .entity(…) 将响应 JSON 直接渲染到记录中,无需任何手动 JSON 解析:
private Verdict evaluate(ChatClientRequest request, ChatClientResponse response) {
String question = request.prompt().getUserMessage().getText();
String answer = response.chatResponse().getResult().getOutput().getText();
return judgeClient.prompt()
.system(JUDGE_SYSTEM_PROMPT)
.user("Question: " + question + "\n\nAnswer: " + answer)
.call()
.entity(Verdict.class);
}
Spring AI会将从Verdict记录派生的JSON模式注入模型调用中,并处理反序列化。这消除了对JSON解析进行try-catch处理的必要性,使评估代码能够专注于提示语本身,而非格式处理。
4.6. 通过反馈增强请求
当评分未达标时,我们不会使用原始提示词从头开始。相反,我们对其进行增强,即在用户消息后附加评委的点评,以便模型在下次尝试时拥有完整的上下文。addFeedback() 辅助方法通过 ChatClientRequest.mutate() 实现此功能:
private ChatClientRequest addFeedback(ChatClientRequest original, String feedback) {
Prompt augmented = original.prompt()
.augmentUserMessage(msg -> msg.mutate()
.text(msg.getText()
+ "\n\nYour previous answer was insufficient. Feedback: " + feedback
+ "\nPlease provide an improved answer.")
.build());
return original.mutate().prompt(augmented).build();
}
augmentUserMessage() 方法会返回用户消息的可变副本,同时不影响请求的其他部分。系统提示、工具定义和对话历史记录均保持不变。更新后的ChatClientRequest会被传递到下一个循环迭代中。
5. 配置应用程序
让我们在同一个 @Configuration 类中声明这两个 Bean。LlmJudgeAdvisor Bean 会接收其专属的 ChatClient 实例(该实例由相同的自动配置 ChatClient.Builder 构建而成),以及来自 application.properties 的质量阈值:
@Configuration
public class ChatConfig {
@Bean
public ChatClient chatClient(
ChatClient.Builder builder,
LlmJudgeAdvisor judgeAdvisor
) {
return builder
.defaultAdvisors(judgeAdvisor)
.build();
}
@Bean
public LlmJudgeAdvisor llmJudgeAdvisor(
ChatClient.Builder builder,
@Value("${judge.score-threshold:0.7}") double scoreThreshold,
@Value("${judge.max-refinements:2}") int maxRefinements
) {
return new LlmJudgeAdvisor(
builder.build(),
scoreThreshold,
maxRefinements
);
}
}
然后,我们可以在 application.properties 中设置这些阈值:
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model=gpt-4o-mini
judge.score-threshold=0.75
judge.max-refinements=2
评分阈值0.75意味着,只有当评判器对答案的评分达到至少75%的质量时,我们才会接受该答案。将最大优化次数设置为2,可将递归循环限制在两次优化尝试内,从而防止API使用量失控。
6. 测试顾问
让我们实现一个带有单个模拟ChatModel的@SpringBootTest。生成器和评判器都使用相同的底层实例,因此一个模拟对象即可覆盖整个调用序列。无需API密钥。
该测试模拟了四个符合预期流程的连续chatModel 响应:
@SpringBootTest
class LlmJudgeAdvisorTest {
@MockitoBean
ChatModel chatModel;
@Autowired
ChatClient chatClient;
@Test
void givenLowQualityAnswer_whenAdvisorRuns_thenAnswerIsRefined() {
when(chatModel.call(any(Prompt.class)))
.thenReturn(buildChatResponse("It runs Java."))
.thenReturn(buildChatResponse("{\"score\": 0.3, \"feedback\": \"Too vague.\"}"))
.thenReturn(buildChatResponse(
"The JVM executes Java bytecode, manages memory, and enables platform independence."))
.thenReturn(buildChatResponse("{\"score\": 0.9, \"feedback\": \"Complete and accurate.\"}"));
String result = chatClient.prompt()
.user("Explain what a JVM is.")
.call()
.content();
assertThat(result).contains("bytecode");
}
private ChatResponse buildChatResponse(String content) {
return new ChatResponse(List.of(new Generation(new AssistantMessage(content))));
}
}
这四个模拟响应直接对应“生成-评估-优化-评估”循环:第一次调用是生成器的弱响应,第二次调用是评判器的低评分,第三次调用是优化后的生成器响应,第四次调用是评判器的通过评分。带有顾问的chatClient是真实的,仅底层模型被模拟,这意味着完整的顾问逻辑将像在生产环境中一样运行。
7. 结论
在本文中,我们利用递归的 CallAdvisor 在 Spring AI 中实现了“LLM-as-a-Judge”模式。我们看到“生成-评估-优化”循环如何自然地映射到顾问模型上:首次模型调用生成答案,Spring AI 的结构化输出将评委的响应映射为带类型的 Verdict,而该循环会根据反馈不断完善原始请求,直到得分足够高或达到尝试次数上限为止。
最终形成了一个可重用的组件,它能自动提升输出质量,并且对任何使用 ChatClient 的调用者而言完全透明。
作者:Ralf Ueberfuhr
审阅人:David Martinez