Spring AI 大模型返回内容格式化源码分析及简单使用

为什么需要格式化输出

对于依赖可靠解析输出值的下游应用程序来说,生成结构化输出是LLMs非常重要的能力。开发人员希望快速将 AI 模型的结果转换为数据类型,例如 JSON、XML 或 Java 类,这些数据类型可以传递给其他应用程序函数和方法。

特别是函数调用、智能体等都需要将大模型的输出进行格式化,然后调用外部函数辅助大模型更好的回答提出的问题。

哪些大模型支持结构化输出

并不是所有的大模型都支持格式化输出的,目前知道的支持结构化输出的大模型如下;

  • OpenAI
  • Anthropic Claude 3 【美国初创公司Anthropic发布的大模型】
  • Azure OpenAI 【微软大模型】
  • Mistral AI 【法国2023年成立的年轻人工智能公司发布的大模型】
  • Ollama 【本地运行大模型工具】,放在这里有点不太懂???
  • Vertex AI Gemini 【Google公司的大模型】
  • Bedrock Anthropic 2 【Anthropic公司发布的 Claude 2大模型】
  • Bedrock Anthropic 3 【Anthropic公司发布的 Claude 3大模型】
  • Bedrock Cohere 【初创公司Cohere的大模型】
  • Bedrock Llama 【Meta公司发布的Llama大模型】

后续会继续补充!

处理流程图

image.png Spring AI提供了调用之前和调用之后两个步骤进行处理;

  • 在LLM调用之前,转换器会将格式说明附加到提示词中,为模型生成所需的输出结构提供明确的指导。

  • LLM调用后,转换器获取模型的输出文本,并将其转换为结构化类型的实例。此转换过程涉及分析原始文本输出并将其映射到相应的结构化数据表示形式,例如 JSON、XML 或特定于域的数据结构。

类交互图

image.png

第一步:通过PromptTemplate定义提示词模板,然后通过FormatProvider#getFormat()获取指示大模型输出格式的指令。

第二步:通过ChatModel调用大模型,并返回原始文本输出。

第三步:通过Spring AI提供的Converter<String,T> 转换结构化内容输出。

源码分析

注意,本文分析的是1.0.0-SNAPSHOT版本的,与1.0.0之前的版本相差比较大。

整体架构图

image.png

StructuredOutputConverter接口

public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {

    /**
     * 该接口废弃,应该使用父接口中的convert方法替代
     * @deprecated Use the {@link #convert(Object)} instead.
     */
    default T parse(@NonNull String source) {
       return this.convert(source);
    }

}

该接口继承两个接口 Converter<String, T>FormatProvider

其中Converter<String, T> springframework core包中的转换器接口。之所以继承该接口,官方文档说保持所有转换器风格的一致性。

FormatProvider 由 Spring AI 提供,主要获取格式化指令内容,指导大模型以什么格式输出内容。

package org.springframework.ai.converter;

public interface FormatProvider {

    /**
     * 返回含有指导大模型返回格式化的指令,大家先这么理解着,后续示例中会使用,让大家更好地理解它
     */
    String getFormat();
}

BeanOutputConverter 源码

BeanOutputConverterStructuredOutputConverter<T> 的唯一子类。具体源码如下【只留了核心代码】;

public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {

    /** The object mapper used for deserialization and other JSON operations. */
    @SuppressWarnings("FieldMayBeFinal")
    private ObjectMapper objectMapper;

    
    /**
     * Parses the given text to transform it to the desired target type.
     * @param text The LLM output in string format.
     * @return The parsed output in the desired target type.
     */
    @Override
    public T convert(@NonNull String text) {
       try {
          if (text.startsWith("```json") && text.endsWith("```")) {
             text = text.substring(7, text.length() - 3);
          }
          return (T) this.objectMapper.readValue(text, this.typeRef);
       }
       catch (JsonProcessingException e) {
          logger.error("Could not parse the given text to the desired target type:" + text + " into " + this.typeRef);
          throw new RuntimeException(e);
       }
    }

    /**
     * Provides the expected format of the response, instructing that it should adhere to
     * the generated JSON schema.
     * @return The instruction format string.
     */
    @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);
    }

}

