Spring AI的开发场景中,和大语言模型(LLM)交互时,经常会遇到需要将模型输出转换为特定结构化格式的需求。本文就聚焦于此,详细介绍Spring AI提供的MapOutputConverterListOutputConverterBeanOutputConverter这三个内置类,它们能帮助我们将LLM的输出转化为列表、映射或Java Bean定义的复杂结构,还会结合实际示例,教大家如何使用这些转换器来实现结构化输出。

一、准备工作

在运行代码之前,有两个关键步骤需要完成。首先是设置OpenAPI项目密钥,要把它设为环境变量,让应用程序能从环境变量中读取。比如在终端里,可以通过下面这条命令来设置(其中[api_key_copied_from_openai_site]需要替换成从OpenAI网站复制的真实密钥):

export OPENAI_API_KEY=[api_key_copied_from_openai_site] 

然后在项目的application.properties文件里,就可以这样引用这个API密钥:

spring.ai.openai.api-key=${OPENAI_API_KEY} 

接下来,要添加与我们交互的LLM相关的Maven依赖。在这个示例里,我们使用OpenAI的ChatGPT,所以需要在项目里添加下面这个依赖:

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency> 

如果想了解完整的设置步骤,可以参考Spring AI教程。

二、Spring AI中MapOutputConverter的使用示例

MapOutputConverter类的作用是,让提示要求LLM以符合RFC8259标准的JSON格式输出,并且输出的结构要是java.util.HashMap类型。等LLM返回响应后,这个转换器会把JSON格式的响应解析出来,填充到Map实例里。

MapOutputConverter的工作方式是,在用户消息后面追加一段固定文本,以此来要求特定的格式。我们可以查看MapOutputConverter类的源代码,或者调用它的toFormat()方法,就能看到这段固定文本。下面是MapOutputConverter类里getFormat()方法的代码:

