在Spring AI中使用递归顾问构建LLM评判器
1. 概述
借助 Spring AI,我们可以将 LLM 集成到 Spring 应用中。这对于处理非结构化数据(如维基页面、电子邮件或聊天消息)非常有用,例如通过提取结构化信息并将其存储到数据库中。然而,LLM 的响应具有不确定性,因此既无法测试,也无法被应用代码轻松处理。实现 LLM 响应质量门控的最佳方式是再次使用 LLM 进行评估。这种模式被称为 LLM-as-a-Judge。
在本教程中,我们将学习如何在 Spring AI 中实现 LLM-as-a-Judge 模式。
2. Maven 依赖
我们在 pom.xml 中添加 Spring AI OpenAI 启动器:
最新版本的 spring-ai-starter-model-openai 可在 Maven Central 上获取。
3. LLM-as-a-Judge 模式
LLM-as-a-Judge 模式类似于代码评审。开发者(生成器)编写代码,高级工程师(评审者)进行审查并给出结构化反馈。如果评审者认为代码不符合要求,开发者需要改进,然后评审者再次审查。
应用到 LLM 上,流程如下:
- 生成器 对用户问题生成初始回答。
- 评审者 根据评分规则对该回答进行评估,返回数值评分和书面反馈。
- 如果评分过低,生成器将收到原始问题以及评审者的批评,并生成改进后的回答。
- 这个过程会重复,直到回答满足要求。为避免因始终无法达到所需质量而无限循环,最佳实践是限制循环次数。
这种模式无需微调模型、无需人工介入、无需修改调用代码,即可提升输出质量。
该模式在弱回答会造成实际影响的场景中价值最大,例如客服机器人——若对账单问题的回答含糊不清,会直接损害信任。在这种情况下,该模式充当了模型和用户之间无形的质量门控。
3.1. 为什么递归顾问是合适的选择
Spring AI 中的 Advisors(顾问) 是拦截器,包裹了 ChatClient 的请求/响应周期。CallAroundAdvisor 可以在请求到达模型前进行检查,并在响应返回调用方前修改响应,但每次调用只执行一次模型调用。
递归顾问(Recursive Advisors) 更进一步:它们持有一个内部的 ChatClient 引用,可以在顾问自身内部发起额外的模型调用。
我们可以将此能力应用于一个特定问题:将第二个模型调用用作评审者,而不仅仅是扩展。 生成-评审-改进循环成为一个自包含的组件,对调用方完全透明。
调用方像往常一样调用 chatClient.prompt().user("...").call().content(),评审逻辑完全不可见。
4. 实现 LLM-as-a-Judge 顾问
我们将该功能分为三部分构建:用于判决的值对象、评审者提示模板,以及顾问本身。
4.1. 判决记录
评审者返回一个结构化的判决。我们将其建模为一个简单的 Java 记录:
在本示例中,score 的取值范围是 0.0(差)到 1.0(优秀)。feedback 解释缺失或薄弱之处,在改进阶段会反馈给生成器。
4.2. 评审者提示模板
评审者需要一个清晰、结构化的系统提示。我们将其定义为顾问内部的常量:
声明明确的评分规则和严格的 JSON 输出格式,对于从评审模型获取可解析且一致的答案至关重要。
4.3. 顾问类及其依赖
递归顾问的关键设计决策是:它持有自己的 ChatClient 实例,以便独立于顾问链发起额外的模型调用。 我们还注入了可配置的质量阈值:
ChatClient.Builder 接收与应用其余部分相同的自动配置模型设置。我们也可以在此处接入一个不同的、专门的评审模型。官方 Spring AI 文档 特别建议这样做,以避免模型对自己的输出评价过于宽松。
4.4. 评估和改进响应
adviseCall() 是顾问链调用的唯一入口点。我们不直接转发请求一次,而是调用 chain.copy(this).nextCall(request),这会创建一个包含我们顾问的子链。这使得循环能够跨多次尝试重新评估和重新生成:
循环需要显式上界以避免无限制递归。在最后一次允许的尝试中,方法直接返回响应,跳过评估。如果评分满足 scoreThreshold,则提前返回。否则,使用评审者的反馈增强请求,并继续下一次迭代。
4.5. 评估回答
evaluate() 方法将原始问题和生成的回答发送给评审模型。 我们可以使用 .entity(...) 直接将响应 JSON 渲染到我们的记录中,无需手动解析 JSON:
Spring AI 会将从 Verdict 记录派生的 JSON schema 注入到模型调用中,并处理反序列化。这消除了任何 try-catch 解析 JSON 的需要,使评估代码专注于提示,而非格式处理。
4.6. 使用反馈增强请求
当评分不达标时,我们不会使用原始提示重新开始,而是进行增强——即将评审者的批评附加到用户消息中,以便模型在下次尝试时拥有完整上下文。addFeedback() 辅助方法使用 ChatClientRequest.mutate() 实现:
augmentUserMessage() 为我们提供了用户消息的可变副本,而不影响请求的其余部分。 系统提示、工具定义和对话历史均保持不变。更新后的 ChatClientRequest 被传递到下一个循环迭代。
5. 配置应用程序
我们在一个 @Configuration 类中声明两个 Bean。LlmJudgeAdvisor Bean 接收自己的 ChatClient 实例(从相同的自动配置 ChatClient.Builder 构建),以及来自 application.properties 的质量阈值:
然后在 application.properties 中设置阈值:
评分阈值设为 0.75 意味着评审者必须对回答给出至少 75% 的质量评分,我们才接受。将最大改进次数设为 2 限制递归循环最多进行两次改进尝试,防止 API 使用失控。
6. 测试顾问
我们实现一个使用单一模拟 ChatModel 的 @SpringBootTest。 生成器和评审者使用相同的底层实例,因此一个模拟即可覆盖整个调用序列。无需 API 密钥。
测试模拟了四个连续的 chatModel 响应,匹配预期的流程:
四个模拟响应直接对应生成-评估-改进-评估循环:第一个是生成器的弱响应,第二个是评审者的低分判决,第三个是生成器的改进响应,第四个是评审者的通过判决。带有顾问的 chatClient 是真实的,只有底层的模型是模拟的。这意味着完整的顾问逻辑会像在生产环境中一样运行。
7. 结论
在本文中,我们使用递归 CallAdvisor 在 Spring AI 中实现了 LLM-as-a-Judge 模式。我们看到了生成-评估-改进循环如何自然地映射到顾问模型上:第一个模型调用产生回答,Spring AI 的结构化输出将评审者的响应映射为类型化的 Verdict,循环使用反馈增强原始请求,直到评分足够或达到尝试上限。
结果是一个可重用的组件,能自动提升输出质量,并且对任何使用 ChatClient 的调用方完全透明。
本教程的代码可在 GitHub 上获取。