BeanOutputConverter 使用的方案是 ObjectMapper。 想必大家对 ObjectMapper 都很熟悉了。BeanOutputConverter本身代码也比较简单。

AbstractMessageOutputConverter<T>

AbstractMessageOutputConverter<T> - 使用预定的转换器对大模型输出进行格式化输出。未提供默认 FormatProvider 实现。

package org.springframework.ai.converter;

import org.springframework.messaging.converter.MessageConverter;

/**
 * Abstract {@link StructuredOutputConverter} implementation that uses a pre-configured
 * {@link MessageConverter} to convert the LLM output into the desired type format.
 *
 * @param <T> Specifies the desired response type.
 * @author Mark Pollack
 * @author Christian Tzolov
 */
public abstract class AbstractMessageOutputConverter<T> implements StructuredOutputConverter<T> {
    // 引用springframework-messaging提供的消息转换器
    private MessageConverter messageConverter;

    public AbstractMessageOutputConverter(MessageConverter messageConverter) {
       this.messageConverter = messageConverter;
    }

    public MessageConverter getMessageConverter() {
       return this.messageConverter;
    }

}

MapOutputConverter 源码

public class MapOutputConverter extends AbstractMessageOutputConverter<Map<String, Object>> {

    public MapOutputConverter() {
        // 调用父类构造方法,设置converter。
       super(new MappingJackson2MessageConverter());
    }

    @Override
    public Map<String, Object> convert(@NonNull String text) {
       // 大模型返回的String类型为 ```json xxxx ```
       if (text.startsWith("```json") && text.endsWith("```")) {
          text = text.substring(7, text.length() - 3);
       }
       // 
       Message<?> message = MessageBuilder.withPayload(text.getBytes(StandardCharsets.UTF_8)).build();
       return (Map) this.getMessageConverter().fromMessage(message, HashMap.class);
    }

