Spring AI 评估-优化器模式完整指南

大家好,今天想和大家分享的是关于 Spring AI 的"评估-优化器"(Evaluator Optimizer Pattern)模式的内容,并通过一个实际应用案例-智能电子邮件生成,构建能够自我完善的高质量 AI 应用。

在之前发布的文章中,已经介绍了Spring AI Workflow的几种主要模式,这里不再赘述。这篇文章中,我将主要介绍评估-优化器模式 (Evaluator-Optimizer Pattern) ,旨在解决 AI 应用开发过程中的挑战:如何确保高质量的输出。通过使用该模式,能够让我们开发的 AI 应用实现自我审视和改进,直至达到质量标准的工作流。

评估-优化器模式 (Evaluator-Optimizer Pattern)概述

简单举一个场景示例来便于大家理解:比如你正在撰写一份重要报告,首先你先完成初稿的撰写,然后交给一位你信得过的同事(一个编辑)进行审阅;编辑读完初稿后,指出了几处不足并给出了修改建议;接下来,你采纳反馈,修改草稿,甚至可能再让他看一遍。这个"撰写-审阅"的循环不断重复,直到报告被打磨得近乎完美。

"评估-优化器"模式做的就是这件事,只不过是让 AI 自己来完成。在这里,我们设定两个角色:

  • 生成器 (The Writer): 这个 Agent 负责针对给定任务,生成初始的响应或解决方案。
  • 评估器 (The Editor): 这个 Agent 的任务是根据一套预定义标准,严格地评估"生成器"的输出。然后,它会给出一个结论:输出是否合格,还是需要改进?

这张图说明了整个评估-优化的流程,包括以下几个步骤:

  1. 生成 (Generate): "生成器"产出初始方案。

  2. 评估 (Evaluate): "评估器"审查该方案。

  3. 决策 (Decide):

    • 如果"评估器"给出"通过" (PASS) 的结论,循环结束,并返回最终方案。
    • 如果"评估器"给出"需要改进" (NEEDS_IMPROVEMENT) 的结论,它的反馈意见将被传回给"生成器"。
  4. 重复 (Repeat): "生成器"采纳反馈意见,创建一个经过改进的新方案,然后流程重新开始。

什么时候该用"评估-优化器"工作流模式?

如果遇到以下情况,该模式可以作为首选方案:

  • 对生成的内容质量要求比较高: 比如生成法律文件、编写关键业务代码,或起草公司官方通告等任务。
  • 有明确的评估标准: 如果你能清晰地定义什么是"好"的输出 (例如,"代码必须是线程安全的",或"邮件的语气必须专业且富有同理心")。
  • 迭代优化能创造价值: 比如创意写作、解决复杂问题或进行精细化的翻译任务。

案例实践:智能邮件生成

接下来,我将通过构建一个智能邮件生成的案例项目来进一步说明该模式的正确使用方式,这个项目的功能是帮你撰写专业的电子邮件,无论是申请加薪、为迟到道歉并说明理由,还是跟进工作面试等场景,确保撰写的邮件内容看起来足够专业。

完整的智能邮件生成的流程包括以下5个步骤:

  1. 告诉它你想写哪种类型的邮件。
  2. "生成 (Generate)" Agent 创建一份草稿。
  3. "评估 (Evaluate)" Agent 审阅草稿,并指出需要修改的地方。
  4. 不断地改进优化,直到撰写的邮件草稿变得完美。
  5. 最终得到一封文辞优美、专业得体的邮件,可以直接发送。

下面是这个 Spring Boot 应用的目录结构:

以下是相关类文件的简要说明:

  • SpringAiEvaluatorOptimizerWorkflowApplication.java:启动 Spring Boot 应用的主入口。
  • EmailController.java:REST 控制器,通过 /api/email/generate url端点接收请求。
  • EmailService.java:服务层,作为控制器与核心工作流逻辑之间的桥梁。
  • EmailEvaluatorOptimizer.java:核心类,包含了"评估-优化器"模式的逻辑和系统提示词。
  • EmailRequest.java:DTO (数据传输对象),代表用户的邮件请求。
  • EmailResponse.java:DTO,代表返回给客户端的最终响应。
  • WriterResponse.java:Record 类型,表示 生成 (Generate)" Agent的输出,包括其思考过程和生成的邮件内容。
  • EditorResponse.java:Record 类型,表示"评估 (Evaluate)" Agent的结论和改进建议。
  • EmailResult.java:工作流内部使用的 DTO,用于在迭代优化过程中捕获最终结果和改进步骤。
  • application.yml:Spring AI 相关配置。
  • pom.xml:Maven 依赖配置。

