Spring AI结构化输出
Java作为面向对象语言,如果Spring AI调用模型可以直接响应对象,对于依赖可靠输入值的下游应用程序非常重要,Spring AI提供了可以直接响应entity的配置。
本节我们将实现Spring AI 结构化输出entity、List、Map对象。
原理

-
• 在LLM调用之前,转换器会在提示中添加格式说明,为模型生成所需输出结构提供明确的指导。这些说明充当蓝图,指定模型的响应,使其符合指定的格式。 -
• 在LLM调用之后,转换器将模型的输出文本转换为结构化类型的实例。这一转换过程包括解析原始文本输出并将其映射到相应的结构化数据表示,例如JSON、XML或特定领域的数据结构。
准备
搭建SpringBoot项目、添加SpringAI依赖、配置Open AI参数,请大家直接看Spring AI入门这篇。
因为众所周知的原因,我们不能直接访问国外网站。如果有需要Open AI 代理地址,请关注公众号,点击菜单apikey 免费获取!
代码
开始调用之前,我们先创建ChatController类,然后注入ChatClient,Spring AI框架已将ChatModel注入到ChatClient中,所以这里我们不需要对模型进行任何配置。
@RestController
public class ChatController {
private final ChatClient chatClient;
public ChatController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
}
由于本节我们将响应entity对象,这里在ChatController中创建一个对象,该对象仅有两个属性:String actor
演员名称List<String> movies
出演的电影列表
// record是Java 14引入的一个新特性,用于创建不可变的数据载体类。
// 它简化了创建简单数据类的过程,自动提供了equals、hashCode、toString等方法的实现。
record ActorFilms(String actor, List<String> movies) {}
响应entity对象
这里我们创建一个get方法,接收一个userInput参数。
@RestController
public class ChatController {
...
@GetMapping("/entity")
ActorFilms entity(String userInput) {
return this.chatClient.prompt()
.user(userInput)
.call()
.entity(ActorFilms.class);
}
}
这里SpringAI进行了更高等级的封装,无法从代码直观感受到结构化输出的原理。下面使用更底层的原子API来实现同样的响应。
@RestController
publicclassChatController {
...
@GetMapping("/originEntity")
ActorFilms originEntity(String userInput) {
// 创建bean输出转换器
BeanOutputConverter<ActorFilms> beanOutputConverter =
newBeanOutputConverter<>(ActorFilms.class);
// 获取bean对象格式指令
Stringformat= beanOutputConverter.getFormat();
// 将指令、用户输入传入chatClient,然后调用模型,获取模型输出
Stringoutput=this.chatClient.prompt(format)
.user(userInput)
.call()
.content();
// 将模型输出转换成bean对象
ActorFilmsactorsFilms= beanOutputConverter.convert(output);
return actorsFilms;
}
}
这里我们在浏览器发送get请求http://localhost:8080/entity?userInput=列出成龙出演的电影
或http://localhost:8080/originEntity?userInput=列出成龙出演的电影


nice 就是这么简单!
响应List对象
Spring AI还提供了重载的entity方法,签名是`entity(ParameterizedTypeReference type)“,允许指定诸如泛型列表等类型。
@RestController
public class ChatController {
...
@GetMapping("/entityList")
List<ActorFilms> entityList(String userInput) {
List<ActorFilms> actorFilms = chatClient.prompt()
.user(userInput)
.call()
.entity(new ParameterizedTypeReference<List<ActorFilms>>() {});
return actorFilms;
}
}
这里我们在浏览器发送一个get请求http://localhost:8080/entityList?userInput=列出周星驰、周润发、成龙出演的电影

这里就不再展示原子API的写法了,就是将new BeanOutputConverter<>(ActorFilms.class);
改为new BeanOutputConverter<>(new ParameterizedTypeReference<List<ActorFilms>>() {});
不再赘述
响应Map对象
既然提供了 List 的响应,那么Map也必然不可少。
@RestController
publicclassChatController {
...
// 响应Map<String, Object>对象
@GetMapping("/mapEntity")
Map<String, Object> mapEntity() {
Map<String, Object> result = this.chatClient.prompt()
.user(u -> u.text("Provide me a List of {subject}")
.param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'"))
.call()
.entity(newParameterizedTypeReference<Map<String, Object>>() {
});
return result;
}
}
浏览器发送get请求:http://localhost:8080/mapEntity

如果使用原子API,则需要将转换器改为new MapOutputConverter();
,表明我们要求输出的是一个Map类型。
流式响应对象
上面的接口都是同步响应,这是由于结构化输出的原理决定的,全部数据响应完,数据结构才完整,才能进行最后一步文本转结构化。
目前SpringAI流式响应,只能使用更底层的原子 API,使用方式如下:
@RestController
publicclassChatController {
...
@GetMapping("/entityListStream")
List<ActorFilms> entityListStream(String userInput) {
varconverter=newBeanOutputConverter<>(newParameterizedTypeReference<List<ActorFilms>>() {});
Flux<String> flux = this.chatClient.prompt(converter.getFormat())
.user(userInput)
.stream()
.content();
Stringcontent= flux.collectList().block().stream().collect(Collectors.joining());
List<ActorFilms> actorFilms = converter.convert(content);
return actorFilms;
}
}
这里我们在浏览器发送一个get请求http://localhost:8080/entityListStream?userInput=列出周星驰、周润发、成龙出演的电影

执行后大家一定发现,其实json最终也是一起响应的。Spring AI说未来将提供响应式stream输出,就让我们期待一波吧!
最后
结构化输出是大模型开发不能避免的环节,Spring AI的封装帮助程序员节省大量时间,致使大模型为下游程序、函数提供可靠输入,当然理解其原理我们才能更灵活掌握结构化输出用法。
文章源码、Open AI代理地址、免费api-key请关注公众号免费获取。