    // 发送给大模型提示词中的format内容,能够指导大模型按照format的说明进行返回
    @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.
             Remove the ```json markdown from the output.
             """;
       return String.format(raw, HashMap.class.getName());
    }

}

MapOutputConverterAbstractMessageOutputConverter<T> 的唯一的实现类。其内部使用的MessageConverter类型为springframework.messaging包提供的消息转换器 MappingJackson2MessageConverter。使用Jackson 2 JSON库实现消息与JSON格式之间相互转换。

Spring 还提供了其它消息类型转换器,大大简化消息的读取和写入。

  • MappingJacksonMessageConverter:使用Jackson JSON库实现消息与JSON格式之间的相互转换
  • MarshallingMessageConverter:使用JAXB库实现消息与XML格式之间的相互转换
  • SimpleMessageConverter:实现String与TextMessage之间的相互转换,字节数组与Bytes Message之间的相互转换,Map与MapMessage之间的相互转换 以及Serializable对象与ObjectMessage之间的相互转换。

这里不再详细介绍MessageConveter的实现,如果大家有兴趣自行研究其实现原理。

AbstractConversionServiceOutputConverter 源码

public abstract class AbstractConversionServiceOutputConverter<T> implements StructuredOutputConverter<T> {

    private final DefaultConversionService conversionService;

    public AbstractConversionServiceOutputConverter(DefaultConversionService conversionService) {
       this.conversionService = conversionService;
    }

    public DefaultConversionService getConversionService() {
       return this.conversionService;
    }

}

AbstractConversionServiceOutputConverter 实现原理与 AbstractMessageOutputConverter<T> 不一样,AbstractConversionServiceOutputConverter 使用的是springframework.core模块提供的 ConversionService 其提供了一种机制来执行类型之间的转换。默认使用 DefaultConversionService,内部提供了许多内置类型转换的支持。

ConversionService 允许开发者定义自己的转换逻辑,并注册到服务中,从而可以在运行时动态地转换对象。这种机制在数据绑定、服务层方法参数转换以及热河需要类型转换的场景中都非常有用。

ConversionService 在SpringMVC中也扮演重要的角色,它用于将请求参数绑定到控制器方法的参数上时进行类型转换。

ListOutputConverter 源码

ListOutputConverterAbstractConversionServiceOutputConverter<T>唯一实现类。

public class ListOutputConverter extends AbstractConversionServiceOutputConverter<List<String>> {

    public ListOutputConverter(DefaultConversionService defaultConversionService) {
       super(defaultConversionService);
    }

    @Override
    public String getFormat() {
       return """
             Your response should be a list of comma separated values
             eg: `foo, bar, baz`
             """;
    }

    @Override
    public List<String> convert(@NonNull String text) {
       return this.getConversionService().convert(text, List.class);
    }

}

实现非常简单,也没有太多需要说明的。

转换器实现原理

转换器实现方案
BeanOutputConverter底层使用 ObjectMapper 实现转换
ListOutputConverter底层使用 springframework.core 提供的 ConversionService 实现的
MapOutputConverter底层使用 springframework.messaging 模块提供的消息转换器实现的

三种类型的转换,使用了三种实现方案

示例

在 [# 05. Spring AI 提示词模版源码分析及简单的使用] 基础上,我们返回一个电影对象。

定义Model

package com.ivy.model;

/**
 * 电影返回对象
 *
 * @param director
 * @param filmName
 * @param publishedDate
 * @param description
 */
public record Film(String director, String filmName, String publishedDate, String description) {
}

BeanOutputConverter示例

package com.ivy.controller;

import com.ivy.model.Film;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.converter.StructuredOutputConverter;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class StructuredOutputController {
    @Resource
    private OpenAiChatModel openAiChatModel;

    @GetMapping("/bean")
    public Film structuredOutput(String director) {
        // 定义提示词模版
        // 其中 format指定输出的格式
        final String template = """
                        请问{director}导演最受欢迎的电影是什么?哪年发行的,电影讲述的什么内容?
                        {format}
                """;
        // 定义结构化输出转化器, 生成Bean
        StructuredOutputConverter<Film> structured = new BeanOutputConverter<>(Film.class);
        // 生成提示词对象
        PromptTemplate promptTemplate = new PromptTemplate(template);
        Prompt prompt = promptTemplate.create(Map.of("director", director, "format", structured.getFormat()));

        ChatClient chatClient = ChatClient.builder(openAiChatModel)
                .build();
        String content = chatClient.prompt(prompt).call().content();
        // 转换
        return structured.convert(content);
    }


}

测试结果

image.png

另外一种实现方式

@GetMapping("/bean2")
public Film structuredOutput2(String director) {
    return ChatClient.create(openAiChatModel)
            .prompt()
            .user(u -> u.text("""
                            请问{director}导演最受欢迎的电影是什么?哪年发行的,电影讲述的什么内容
                    """).params(Map.of("director", director))
            ).call()
            .entity(Film.class);

}

image.png

putConverterMapOutputConverter` 使用大家可以参考github上的代码,文章篇幅原因不在列举。

可能大家都想学习AI大模型技术,也想通过这项技能真正达到升职加薪,就业或是副业的目的,但是不知道该如何开始学习,因为网上的资料太多太杂乱了,如果不能系统的学习就相当于是白学。为了让大家少走弯路,少碰壁,这里我直接把全套AI技术和大模型入门资料、操作变现玩法都打包整理好,希望能够真正帮助到大家。

👉AI大模型学习路线汇总👈
大模型学习路线图,整体分为7个大的阶段:(全套教程文末领取哈)
在这里插入图片描述

第一阶段: 从大模型系统设计入手,讲解大模型的主要方法;

第二阶段: 在通过大模型提示词工程从Prompts角度入手更好发挥模型的作用;

第三阶段: 大模型平台应用开发借助阿里云PAI平台构建电商领域虚拟试衣系统;

第四阶段: 大模型知识库应用开发以LangChain框架为例,构建物流行业咨询智能问答系统;

第五阶段: 大模型微调开发借助以大健康、新零售、新媒体领域构建适合当前领域大模型;

第六阶段: 以SD多模态大模型为主,搭建了文生图小程序案例;

第七阶段: 以大模型平台应用与开发为主,通过星火大模型,文心大模型等成熟大模型构建大模型行业应用。

👉大模型实战案例👈
光学理论是没用的,要学会跟着一起做,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
在这里插入图片描述

👉大模型视频和PDF合集👈
观看零基础学习书籍和视频,看书籍和视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。
在这里插入图片描述

在这里插入图片描述

👉学会后的收获:👈
• 基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;

• 能够利用大模型解决相关实际项目需求: 大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;

• 基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能, 学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;

• 能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力: 大模型应用开发需要掌握机器学习算法、深度学习框架等技术,这些技术的掌握可以提高程序员的编码能力和分析能力,让程序员更加熟练地编写高质量的代码。

👉获取方式:
😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费】🆓

在这里插入图片描述