第 1 步:添加 Maven 依赖、配置应用属性

关于这个项目的Maven依赖项以及Spring AI的相关配置,可参考Spring AI Chain工作流模式完整指南这篇文章。项目的技术栈主要使用的是JDK17、Spring boot3.5、Spring AI1.0以及Google的gemini-2.5-flash模型版本。

第 2 步:定义数据传输对象 (DTO)

在编写工作流逻辑之前,需要先定义好传输数据的结构,确保数据在从 API 接口、 LLM 调用之间流转以及最终返回给用户的整个过程中,始终保持简洁、结构化和类型安全。这里我使用的是 Java 的 Record 类型,因为它简洁、不可变特性。

定义用户输入 DTO:首先,让我们定义一个数据结构来捕获从 API 传来的用户输入,用于封装用户发送到 API 的所有信息,包括邮件类型、收件人、邮件大概要表达的内容以及邮件采用的语气(如正式、专业等)。

vbnet 复制代码
public record EmailRequest(
        String emailType,        // e.g., "job_followup", "raise_request"
        String recipientName,    // "John Smith"
        String mainMessage,      // "I want to follow up on my interview"
        String tonePreference    // "professional", "friendly", "formal"
) {}

定义"生成 (Generate)" Reponse DTO :这个 DTO 封装了 生成 (Generate) Agent的直接输出,包含了生成的邮件内容以及LLM在reasoning 过程中的解释理由。reasoning 字段则在调试和理解 LLM 在每一轮优化循环中的逻辑时至关重要。

arduino 复制代码
public record WriterResponse(
        String reasoning,
        String email
) {}

定义"评估 (Evaluate)" Response DTO :这个 Record 代表了"评估 (Evaluate)" Agent 给出的关键评判,并在草稿需要修改时提供具体、可行的反馈建议。 verdict 字段表示控制信号,告诉工作流是终止还是继续。suggestions 字段表示LLM提供的反馈建议,用于在下一轮迭代中做出改进。

arduino 复制代码
public record EditorResponse(
        String verdict,
        String suggestions
) {}

定义邮件结果 DTO:这个 Record 类是一个内部数据载体,负责将最终结果从复杂的工作流逻辑中传递回服务层。包含了审核通过的邮件内容以及整个优化过程的历史记录。

arduino 复制代码
import java.util.List;

public record EmailResult(
        String email,
        List<String> improvementSteps
) {}

定义最终 API 响应 DTO :这是我们的 API 最终返回给客户端的、面向用户的 DTO。finalEmail 表示最终邮件内容, improvementRounds 优化轮数, processLog 字段则记录了整个评估优化的过程。

arduino 复制代码
import java.util.List;

public record EmailResponse(
        String finalEmail,        // The polished email
        int improvementRounds,    // How many times we improved it
        List<String> processLog   // Think of it like a journal of changes
) {}

第 4 步:"评估-优化器"工作流的实现:EmailEvaluatorOptimizer类是实现智能邮件生成项目的整个"编写、审阅、优化"循环过程的核心部分,负责协调整个流程、管理提示、处理与 LLM 的通信,并运行递归式的改进过程。

arduino 复制代码
package com.autogenerater.workflow;

import com.autogenerater.dto.EditorResponse;
import com.autogenerater.dto.EmailResult;
import com.autogenerater.dto.WriterResponse;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
public class EmailEvaluatorOptimizer {

    private final ChatClient chatClient;
    private static final int MAX_ATTEMPTS = 4; // Don't try forever!

    // Instructions for our Writer AI
    private static final String WRITER_INSTRUCTIONS = getWriterInstructions();

    // Instructions for our Editor AI
    private static final String EDITOR_INSTRUCTIONS = getEditorInstructions();

