Spring AI结构化输出转换器:List、Map和Bean的应用详解

后端 潘老师 2个月前 (02-20) 53 ℃ (0) 扫码查看

在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开发中更上一层楼!如果有任何问题,欢迎一起交流探讨。

归属教程 Spring AI 快速入门教程汇总

文章目录 Spring AI是什么?有啥优势? 如何在项目中使用Spring AI? Spring AI详细功 […]


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/14798.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】