練習 - 使用 Spring AI 實作 Evaluator-Optimizer 代理程式模式
在此單元中,您會擴充RAG應用程式,以示範 Evaluator-Optimizer Agent 模式。 此模式會使用多個 AI 代理程式,反覆產生、評估和精簡內容。 您可以使用此模式,從部落格文章產生和精簡內容。
實現 Evaluator-Optimizer 代理程式模式 用於部落格文章生成
在此練習中,您會實作 Evaluator-Optimizer 代理程式模式,以改善產生的內容。 在此設計中,一個 AI 代理程式 - 寫入 器 - 會產生初始草稿 - 例如部落格文章。 另一個代理程式 - 評估工具 - 檢閱並提供具體可行的回饋。 寫入器會根據意見反應來精簡草稿,而程式會重複執行,直到核准內容或達到反覆運算次數上限為止。
設定環境變數
在此練習中,您需要先前練習中的一些環境變數。 如果您使用相同的Bash視窗,這些變數仍應存在。 如果變數已無法使用,請使用下列命令來重新建立它們。 請務必將 <...>
佔位元取代為您自己的值,並使用您先前使用的相同值。
export RESOURCE_GROUP=<resource-group>
export DB_SERVER_NAME=<server-name>
export OPENAI_RESOURCE_NAME=OpenAISpringAI
export AZURE_OPENAI_ENDPOINT=$(az cognitiveservices account show \
--resource-group $RESOURCE_GROUP \
--name $OPENAI_RESOURCE_NAME \
--query "properties.endpoint" \
--output tsv \
| tr -d '\r')
export AZURE_OPENAI_API_KEY=$(az cognitiveservices account keys list \
--resource-group $RESOURCE_GROUP \
--name $OPENAI_RESOURCE_NAME \
--query "key1" \
--output tsv \
| tr -d '\r')
export PGHOST=$(az postgres flexible-server show \
--resource-group $RESOURCE_GROUP \
--name $DB_SERVER_NAME \
--query fullyQualifiedDomainName \
--output tsv \
| tr -d '\r')
建立 BlogWriterService
在 服務 目錄中,建立名為 BlogWriterService.java 的新檔案,並新增下列程序代碼:
package com.example.springaiapp.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* This service demonstrates the Evaluator-Optimizer agent pattern using Spring AI.
*
* The pattern involves multiple AI agents working together to iteratively improve content:
* 1. Writer agent - Creates the initial draft and refines based on feedback
* 2. Editor agent - Evaluates the draft and provides actionable feedback
*
* This iterative refinement continues until the content is approved or reaches max iterations.
*/
@Service
public class BlogWriterService {
private static final Logger logger = LoggerFactory.getLogger(BlogWriterService.class);
private static final int MAX_ITERATIONS = 3; // Maximum number of refinement iterations
private final ChatClient chatClient;
/**
* Initialize the service with a ChatClient that has SimpleLoggerAdvisor.
*
* The SimpleLoggerAdvisor automatically logs all AI interactions (prompts and responses)
* when the application's logging level is set to DEBUG for the advisor package.
*
* @param chatClientBuilder Builder for creating a configured ChatClient
*/
public BlogWriterService(ChatClient.Builder chatClientBuilder) {
// Add SimpleLoggerAdvisor to log requests and responses for debugging
this.chatClient = chatClientBuilder
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
logger.info("BlogWriterService initialized with ChatClient and SimpleLoggerAdvisor");
}
/**
* Generates a concise blog post (max 10 sentences) using the Evaluator-Optimizer agent pattern.
*
* The method uses multiple AI agents to:
* 1. Generate an initial draft
* 2. Evaluate the draft for quality and brevity
* 3. Provide feedback for improvement
* 4. Refine the draft based on feedback
* 5. Repeat until approved or max iterations reached
*
* This method ensures at least one feedback-improvement cycle occurs to demonstrate
* the full evaluator-optimizer pattern in action, regardless of initial draft quality.
*
* @param topic The blog post topic
* @return A BlogGenerationResult containing the content and metadata
*/
public BlogGenerationResult generateBlogPostWithMetadata(String topic) {
logger.info("Starting blog generation with metadata for topic: {}", topic);
BlogGenerationResult result = new BlogGenerationResult();
result.setModelName("Azure OpenAI");
// PHASE 1: WRITER AGENT
// Prompt the Writer agent to generate the initial blog draft
String initialPrompt = String.format("""
You are a professional blog writer. Write a well-structured, engaging blog post about "%s".
The post should have a clear introduction, body paragraphs, and conclusion.
Include relevant examples and maintain a conversational yet professional tone.
IMPORTANT FORMATTING REQUIREMENTS:
1. Format as plain text only (no Markdown, HTML, or special formatting)
2. Use simple ASCII characters only
3. For the title, simply put it on the first line and use ALL CAPS instead of "#" symbols
4. Separate paragraphs with blank lines
5. The blog post must be concise and contain NO MORE THAN 10 SENTENCES total.
""", topic);
// Using Spring AI's fluent API to send the prompt and get the response
logger.info("Sending initial draft generation prompt to AI model");
String draft = chatClient.prompt()
.user(initialPrompt)
.call()
.content();
// Estimate token usage as we can't directly access it
estimateTokenUsage(result, initialPrompt, draft);
logger.info("Initial draft successfully generated for topic: {}", topic);
// PHASE 2: EVALUATION & REFINEMENT LOOP
// Setup for the iterative improvement process
boolean approved = false;
int iteration = 1;
boolean forceFirstIteration = true; // Force at least one feedback cycle to demonstrate the pattern
// Continue until we reach max iterations or get approval (but always do at least one iteration)
while ((!approved && iteration <= MAX_ITERATIONS) || forceFirstIteration) {
logger.info("Starting iteration {} of blog refinement", iteration);
// PHASE 2A: EDITOR AGENT
// Prompt the Editor agent to evaluate the current draft
String evalPrompt = String.format("""
You are a critical blog editor with extremely high standards. Evaluate the following blog draft and respond with either:
PASS - if the draft is exceptional, well-written, engaging, and complete
NEEDS_IMPROVEMENT - followed by specific, actionable feedback on what to improve
Focus on:
- Clarity and flow of ideas
- Engagement and reader interest
- Professional yet conversational tone
- Structure and organization
- Strict adherence to the 10-sentence maximum length requirement
IMPORTANT EVALUATION RULES:
1. The blog MUST have no more than 10 sentences total. Count the sentences carefully.
2. For the first iteration, ALWAYS respond with NEEDS_IMPROVEMENT regardless of quality.
3. Be extremely thorough in your evaluation and provide detailed feedback.
4. If the draft exceeds 10 sentences, it must receive a NEEDS_IMPROVEMENT rating.
5. Even well-written drafts should receive suggestions for improvement in early iterations.
Draft:
%s
""", draft);
// Send the evaluation prompt to the AI model
logger.info("Sending draft for editorial evaluation (iteration: {})", iteration);
String evaluation = chatClient.prompt()
.user(evalPrompt)
.call()
.content();
// After first iteration, remove the force flag
if (forceFirstIteration) {
forceFirstIteration = false;
}
estimateTokenUsage(result, evalPrompt, evaluation);
// Check if the Editor agent approves the draft
if (evaluation.toUpperCase().contains("PASS") && iteration > 1) { // Only allow PASS after first iteration
// Draft is approved, exit the loop
approved = true;
logger.info("Draft approved by editor on iteration {}", iteration);
} else {
// Draft needs improvement, extract the specific feedback
String feedback = extractFeedback(evaluation);
logger.info("Editor feedback received (iteration {}): {}", iteration, feedback);
result.addEditorFeedback(feedback);
// PHASE 2B: WRITER AGENT (REFINEMENT)
// Prompt the Writer agent to refine the draft based on the feedback
String refinePrompt = String.format("""
You are a blog writer. Improve the following blog draft based on this editorial feedback:
Feedback: %s
Current Draft:
%s
IMPORTANT REQUIREMENTS:
1. The final blog post MUST NOT exceed 10 sentences total.
2. Maintain a clear introduction, body, and conclusion structure.
3. Keep formatting as plain text only (NO Markdown, HTML, or special formatting)
4. For the title, use ALL CAPS instead of any special formatting
5. Separate paragraphs with blank lines
6. Use only simple ASCII characters
7. Provide the complete improved version while addressing the feedback.
8. Count your sentences carefully before submitting.
""", feedback, draft);
// Send the refinement prompt to the AI model
logger.info("Requesting draft revision based on feedback (iteration: {})", iteration);
String revisedDraft = chatClient.prompt()
.user(refinePrompt)
.call()
.content();
estimateTokenUsage(result, refinePrompt, revisedDraft);
draft = revisedDraft;
logger.info("Revised draft received for iteration {}", iteration);
}
iteration++;
}
// PHASE 3: FINALIZATION
// Set final result properties
result.setContent(draft);
result.setApproved(approved);
result.setIterations(iteration - 1);
if (!approved) {
logger.warn("Maximum iterations ({}) reached without editor approval", MAX_ITERATIONS);
} else {
logger.info("Blog post generation completed successfully for topic: {}", topic);
}
return result;
}
/**
* Helper method to extract actionable feedback from the Editor agent's evaluation.
* This extracts the text after "NEEDS_IMPROVEMENT" to get just the feedback portion.
*
* @param evaluation The full evaluation text from the Editor agent
* @return Just the actionable feedback portion
*/
private String extractFeedback(String evaluation) {
if (evaluation == null) return "";
int idx = evaluation.toUpperCase().indexOf("NEEDS_IMPROVEMENT");
if (idx != -1) {
// Return text after "NEEDS_IMPROVEMENT"
return evaluation.substring(idx + "NEEDS_IMPROVEMENT".length()).trim();
}
return evaluation;
}
/**
* Helper method to estimate token usage as we can't directly access it
* This is a rough estimation: approximately 4 characters per token
*/
private void estimateTokenUsage(BlogGenerationResult result, String prompt, String response) {
try {
// Very rough estimation: ~4 characters per token
int estimatedPromptTokens = prompt.length() / 4;
int estimatedCompletionTokens = response.length() / 4;
result.addPromptTokens(estimatedPromptTokens);
result.addCompletionTokens(estimatedCompletionTokens);
logger.debug("Estimated token usage: prompt={}, completion={}, total={}",
estimatedPromptTokens, estimatedCompletionTokens,
estimatedPromptTokens + estimatedCompletionTokens);
} catch (Exception e) {
logger.warn("Failed to estimate token usage", e);
}
}
/**
* Class to hold blog generation result, including the content and metadata.
*/
public static class BlogGenerationResult {
private String content;
private int iterations;
private boolean approved;
private int promptTokens;
private int completionTokens;
private int totalTokens;
private String modelName;
private List<String> editorFeedback = new ArrayList<>();
// Getters and setters
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public int getIterations() {
return iterations;
}
public void setIterations(int iterations) {
this.iterations = iterations;
}
public boolean isApproved() {
return approved;
}
public void setApproved(boolean approved) {
this.approved = approved;
}
public int getPromptTokens() {
return promptTokens;
}
public void setPromptTokens(int promptTokens) {
this.promptTokens = promptTokens;
this.totalTokens = this.promptTokens + this.completionTokens;
}
public void addPromptTokens(int tokens) {
this.promptTokens += tokens;
this.totalTokens = this.promptTokens + this.completionTokens;
}
public int getCompletionTokens() {
return completionTokens;
}
public void setCompletionTokens(int completionTokens) {
this.completionTokens = completionTokens;
this.totalTokens = this.promptTokens + this.completionTokens;
}
public void addCompletionTokens(int tokens) {
this.completionTokens += tokens;
this.totalTokens = this.promptTokens + this.completionTokens;
}
public int getTotalTokens() {
return totalTokens;
}
public String getModelName() {
return modelName;
}
public void setModelName(String modelName) {
this.modelName = modelName;
}
public List<String> getEditorFeedback() {
return editorFeedback;
}
public void setEditorFeedback(List<String> editorFeedback) {
this.editorFeedback = editorFeedback;
}
public void addEditorFeedback(String feedback) {
if (this.editorFeedback == null) {
this.editorFeedback = new ArrayList<>();
}
this.editorFeedback.add(feedback);
}
}
}
此實作包含下列主要功能:
使用元數據產生部落格。
generateBlogPostWithMetadata
方法:- 在給定主題上建立結構良好的部落格文章,其中包含有關產生程序的詳細元數據。
- 搭配 Writer 和 Editor 代理程式使用反覆的優化流程。
- 強制執行 10 個句子的最大長度。
- 追蹤反覆專案、核准狀態、令牌使用量和編輯器意見反應歷程記錄。
- 傳回結構化
BlogGenerationResult
物件中的所有資訊。
令牌使用估計:
- 藉由計算字元,提供令牌使用量的粗略近似值。
- 追蹤提示符號、完成符號和總符號的使用情況。
- 此程式是因應措施,因為我們不再直接存取 類別
ChatResponse
。
BlogGenerationResult
內部類別:- 做為所產生內容及其元數據的容器。
- 包含內容、反覆專案、核准狀態、令牌使用方式和編輯器意見反應的欄位。
- 為追蹤中繼資料提供 getter、setter 和便利方法。
此服務經過徹底批注,以說明 Evaluator-Optimizer 代理程式模式,以及 Spring AI 的 Fluent API 如何促進 AI 代理程式之間的互動。
建立 BlogWriterController 類別
若要透過 REST 端點公開部落格產生功能,請在控制器目錄中建立名為 BlogWriterController.java 的新檔案,然後新增下列程式代碼:
package com.example.springaiapp.controller;
import com.example.springaiapp.service.BlogWriterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
@RestController
@RequestMapping("/api/blog")
public class BlogWriterController {
private final BlogWriterService blogWriterService;
@Autowired
public BlogWriterController(BlogWriterService blogWriterService) {
this.blogWriterService = blogWriterService;
}
@GetMapping(produces = "application/json")
public Map<String, Object> generateBlogPost(@RequestParam String topic) {
// Generate the blog post and capture metadata
BlogWriterService.BlogGenerationResult result = blogWriterService.generateBlogPostWithMetadata(topic);
// Create a structured JSON response
Map<String, Object> response = new HashMap<>();
response.put("topic", topic);
response.put("content", result.getContent());
response.put("metadata", createMetadataObject(result));
return response;
}
private Map<String, Object> createMetadataObject(BlogWriterService.BlogGenerationResult result) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("iterations", result.getIterations());
metadata.put("approved", result.isApproved());
metadata.put("totalTokensUsed", result.getTotalTokens());
if (result.getEditorFeedback() != null && !result.getEditorFeedback().isEmpty()) {
List<Map<String, Object>> feedbackHistory = new ArrayList<>();
for (int i = 0; i < result.getEditorFeedback().size(); i++) {
Map<String, Object> feedbackEntry = new HashMap<>();
feedbackEntry.put("iteration", i + 1);
feedbackEntry.put("feedback", result.getEditorFeedback().get(i));
feedbackHistory.add(feedbackEntry);
}
metadata.put("editorFeedback", feedbackHistory);
}
// Include token usage statistics if available
if (result.getPromptTokens() > 0) {
Map<String, Object> tokenUsage = new HashMap<>();
tokenUsage.put("promptTokens", result.getPromptTokens());
tokenUsage.put("completionTokens", result.getCompletionTokens());
tokenUsage.put("totalTokens", result.getTotalTokens());
metadata.put("tokenUsage", tokenUsage);
}
// Include model information if available
if (result.getModelName() != null) {
metadata.put("model", result.getModelName());
}
return metadata;
}
}
此控制器會公開 GET 端點,以 /api/blog
接受 topic
參數,並傳回結構化 JSON 回應,其中包含產生的部落格內容和產生程式的詳細元數據。 中繼資料包含以下資訊:
- 生成期間執行的迭代次數。
- 編輯代理人對最後部落格文章的核准狀態。
- 預估的語彙基元 (token) 使用統計資料:提示 (prompt)、完成 (completion) 和總 token 數。
- 每次迭代的編輯反饋歷史。
- 所使用 AI 模型的相關信息。
測試部落格生成
新增 BlogWriterService
和其控制器之後,請使用下列命令來編譯並執行應用程式:
mvn spring-boot:run
然後,使用下列命令來測試部落格產生端點:
curl --request GET \
--url 'http://localhost:8080/api/blog?topic=Java%2520on%2520Azure'
此命令會傳回 JSON 回應,其中包含有關產生程式的部落格內容和元數據,如下列範例所示:
{
"topic": "Java on Azure",
"content": "JAVA ON AZURE\n\nIf you're a Java developer looking to elevate your applications in the cloud, Microsoft Azure offers a powerful platform for building and scaling your projects.\n\nAzure App Service allows for quick deployment of Java web applications with frameworks like Spring Boot and Java EE, while Azure Functions provides a serverless option that lets you write Java code without worrying about the underlying infrastructure. Together, these services make it easier for developers to focus on coding rather than managing servers.\n\nFor example, a startup used Azure to build a scalable e-commerce app, leveraging App Service for web hosting and Azure Functions for processing payments. This combination streamlined their development process and improved efficiency.\n\nIn summary, Azure's services enhance flexibility, simplify deployment, and foster innovation for Java developers. How could these tools transform your Java development experience?",
"metadata": {
"iterations": 3,
"approved": false,
"totalTokensUsed": 5480,
"editorFeedback": [
{
"iteration": 1,
"feedback": "1. **Length**: The draft exceeds the 10-sentence maximum requirement, containing 12 sentences. You need to condense the content without losing essential information.\n \n2. **Clarity and Flow**: While the ideas are mostly clear, the flow can be improved by connecting the sentences more cohesively. For example, consider linking the features of Azure more directly to the benefits for Java developers.\n\n3. **Engagement and Reader Interest**: The draft is informative but could be more engaging. Adding a question or a call-to-action might spark more interest and prompt readers to think about how they could apply this information.\n\n4. **Professional yet Conversational Tone**: The tone is somewhat formal. Try using a more conversational style to make it more relatable, such as directly addressing the reader (\"If you're a Java developer...\").\n\n5. **Structure and Organization**: Consider rearranging the content to start with a more compelling hook that highlights the importance of Java in the cloud landscape before diving into specific features.\n\nTo improve the draft, aim to succinctly capture the main points and eliminate redundancy while keeping within the sentence limit."
},
{
"iteration": 2,
"feedback": "While the draft is informative and covers important features of Azure for Java developers, it exceeds the 10-sentence limit, which is a critical requirement. Here are specific, actionable suggestions to improve the draft:\n\n1. **Condense Information**: Try to combine related ideas into fewer sentences. For example, you could merge the sentences about Azure App Service and Docker support to create a more concise point about deployment options.\n \n2. **Remove Redundant Phrases**: Phrases like \"ideal platform\" and \"simplifies the process\" could be streamlined to save space.\n\n3. **Focus on Key Features**: You might want to highlight only one or two standout features instead of discussing multiple options, which would help keep the content focused and within the sentence limit.\n\n4. **Engaging Question**: The concluding question is a good touch, but consider integrating it more seamlessly into the conclusion to avoid exceeding the limit.\n\nBy implementing these changes, you can create a more concise and impactful blog post."
},
{
"iteration": 3,
"feedback": "1. The draft contains 11 sentences, exceeding the 10-sentence maximum requirement. Consider condensing some of the ideas to fit this constraint.\n2. The structure could benefit from clearer transitions between the main points about Azure services. For instance, you might explicitly link how App Service and Azure Functions both contribute to a developer's efficiency.\n3. While the concluding question is engaging, it could be more impactful if you briefly summarize the benefits discussed before asking it, reinforcing the key takeaways.\n4. Aim for a more conversational tone by using simpler language in some areas. For example, \"allows for seamless deployment via Docker\" could be rephrased to \"makes it easy to deploy using Docker.\"\n5. Consider adding a specific example or a brief case study to make the content more relatable and demonstrate practical application. \n\nBy addressing these points, the blog can be more engaging and adhere to the required length."
}
],
"tokenUsage": {
"promptTokens": 3590,
"completionTokens": 1890,
"totalTokens": 5480
},
"model": "Azure OpenAI"
}
}
預期的行為
透過此實作,您應該會持續地在意見反應迴圈中看到至少兩次反覆 (通常是三次)。 第一次反覆是由指示 Editor 代理程式在第一輪總是提供改進意見反應所保證的。 此指示可確保您可以觀察作用中的完整 Evaluator-Optimizer 代理程式模式。 如果沒有這個強制反覆,您可能偶爾會看到編輯代理立即批准第一個草稿,這樣不能完整呈現整個模式。
JSON 回應提供產生程式的寶貴見解,讓您能夠追蹤下列資訊:
- 需要多少反覆專案 - 此範例中有三個。
- 編輯器代理程式的貼文核准狀態 - 在此範例中為 false,這表示它已達到反覆項目數目上限。
- 在每次反覆期間都會提供來自 Editor 代理程式的具體意見反應。
- 整個程序的估計令牌使用量。
您也可以檢查 SimpleLoggerAdvisor
自動擷取的詳細記錄,方法是確保對於 Spring AI 套件,您的應用程式的記錄層級設定為 DEBUG。 這個設定會顯示在下列範例中:
# In application.properties
logging.level.org.springframework.ai.chat.client.advisor=DEBUG
單元摘要
在此單元中,您藉由合併 Evaluator-Optimizer 代理程式模式來擴充 Spring AI 應用程式功能。 此模式透過自動化評估和優化反覆精簡部落格文章,藉此增強內容產生。 您也瞭解如何實作結構化 JSON 回應,其中包含有關產生程式的詳細元數據,並提供 AI 支援之內容產生反覆精簡和資源使用量的深入解析。