    // Constructor - Spring will create this for us
    public EmailEvaluatorOptimizer(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    // Main method that starts the improvement process
    public EmailResult createPerfectEmail(String emailType, String recipientName, String mainMessage, String tonePreference) {
        // Start the recursive improvement loop
        return improveEmailRecursively(emailType, recipientName, mainMessage, tonePreference, "", new ArrayList<>(), 1);
    }

    // Recursive method that keeps improving the email until it's perfect
    private EmailResult improveEmailRecursively(String emailType, String recipientName, String mainMessage,
                                                String tonePreference, String previousFeedback,
                                                List<String> processLog, int attemptNumber) {

        // Safety check - don't go on forever!
        if (attemptNumber > MAX_ATTEMPTS) {
            processLog.add("Reached maximum attempts - returning best version");
            return new EmailResult("Maximum attempts reached", processLog);
        }

        System.out.println("--- Attempt " + attemptNumber + " ---");

        // Step 1: Writer AI creates/improves the email
        WriterResponse draft = writeEmail(emailType, recipientName, mainMessage, tonePreference, previousFeedback);
        processLog.add("Round " + attemptNumber + ": Created email draft");

        System.out.println("Writer's reasoning: " + draft.reasoning());
        System.out.println("Email draft:\n" + draft.email());

        // Step 2: Editor AI reviews the email
        EditorResponse review = reviewEmail(draft.email(), emailType, tonePreference);

        System.out.println("Editor's verdict: " + review.verdict());
        System.out.println("Editor's suggestions: " + review.suggestions());

        // Step 3: Are we done? If yes, return the perfect email!
        if ("GOOD_TO_SEND".equals(review.verdict())) {
            processLog.add("Editor approved: Email is ready to send!");
            return new EmailResult(draft.email(), processLog);
        }

        // Step 4: Not perfect yet, prepare feedback and try again
        String feedbackForNextRound = "Previous email:\n" + draft.email() +
                "\n\nEditor feedback: " + review.suggestions() +
                "\n\nPlease improve the email based on this feedback.";

        processLog.add("Editor feedback: " + review.suggestions());

        // Recursive call - try again with the feedback
        return improveEmailRecursively(emailType, recipientName, mainMessage, tonePreference,
                feedbackForNextRound, processLog, attemptNumber + 1);
    }

    // Method that asks Writer AI to create/improve email
    private WriterResponse writeEmail(String emailType, String recipientName, String mainMessage, String tonePreference, String feedback) {

        String prompt = String.format("""
                %s
                
                Email Details:
                - Type: %s
                - Recipient: %s  
                - Main message: %s
                - Tone: %s
                
                %s
                """, WRITER_INSTRUCTIONS, emailType, recipientName, mainMessage, tonePreference, feedback);

        return chatClient.prompt()
                .user(prompt)
                .call()
                .entity(WriterResponse.class);
    }

    // Method that asks Editor AI to review email
    private EditorResponse reviewEmail(String emailContent, String emailType, String tonePreference) {

        String prompt = String.format("""
                %s
                
                Email to review:
                %s
                
                Context: This is a %s email that should have a %s tone.
                """, EDITOR_INSTRUCTIONS, emailContent, emailType, tonePreference);

        return chatClient.prompt()
                .user(prompt)
                .call()
                .entity(EditorResponse.class);
    }

    private static String getWriterInstructions() {
        return """
                You are a professional email writing assistant. Your task is to compose a clear, complete, and professional email based on the user's instructions.
                
                -   Create a suitable subject line and a full email body that covers all the user's key points.
                -   Ensure the tone is appropriate for the situation.
                -   If you receive feedback for revision, apply it to the next draft.
                
                Return your response as a single-line JSON object:
                {"reasoning": "A brief summary of the email's content and tone.", "email": "The complete email content."}
                """;
    }

    private static String getEditorInstructions() {
        return """
                You are a meticulous editor focused on ensuring communications are powerful and concise.
                
                You must evaluate the draft against one primary rule, then a secondary one.
                
                **1. The 80-Word Rule (Primary Check):**
                   - The body of the email (from the greeting to the closing) **MUST be 80 words or less.**
                   - **If the draft is over 80 words, the verdict is ALWAYS "NEEDS_WORK".** Your feedback must state the word count and instruct the writer to shorten the email significantly.
                
                **2. The Clarity Rule (Secondary Check - only if word count is met):**
                   - If the email is 80 words or less, check if its core message is clear and easy to understand.
                
                Your Verdict:
                -   If the word count is over 80, it automatically **NEEDS_WORK**.
                -   If the word count is 80 or less AND the message is clear, it is **"GOOD_TO_SEND"**.
                
                Return your response as a single-line JSON object:
                {"verdict": "GOOD_TO_SEND or NEEDS_WORK", "suggestions": "If NEEDS_WORK, state the word count and the need for brevity. For example: 'The draft is 110 words, exceeding the 80-word limit. Please revise for conciseness.'"}
                """;
    }

}

工作原理:整个过程是一个简单的循环:

