03.结构化返回

一灰灰blogSpringAISpringSpringAI约 1765 字大约 6 分钟

03. 结构化返回

通常情况下,在我们不显示要求大模型返回什么样的数据结构时,大模型返回的大多不是结构化的数据;对于上层的业务开发来说,将大模型返回的关键信息映射为结构化的数据模型是一个非常难受的事情

SpringAI提供了一系列的返回结果结构化转换器来实现上面的痛点;接下来我们来具体看一下,可以怎么处理返回结果

一、实例演示

首先我们需要创建一个SpringAI的项目,基本流程同 创建一个SpringAI-Demo工程

1. 初始化

创建一个MVC的API,用于提供与大模型的交互

@RestController
public class ChatController {

    private final ZhiPuAiChatModel chatModel;

    @Autowired
    public ChatController(ZhiPuAiChatModel chatModel) {
        this.chatModel = chatModel;
    }
}

2. BeanOutputConverter

借助BeanOutputConverter来实现返回结果映射为java的POJO类,首先我们定义一个提示词模板,主要用于查询某个导演的作品

帮我返回五个{actor}导演的电影名,要求中文返回

我们希望返回的结构如下:

{
  "actor": "周星驰",
  "movies": [
    "大话西游之大圣娶亲",
    "喜剧之王",
    "功夫",
    "西游降魔篇",
    "长江七号"
  ]
}

因此我们可以定义一个record,用于承接返回的结果

record ActorsFilms(String actor, List<String> movies) {
}

借助ChatClient来实现结果解析

/**
 * 基于ChatClient实现返回结果的结构化映射
 *
 * @param actor
 * @return
 */
@GetMapping("/ai/generate")
public ActorsFilms generate(@RequestParam(value = "actor", defaultValue = "周星驰") String actor) {
    PromptTemplate template = new PromptTemplate("帮我返回五个{actor}导演的电影名,要求中文返回");
    Prompt prompt = template.create(Map.of("actor", actor));

    ActorsFilms films = ChatClient.create(chatModel).prompt(prompt).call().entity(ActorsFilms.class);
    return films;
}

为什么上面的方式就可以实现结果映射为java的POJO类呢? 我们可以debug一下ChatClient.create(chatModel).prompt(prompt).call()返回的对象

从下面的截图中可以看到,在传递给大模型的请求中,context参数中,指定了要求大模型返回的数据格式(这里基于Advisors来实现的上下文数据附加/扩充提示词的功能)

当然也可以显示使用BeanOutputConverter基于ChatModel来实现

/**
 * 基于BeanOutputConverter实现返回结果结构化映射
 *
 * @param actor
 * @return
 */
@GetMapping("/ai/gen2")
public ActorsFilms gen2(@RequestParam(value = "actor", defaultValue = "周星驰") String actor) {
    BeanOutputConverter<ActorsFilms> beanOutputConverter = new BeanOutputConverter<>(ActorsFilms.class);
    String format = beanOutputConverter.getFormat();

    PromptTemplate template = new PromptTemplate("""
                帮我返回五个{actor}导演的电影名
                {format}
            """);
    Prompt prompt = template.create(Map.of("actor", actor, "format", format));
    Generation generation = chatModel.call(prompt).getResult();
    if (generation == null) {
        return null;
    }
    return beanOutputConverter.convert(generation.getOutput().getText());
}

注意上面的实现,在提示词模板中,新增了 {format},其值由 BeanOutputConvertergetFormat() 方法获取;其实现原理是直接在提示词中添加了结构化的返回结果格式,因此,大模型返回的数据结构,会按照这个格式进行解析

接下来实际访问看看表现情况

从多次体验的结果来看,结果的格式与定义的POJO类一致,因此,基于BeanOutputConverter的实现,可以达到我们想要的结果;但是在有限的几次访问尝试中,返现ChatClient方式,返回的结果中actor可能为null,没有正确获取到值,这也侧面说明,大模型返回数据的不可控性

3. 属性排序

借助@JsonPropertyOrder来实现排序,这个注解适用于record和普通的class

@JsonPropertyOrder({"actor", "movies"})
    record ActorsFilms(String actor, List<String> movies) {
}

@GetMapping("/ai/genList")
public List<ActorsFilms> genList(@RequestParam(value = "actor1", defaultValue = "周星驰") String actor1,
                                 @RequestParam(value = "actor2", defaultValue = "张艺谋") String actor2) {
    List<ActorsFilms> actorsFilms = ChatClient.create(chatModel).prompt()
            .user(u ->
                    u.text("帮我返回五个{actor1}和{actor2}导演的电影名,要求中文返回")
                            .params(Map.of("actor1", actor1, "actor2", actor2)))
            .call()
            .entity(new ParameterizedTypeReference<List<ActorsFilms>>() {
            });
    return actorsFilms;
}