public class MapOutputConverter extends AbstractMessageOutputConverter<Map<String, Object>> { //... @Override public String getFormat() { String raw = """ Your response should be in JSON format. The data structure for the JSON should match this Java class: %s Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation. """; return String.format(raw, HashMap.class.getName()); } } 

在下面这个例子里,我们给程序提供一个国家列表,然后让LLM以Map格式返回每个国家及其首都的信息。

import org.springframework.ai.chat.ChatClient; import org.springframework.ai.prompt.Prompt; import org.springframework.ai.prompt.PromptTemplate; import org.springframework.ai.response.MapOutputConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Map; // 定义一个RestController,用于处理HTTP请求 @RestController public class CountryCapitalController { // 注入ChatClient实例,用于和LLM交互 @Autowired private ChatClient chatClient; // 处理GET请求,路径为/country-capital-service/map @GetMapping("/country-capital-service/map") public Map<String, Object> getCapitalNamesInMap(@RequestParam String countryNamesCsv) { // 检查传入的国家名称CSV字符串是否为空 if (countryNamesCsv == null || countryNamesCsv.isEmpty()) { // 如果为空,抛出异常 throw new IllegalArgumentException("Country names CSV cannot be null or empty"); } // 创建MapOutputConverter实例 MapOutputConverter converter = new MapOutputConverter(); // 获取要求的格式文本 String format = converter.getFormat(); // 创建提示模板对象 PromptTemplate pt = new PromptTemplate("For these list of countries {countryNamesCsv}, return the list of capitals. {format}"); // 根据传入的参数和格式,渲染提示 Prompt renderedPrompt = pt.create(Map.of("countryNamesCsv", countryNamesCsv, "format", format)); // 调用ChatClient发送提示,获取响应 ChatResponse response = chatClient.call(renderedPrompt); // 获取响应结果 Generation generation = response.getResult(); // 解析响应内容,返回Map对象 return converter.parse(generation.getOutput().getContent()); } } 

当API收到请求后,会把countryNamesCsv替换为传入的国家列表,把format替换为MapOutputConvertergetFormat()方法返回的固定文本,从而准备好最终的提示。LLM返回的响应,以及API最终返回的内容,都是Map格式的JSON输出。比如,我们访问http://localhost:8080/country-capital-service/map?countryNamesCsv=India, USA, Canada, Israel,得到的响应可能是这样:

{ "Canada": "Ottawa", "USA": "Washington D.C.", "Israel": "Jerusalem", "India": "New Delhi" } 

三、Spring AI中ListOutputConverter的使用示例

ListOutputConverter类的工作原理和MapOutputConverter类似,不过它是让提示要求LLM以逗号分隔值的列表形式输出。之后,Spring AI会借助Jackson把这些CSV值解析成List。下面是ListOutputConverter类里getFormat()方法的代码:

public class ListOutputConverter extends AbstractConversionServiceOutputConverter<List<String>> { //... @Override public String getFormat() { return """ Your response should be a list of comma separated values eg: `foo, bar, baz` """; } } 

在下面这个例子里,我们同样提供一个国家列表,这次要求LLM以列表形式返回这些国家的首都。

import org.springframework.ai.chat.ChatClient; import org.springframework.ai.prompt.Prompt; import org.springframework.ai.prompt.PromptTemplate; import org.springframework.ai.response.ListOutputConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; // 定义一个RestController,用于处理HTTP请求 @RestController public class CountryCapitalListController { // 注入ChatClient实例,用于和LLM交互 @Autowired private ChatClient chatClient; // 处理GET请求,路径为/country-capital-service/list @GetMapping("/country-capital-service/list") public List<String> getCapitalNamesInList(@RequestParam String countryNamesCsv) { // 检查传入的国家名称CSV字符串是否为空 if (countryNamesCsv == null || countryNamesCsv.isEmpty()) { // 如果为空,抛出异常 throw new IllegalArgumentException("Country names CSV cannot be null or empty"); } // 创建ListOutputConverter实例,传入DefaultConversionService ListOutputConverter converter = new ListOutputConverter(new DefaultConversionService()); // 获取要求的格式文本 String format = converter.getFormat(); // 创建提示模板对象 PromptTemplate pt = new PromptTemplate("For these list of countries {countryNamesCsv}, return the list of capitals. {format}"); // 根据传入的参数和格式,渲染提示 Prompt renderedPrompt = pt.create(Map.of("countryNamesCsv", countryNamesCsv, "format", format)); // 调用ChatClient发送提示,获取响应 ChatResponse response = chatClient.call(renderedPrompt); // 获取响应结果 Generation generation = response.getResult(); // 解析响应内容,返回List对象 return converter.parse(generation.getOutput().getContent()); } } 

LLM返回的响应是CSV格式的普通字符串,比如:

New Delhi, Washington D.C., Ottawa, Jerusalem 

然后,程序会用converter.parse()方法把这个CSV字符串解析成java.util.List。我们可以通过访问http://localhost:8080/country-capital-service/list?countryNamesCsv=India, USA, Canada, Israel来测试这个API,得到的响应可能是这样:

[ "New Delhi", "Washington D.C.", "Ottawa", "Jerusalem" ] 

四、Spring AI中BeanOutputConverter的使用示例

BeanOutputConverter类主要用于让LLM以和Java POJO匹配的JSON格式和结构返回响应,保证LLM响应里的字段和指定Java Bean里的字段是兼容的。在下面这个BeanOutputConverter类的getFormat()方法里,会根据提供的Java Bean类生成JSON模式。

public class BeanOutputConverter<T> implements StructuredOutputConverter<T> { //... @Override public String getFormat() { String template = """ Your response should be in JSON format. Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation. Do not include markdown code blocks in your response. Remove the ```json markdown from the output. Here is the JSON Schema instance your output must adhere to: ```%s``` """; return String.format(template, this.jsonSchema); } } 

来看另一个例子,在这个例子里,我们传入一个国家名称,让LLM返回这个国家最受欢迎的10个城市,并且响应要遵循下面这个Java Bean的结构:

// 定义一个记录类Pair,包含国家名称和城市列表两个字段 public record Pair(String countryName, List<String> cities) {} 

下面看看如何使用BeanOutputConverter来设置提示并解析收到的响应:

import org.springframework.ai.chat.ChatClient; import org.springframework.ai.prompt.Prompt; import org.springframework.ai.prompt.PromptTemplate; import org.springframework.ai.response.BeanOutputConverter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; // 定义一个RestController,用于处理HTTP请求 @RestController public class CountryCitiesController { // 注入ChatClient实例,用于和LLM交互 @Autowired private ChatClient chatClient; // 处理GET请求,路径为/country-capital-service/bean @GetMapping("/country-capital-service/bean") public Pair getCapitalNamesInPojo(@RequestParam String countryName) { // 创建BeanOutputConverter实例,指定要转换的目标类型为Pair BeanOutputConverter<Pair> converter = new BeanOutputConverter(Pair.class); // 获取要求的格式文本 String format = converter.getFormat(); // 创建提示模板对象 PromptTemplate pt = new PromptTemplate("For these list of countries {countryName}, return the list of its 10 popular cities. {format}"); // 根据传入的参数和格式,渲染提示 Prompt renderedPrompt = pt.create(Map.of("countryName", countryName, "format", format)); // 调用ChatClient发送提示,获取响应 ChatResponse response = chatClient.call(renderedPrompt); // 获取响应结果 Generation generation = response.getResult(); // 解析响应内容,返回Pair对象 return converter.parse(generation.getOutput().getContent()); } } 

我们可以通过访问http://localhost:8080/country-capital-service/bean?countryName=USA来验证生成的响应,得到的响应可能是这样:

{ "countryName": "USA", "cities": [ "New York City", "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia", "San Antonio", "San Diego", "Dallas", "San Jose" ] } 

五、总结

在这篇Spring AI教程里,我们详细了解了LLM的输出并不总是非结构化文本,很多时候需要它以固定格式返回内容,这时候MapOutputConverterListOutputConverterBeanOutputConverter这些结构化输出转换器就派上用场了。建议大家多去尝试使用这些API,用不同的格式请求输出,这样能更好地理解和掌握它们的用法。希望大家在学习和实践中不断积累经验,在Spring AI开发中更上一层楼!如果有任何问题,欢迎一起交流探讨。