  1. 编写: Writer AI 创建一份完整的草稿。
  2. 审阅: Editor AI 检查草稿是否少于 80 个单词
  3. 优化: 如果草稿太长,"编辑"会将其退回,并附上缩短的指示。这个循环会不断重复,直到邮件被批准为止。

关键方法:

  • createPerfectEmail(...) : 启动整个工作流。
  • improveEmailRecursively(...) : 执行"编写/审阅"循环的逻辑,它包含一个 MAX_ATTEMPTS 安全阈值,即最大的循环迭代次数,以防无限循环。
  • getWriterInstructions() / getEditorInstructions() : 这两个私有方法中定义了前面设定的不同角色的系统提示词(System prompt)。

第 5 步:创建服务类

这里定义的EmailService 类是作为控制器和工作流逻辑层之间的衔接;通过调用 optimizer.createPerfectEmail(...) 方法。将生成邮件这个复杂的、迭代式的任务,委托给了专门的工作流类。

java 复制代码
package com.autogenerater.service;

import com.autogenerater.dto.EmailRequest;
import com.autogenerater.dto.EmailResponse;
import com.autogenerater.dto.EmailResult;
import com.autogenerater.workflow.EmailEvaluatorOptimizer;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class EmailService {

    private final EmailEvaluatorOptimizer optimizer;

    public EmailService(EmailEvaluatorOptimizer optimizer) {
        this.optimizer = optimizer;
    }

    public EmailResponse generateEmail(EmailRequest request) {
        try {
            // Use our Evaluator-Optimizer to create the perfect email
            EmailResult result = optimizer.createPerfectEmail(
                    request.emailType(),
                    request.recipientName(),
                    request.mainMessage(),
                    request.tonePreference()
            );

            // Convert to response format
            return new EmailResponse(
                    result.email(),
                    (result.improvementSteps().size() / 2) - 1,
                    result.improvementSteps()
            );

        } catch (Exception e) {
            // If something goes wrong, tell the user nicely
            return new EmailResponse(
                    "Sorry, we couldn't generate your email. Please try again.",
                    0,
                    List.of("Error occurred: " + e.getMessage())
            );
        }
    }
}

第 6 步:创建控制器

这一步,通过创建一个控制器EmailController,用于将POST /api/email/generateurl端点暴露出去,接收用户发送的restful 请求。

kotlin 复制代码
package com.autogenerater.controller;

import com.autogenerater.dto.EmailRequest;
import com.autogenerater.dto.EmailResponse;
import com.autogenerater.service.EmailService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/email")
public class EmailController {

    private final EmailService emailService;

    public EmailController(EmailService emailService) {
        this.emailService = emailService;
    }

    @PostMapping("/generate")
    public ResponseEntity<EmailResponse> generateEmail(@RequestBody EmailRequest request) {
        EmailResponse response = emailService.generateEmail(request);
        return ResponseEntity.ok(response);
    }
}

第 7 步:应用入口点

最后定义启动 Spring Boot 应用的主类, 用于Spring boot初始化所有组件,并启动内嵌的Tomcat容器。

typescript 复制代码
package com.autogenerater;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringAiEvaluatorOptimizerWorkflowApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringAiEvaluatorOptimizerWorkflowApplication.class, args);
    }
}

"评估-优化器"工作流模式的完整流程步骤说明

在介绍完上述的关键代码后,我使用浏览器的http调试工具来说明一下整个执行过程:从提出邮件撰写需求,到最终收到一份经过编辑精心打磨的最终邮件版本。