  • 30
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
原生Spring没有直接支持Fastjson对返回的时间戳进行格式化的功能。然而,可以通过自定义一个ResponseBodyAdvice实现这一功能。 首先,需要创建一个步骤如下的类: ``` @ControllerAdvice public class DateSerializerAdvice implements ResponseBodyAdvice<Object> { private final Converter<String, Date> dateConverter; public DateSerializerAdvice() { this.dateConverter = new DateConverter(); } @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return true; } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { if (body == null) { return null; } if (body instanceof String) { return body; } if (body instanceof Collection) { Collection<Object> collection = (Collection<Object>) body; collection.forEach(this::serializeTimestampField); return collection; } serializeTimestampField(body); return body; } private void serializeTimestampField(Object object) { try { BeanInfo beanInfo = Introspector.getBeanInfo(object.getClass()); PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { Method readMethod = propertyDescriptor.getReadMethod(); Method writeMethod = propertyDescriptor.getWriteMethod(); if (readMethod == null || writeMethod == null) { continue; } if (readMethod.getReturnType() != Long.class) { continue; } if (writeMethod.getParameterTypes().length != 1 || writeMethod.getParameterTypes()[0] != Date.class) { continue; } Long timestamp = (Long) readMethod.invoke(object); if (timestamp == null) { continue; } Date date = dateConverter.convert(timestamp.toString()); writeMethod.invoke(object, date); } } catch (Exception e) { // 日志处理 } } } ``` 上述类通过实现ResponseBodyAdvice接口,并重写beforeBodyWrite方法实现了对返回的对象中的时间戳字段进行格式化。 在beforeBodyWrite方法中,先判断返回值类型是否为字符串或者null,如果是则直接返回原值。 如果返回值类型为集合,则遍历集合中的每个元素并调用serializeTimestampField方法进行处理。 serializeTimestampField方法中,我们利用Java内省机制寻找对象中类型为Long的字段,并检查其是否有对应的set方法。如果存在,则调用Fastjson提供的DateConverter将时间戳字符串转换为Date对象,并调用set方法设置格式化好的Date值。 最后,在Spring的配置文件中,通过以下配置将我们的自定义类进行注册: ``` @Configuration public class DateSerializerConfig implements WebMvcConfigurer { @Autowired private DateSerializerAdvice dateSerializerAdvice; @Override public void addInterceptors(InterceptorRegistry registry) { } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { } @Override public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) { handlers.add(dateSerializerAdvice); } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { } } ``` 上述配置类中,我们将自定义的类通过addReturnValueHandlers方法添加到返回值处理器中即可。 通过以上步骤,我们就成功实现了原生Spring框架下对返回的时间戳进行格式化的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员辣条

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值