章
目
录
在Spring AI的开发场景中,和大语言模型(LLM)交互时,经常会遇到需要将模型输出转换为特定结构化格式的需求。本文就聚焦于此,详细介绍Spring AI提供的MapOutputConverter
、ListOutputConverter
和BeanOutputConverter
这三个内置类,它们能帮助我们将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
替换为MapOutputConverter
的getFormat()
方法返回的固定文本,从而准备好最终的提示。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的输出并不总是非结构化文本,很多时候需要它以固定格式返回内容,这时候MapOutputConverter
、ListOutputConverter
和BeanOutputConverter
这些结构化输出转换器就派上用场了。建议大家多去尝试使用这些API,用不同的格式请求输出,这样能更好地理解和掌握它们的用法。希望大家在学习和实践中不断积累经验,在Spring AI开发中更上一层楼!如果有任何问题,欢迎一起交流探讨。
文章目录 Spring AI是什么?有啥优势? 如何在项目中使用Spring AI? Spring AI详细功 […]