如图中所示:应用启动后,通过向 http://localhost:8080/api/email/generate 发送一个 POST 请求来测试"评估-优化器"工作流,请求体是一个包含邮件细节的 JSON。

  1. 请求发起: 用户向 /api/email/generate 端点发送一个 POST 请求。请求体是一个 JSON 对象,包含了邮件的详细信息:类型、收件人、核心信息以及期望的语气。

  2. 控制器接管: EmailController 接收到这个请求。Spring 框架会自动将 JSON 数据转换为一个 EmailRequest DTO。控制器随即把这个 DTO 传递给 EmailService

  3. 服务委托: EmailService 充当一个衔接点。它接收 EmailRequest,然后调用 EmailEvaluatorOptimizer 工作流的 createPerfectEmail 方法,将核心的"编写、审阅、优化" 逻辑全权委托给它处理。

  4. 生成初稿 ("生成"环节): EmailEvaluatorOptimizer 工作流开始第一次尝试。它将用户的请求细节,连同专门的"Writer AI"提示,一同发送给 LLM,输出一份内容完整的邮件初稿。

  5. 严格审阅 ("评估"环节): 工作流立刻将这份初稿再次发送给 LLM,但这次附带的是更严格的"编辑"提示指令。Editor AI 的首要任务是检查草稿是否违反了80 词规则这里之所以使用"80 词规则"是为了强制至少触发一轮反馈,确保"评估器"在第一次尝试时就给出"需要修改"的结论,此处作为演示设定)。

  6. 决策与优化循环 ("优化"环节):

    • 如果邮件草稿超过 80 词, 编辑会返回一个 NEEDS_WORK 值,并附上缩短邮件的反馈。工作流随即进入循环,将草稿和新的反馈意见再次发给Writer AI,进行新一轮尝试。这个循环会一直持续,直到产出的草稿满足字数要求。
    • 如果草稿在 80 词或以内, 编辑会返回 GOOD_TO_SEND 值,循环终止。
  7. 响应交付: 最终,审核通过的 EmailResult 对象,从工作流被返回到服务层。服务层再将其打包成最终的 EmailResponse,其中包含了最终的邮件内容和整个优化过程的详细日志。

最终的输出结果

如图中所示,http请求的响应结果是一个完整的 EmailResponse JSON 对象,其中包含了 finalEmail (最终邮件)、improvementRounds (优化轮数) 以及详细的 processLog (处理日志)。

写在最后

在这篇文章中,我通过一个智能邮件生成的简单示例演示了评估-优化器模式的整个流程,通过这种关注点分离的设计,可以输出比使用一个单一、庞杂的提示更可靠、更高质量的结果。而且,我们也可以在不同的场景中使用它,例如:

  • 图像提示词撰写: 先写一个提示词prompt,然后不断优化它以获得更好的视觉效果。
  • 代码生成: 先生成一个函数,然后审查它是否存在 bug 或风格问题。
  • 项目规划: 先制定一个计划,然后检查是否存在逻辑或预算上的问题。

需要注意的是,我们在使用该模式进行AI应用开发时,需要考虑成本和效率的权衡,因为采用迭代反馈循环的流程势必会导致LLM请求消耗的token数量较多,从而导致成本增加。在这里给出一个小的建议,即我们可以在流程的不同阶段使用不同的模型来达到节省成本的目的。例如:在智能邮件生成这个示例中,使用一个更快、更便宜的模型 (如 GPT-3.5-Turbo) 来生成初稿,然后用一个更强大、"更聪明"的模型 (如 Gemini-2.5-flash) 来进行关键的评估。

好了,今天的分享就这么多,有问题欢迎在评论区讨论,后续会继续分享Spring AI系列的相关教程~

相关推荐
剁椒豆腐脑14 分钟前
阶段二JavaSE进阶阶段之设计模式&继承 2.2
java·设计模式·跳槽·学习方法·改行学it
扫地僧98529 分钟前
免费1000套编程教学视频资料视频(涉及Java、python、C C++、R语言、PHP C# HTML GO)
java·c++·音视频
青春:一叶知秋32 分钟前
【C++开发】CMake构建工具
java·开发语言·c++
77tian43 分钟前
Java Collections工具类:高效集合操作
java·开发语言·windows·microsoft·list
2401_826097621 小时前
JavaEE-Mybatis初阶
java·java-ee·mybatis
KIDAKN1 小时前
JavaEE->多线程2
java·算法·java-ee
chentao1061 小时前
Spring-Boot健康检查的正确打开方式
spring boot
wu_android1 小时前
Java匿名内部类访问全局变量和局部变量的注意事项
java·开发语言
喵手1 小时前
领导让我同事封装一个自定义工具类?结果她说要三小时...
java·后端·java ee
牛马baby1 小时前
synchronized 做了哪些优化?
java·高并发·并发编程·synchronized·锁升级·面试资料·程序员涨薪跳槽