4. MapOutputConverter

上面介绍的是返回一个POJO,接下来看一下直接基于MapOutputConverter来实现用map接收返回结果

这里使用的是上面用过的 ParameterizedTypeReference 来指定返回结果的类型

基于 ChatClient 的方式

@GetMapping("/ai/genMap")
public Map genMap(@RequestParam(value = "actor", defaultValue = "周星驰") String actor) {
    Map<String, Object> actorsFilms = ChatClient.create(chatModel).prompt()
            .user(u ->
                    u.text("帮我返回五个{actor}导演的电影名,要求中文返回")
                            .param("actor", actor))
            .call()
            .entity(new ParameterizedTypeReference<Map<String, Object>>() {
            });
    return actorsFilms;
}

基于MapOutputConverter结合ChatModel来实现

@GetMapping("/ai/genMap2")
public Map genMap2(@RequestParam(value = "actor", defaultValue = "周星驰") String actor) {
    MapOutputConverter mapOutputConverter = new MapOutputConverter();

    String format = mapOutputConverter.getFormat();
    PromptTemplate template = new PromptTemplate("""
                帮我返回五个{actor}导演的电影名,要求中文返回
                {format}
            """);
    Prompt prompt = template.create(Map.of("actor", actor, "format", format));
    Generation generation = chatModel.call(prompt).getResult();
    Map<String, Object> result = mapOutputConverter.convert(generation.getOutput().getText());
    return result;
}

两个接口的返回结果如下图,虽然都是返回的Map,但是仔细看之后,会发现他们的层级并不一样,基于ChatClient返回的层级会多一层,返回的电影被放在了movie属性下,以列表的方式组织;而基于MapOutputConverter返回的就是一层的map,key为数字;

至于孰优孰劣,这里就不予置评,看个人喜好了

5. ListOutputConverter

除了上面返回Map的case之外,再看一下返回列表的场景,借助ListOutputConverter来实现,基本上和前面介绍的差异不大

@GetMapping("/ai/genList1")
public List<String> genList1(@RequestParam(value = "actor", defaultValue = "周星驰") String actor) {
    List<String> actorsFilms = ChatClient.create(chatModel).prompt()
            .user(u ->
                    u.text("帮我返回五个{actor}导演的电影名,要求中文返回")
                            .param("actor", actor))
            .call()
            .entity(new ListOutputConverter(new DefaultConversionService()));
    return actorsFilms;
}

@GetMapping("/ai/genList2")
public List genList2(@RequestParam(value = "actor", defaultValue = "周星驰") String actor) {
    ListOutputConverter listOutputConverter = new ListOutputConverter(new DefaultConversionService());

    String format = listOutputConverter.getFormat();
    PromptTemplate template = new PromptTemplate("""
                帮我返回五个{actor}导演的电影名,要求中文返回
                {format}
            """);
    Prompt prompt = template.create(Map.of("actor", actor, "format", format));
    Generation generation = chatModel.call(prompt).getResult();
    List<String> result = listOutputConverter.convert(generation.getOutput().getText());
    return result;
}

返回实例如下(你会发现返回数据的不准,当然这个就不属于我们这里的范畴了)

二、小结

本文主要介绍在SpringAI中如何结构化的处理大模型返回的结果,从使用方式来看,区分ChatClientChatModel两种不同的使用姿势;其中前者更简单

// 方式一:ChatClient通过借助Advisor从上下文获取信息注入到提示词
ChatClient.create(chatModel).prompt()
        .user("提示词")
        .call()
        .entity(xxx返回的对象类型);


// 方式二:再提示词中明确注入返回对象的格式
BeanOutputConverter<ActorsFilms> beanOutputConverter = new BeanOutputConverter<>(ActorsFilms.class);

String format = this.beanOutputConverter.getFormat();
String template = """
这里是提示词.
{format}
""";

Generation generation = chatModel.call(new PromptTemplate(this.template, Map.of("format", this.format)).create()).getResult();
ActorsFilms actorsFilms = this.beanOutputConverter.convert(this.generation.getOutput().getText());

此外,核心发挥作用的是Converter,SpringAI官方提供了下面这些具体的实现,基本上可以覆盖我们90%以上的业务场景; 若覆盖补全,则考虑通过自定义Converter来实现

文中所有涉及到的代码,可以到项目中获取 https://github.com/liuyueyi/spring-ai-demoopen in new window

Loading...