Spring AI从入门到放弃(2)

1. 进入正题

目前spring-ai的版本是1.0.0-SNAPSHOT,以下的使用,如果没有特殊指出,使用的AI模型都是OpenAI。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0-SNAPSHOT</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

引入Spring AI Starter:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>

application.yml增加配置

spring:
  ai:
    openai:
      # 这里就是上文提到的Token,是与money挂钩的,注意别暴露给别人,使用是收费滴
      api-key: ${OPNEAI-TOkEN}
      base-url: ${OPNEAI-URL}

使用国外的AI Model需要魔法,可以自行去淘宝啥的买一个,注意由于是https请求,会找不到证书,把第三方平台的证书下载后放到java的信任目录下即可。

keytool -import -alias apikey -file "path\cert" -keystore "path\cacerts" -storepass changeit

2. Chat Client API

Chat Client提供了一组Fluent API,用于与模型进行通信,同时支持同步与响应式编程。
Fluent API具有构建Prompt功能的方法,包含指导模型的输出和行为的说明文本,从API的角度可以讲Prompt理解为消息集合。
AI 模型的消息主要有两种类型:

  1. 用户消息:来自用户端直接输入;
  2. 系统消息:系统生成以指导对话。

这些消息通常使用占位符,在运行的时候根据用户的输入进行替换,以自定义AI模型对用户输入的响应。还可以指定提示选项,比如使用AI模型的名称,和控制所生成的输出的随机性创造性的设置等。

Chat Client API可以理解为Spring AI提供的一套通用的AI API。

2.1 创建Chat Client

Spring AI提供了多种Chat Client的创建方式,其都是通过ChatClient.Builder创建的,但是可以通过不同的方式进行创建。

1. 使用自动配置的ChatClient.Builder

@RestController
@RequestMapping("/ai")
public class AiController {

    private final ChatClient chatClient;

    public AiController(ChatClient.Builder chatClientBuilder) {
        // 使用ChatClient.Builder进行创建
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/chat01")
    public String chat01(@RequestParam("message") String message) {
        return this.chatClient.prompt()
                // 用户输入的内容
                .user(message)
                // 向AI Model发送请求
                .call()
                // 将AI返回的结果输出为String
                .content();
    }

}

这只是一个简单的示例,使用ChatClient.Builder创建的ChatClient是通过自动装配进行Builder的,该方式不利于一个多同时使用多个聊天模型。
通过查看ChatClient.Builder.builder()可以发现其也是通过创建一个ChatModel后进行注入的。

class Builder {

    private final ChatClientRequest defaultRequest;

    private final ChatModel chatModel;
    
    // ....
    public ChatClient build() {
        return new DefaultChatClient(this.chatModel, this.defaultRequest);
    }
    // ...
}

因此我们也可以自行通过ChatModel进行创建。

2. 通过ChatModel创建ChatClient

使用该种方式首先要在application.yml中增加属性spring.ai.chat.client.enabled=false禁用自动配置

@GetMapping("/chat02")
public String chat02(@RequestParam("message") String message) {
    String url = "https://api.openai.com";
    String token = "sk-*************************";
    ChatModel chatModel = new OpenAiChatModel(new OpenAiApi(url, token));

    //
    ChatClient.Builder builder = ChatClient.builder(chatModel);
    ChatClient chatClientByBuilder = builder.build();

    // 使用ChatClient.create()创建
    // ChatClient chatClientByCreate = ChatClient.create(chatModel);

    return chatClientByBuilder.prompt()
            .user(message)
            .call()
            .content();
}

2.2 响应

ChatClient提供了多种方式进行格式化AI的输出。

1. 返回ChatResponse

上文中的chatClientByBuilder.prompt().user(message).call().content()返回的是一个String,我们可以到content()方法中看一下:

public String content() {
    return doGetChatResponse().getResult().getOutput().getContent();
}
private ChatResponse doGetChatResponse() {
    return this.doGetChatResponse(this.request, "");
}

我们可以发现其就是通过返回ChatResponse之后获取的。
ChatResponse定义了丰富的结构,包括如何生成响应的元数据,还可以包含多个相应,每个响应都有自己的元数据。元数据包含用于创建响应的Tokens。
可以直接调用chatResponse()来获取ChatResponse对象。

@GetMapping("/chat02")
public ChatResponse  chat02(@RequestParam("message") String message) {
    return this.chatClient.prompt()
            .user(message)
            .call()
            .chatResponse();
}

ChatResponse后续再看吧。

2. 返回实体

大多时候,我们希望AI返回的数据能够自动映射成一个实体。public <T> T entity(Class<T> type)的作用就是将返回映射成一个实体。

@Builder
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class ActorFilms {
    String actor;
    List<String> movies;
}
@GetMapping("/chat03")
public ActorFilms chat03(@RequestParam("message") String message) {
    return this.chatClient.prompt()
            .user(message)
            .call()
            .entity(ActorFilms.class);
}

这时候我们发现输出就是一个实体,注意这里是因为我问了一个可以返回列表的问题,当我们问的问题无法解析成一个实体时,是没用的。

{
	"actor": "周星驰",
	"movies": [
		"大话西游",
		"功夫",
		"喜剧之王"
	]
}

entity()还提供了其他重载的方法,如下:

public <T> T entity(ParameterizedTypeReference<T> type) {
    return doSingleWithBeanOutputConverter(new BeanOutputConverter<T>(type));
}

public <T> T entity(StructuredOutputConverter<T> structuredOutputConverter) {
    return doSingleWithBeanOutputConverter(structuredOutputConverter);
}

private <T> T doSingleWithBeanOutputConverter(StructuredOutputConverter<T> boc) {
    var chatResponse = doGetChatResponse(this.request, boc.getFormat());
    var stringResponse = chatResponse.getResult().getOutput().getContent();
    return boc.convert(stringResponse);
}

public <T> T entity(Class<T> type) {
    Assert.notNull(type, "the class must be non-null");
    var boc = new BeanOutputConverter<T>(type);
    return doSingleWithBeanOutputConverter(boc);
}

我们可以发现其最终都是调用的private <T> T doSingleWithBeanOutputConverter(StructuredOutputConverter<T> boc) ,这里插个眼,后面再看。

3. 返回响应流(stream())

通过stream()可以获取异步的返回结果。

@GetMapping(value= "/chat05", produces="text/html;charset=UTF-8")
public Flux<String> chat05(@RequestParam("message") String message) {
    return chatClient.prompt()
            .user(message)
            .stream()
            .content();
}

注意,不再使用call()方法调用模型API。
同时也可以使用Flux<ChatResponse> chatResponse()流式获取ChatResponse。

@GetMapping(value = "/chat06", produces = "text/html;charset=UTF-8")
public Flux<ChatResponse> chat06(@RequestParam("message") String message) {
    return chatClient.prompt()
            .user(message)
            .stream()
            .chatResponse();
}

消息格式化》》》待补充

2.3 系统设置

系统消息主要是指默认的系统消息,用以指导对话的消息。通过设置默认值,我们只需要在调用ChatClient时候指定User,无需为运行时的每个请求设置系统消息。

1. 默认系统消息

使用@Configuration配置一个ChatClient,并设置默认系统消息。

@Configuration
public class Config {

    @Bean
    ChatClient chatClient(ChatClient.Builder builder) {
        return builder.defaultSystem("你是一个外科医生,总是非常的忙碌。")
                .build();
    }

}

然后我们就可以使用注解注入一个ChatClient了。

@Autowired
private ChatClient chatClient;


@GetMapping(value = "/chat08")
public Flux<ChatResponse> chat08(@RequestParam(value = "message",defaultValue = "讲一个笑话吧") String message) {
    return chatClient.prompt()
            .user(message)
            .stream()
            .chatResponse();
}

2. 带参系统消息

我们在设置了默认系统消息时,很多时候需要根据我们的业务去定制个性化的系统消息,这时候就可以使用带参数的系统消息。

@Bean
ChatClient chatClient(ChatClient.Builder builder) {
    return builder.defaultSystem("你是一个{job},总是非常的忙碌。")
            .build();
}

然后我们可以根据默认系统消息的占位符去修改我们的系统消息。

@GetMapping(value = "/chat09")
public String chat09(@RequestParam(value = "message", defaultValue = "讲一个笑话吧") String message,
                     @RequestParam(value = "job", defaultValue = "厨师") String job) {
    return chatClient.prompt()
            .system(sp -> sp.param("job", job))
            .user(message)
            .call()
            .content();
}

system()方法有以下重载方法:

public ChatClientRequest system(String text) ;

public ChatClientRequest system(Resource textResource, Charset charset);

public ChatClientRequest system(Resource text);

public ChatClientRequest system(Consumer<SystemSpec> consumer);

2.4 其他设置

1. Options

默认设置是defaultOptions,defaultOptions(ChatOptions chatOptions)入参ChatOptions定义的可移植选项或者特定模型的选项。例如OpenAiChatOptions定义的就是OpenAI的选项。
可以使用options(ChatOptions chatOptions)覆盖默认设置。

2. Function

默认有两个,分别是defaultFunction(String name, String description, java.util.function.Function<I, O> function)defaultFunctions(String… functionNames);前者用于在用户文本中引用的函数;或者则是定义的java.util.Function的bean名称。
可以使用function(…)覆盖默认设置。

3. User

默认有三个,分别是defaultUser(String text), defaultUser(Resource text), defaultUser(Consumer<UserSpec> userSpecConsumer),用于定义不同的用户消息。
可以使用user(…)覆盖默认设置。

4. Advisors

默认有俩,defaultAdvisors(RequestResponseAdvisor...advisor),defaultAdvisors(Consumer<AdvisorSpec> advisorSpecConsumer)具体作用后续介绍。
可以使用advisors(RequestResponseAdvisor… advisor);advisors(Consumer<AdvisorSpec> advisorSpecConsumer)覆盖默认设置。

2.5 Advisors

使用用户文本调用AI时候,一种常见的模式就是使用上下文数据追加或者增强提示。
此上下文可以是不同的类型,常见类型主要有:

  • 自己的数据:AI模型还没有训练过的数据,即使模型看到了类似的数据,在生成响应时,附加的上下文数据也会优先。
  • 对话历史记录:聊天模型的API是无状态的,如果你告诉了AI你的名字,他在后续的交互中也不会记住你的,所以必须于每一个请求一起发送对话历史记录,才能确保在生成响应时考虑以前的交互。

2.6 RAG 检索增强

2.7 Chat Memory

接口ChatMemory表示聊天对话历史记录的存储,他用于向会话添加消息,从历史会话中检索消息以及清除会话历史记录。有一个实现类InMemoryChatMemory,就是为聊天对话历史记录提供内存中存储。

3. Chat Model API

聊天模型API使得我们可将聊天AI集成到我们的程序中,利用预训练的语言模型,例如GPT,以自然语言生成类似人类的响应;ChatModel AI的工作原理就是向AI发送提示或者部分对话,然后AI根据训练数据对自然语言的理解生成对话,然后返回给程序,这样我们就可以进一步处理了。

3.1 一些API

主要是一些接口,具体是实现类需要根据使用的模型进行实现。

1.ChatModel

入参出参都使用String,简化了使用方法,避免了复杂的Prompt和ChatResponse的使用。
当需要使用Prompt时,则返回ChatResponse。

public interface ChatModel extends Model<Prompt, ChatResponse> {

	default String call(String message) {// implementation omitted
	}

    @Override
	ChatResponse call(Prompt prompt);
}

2.StreamChatModel

使用stream()方法,更加快捷的返回。

public interface StreamingChatModel extends StreamingModel<Prompt, ChatResponse> {
    @Override
	Flux<ChatResponse> stream(Prompt prompt);
}

3.Prompt

Prompt是一个ModelRequest,封装了Message对象和ChatOptions。可以更加方便的使用。

public class Prompt implements ModelRequest<List<Message>> {

    private final List<Message> messages;

    private ChatOptions modelOptions;

	@Override
	public ChatOptions getOptions() {..}

	@Override
	public List<Message> getInstructions() {...}

    // constructors and utility methods omitted
}

4.Message

Message则封装了一个文本消息、一个Media集合、以及一个MessageType。

public interface Message extends Node<String> {

   String getContent();

   List<Media> getMedia();

   MessageType getMessageType();
}

Message接口具有各种实现类,这些实现类可以处理不同的消息类别,进而可以用在不同大AI模型上。例如OpenAI完成聊天后,可以根据对话角色区分消息类别,并由MessageType映射到具体的角色上。

5.ChatOptions

ChatOptions用于给AI传入选项,是ModelOptions的子类。

public interface ChatOptions extends ModelOptions {

	Float getTemperature();
	void setTemperature(Float temperature);
	Float getTopP();
	void setTopP(Float topP);
	Integer getTopK();
	void setTopK(Integer topK);
}

此外,每个特定于模型的 ChatModel/StreamingChatModel 实现都可以有自己的选项,这些选项可以传递给 AI 模型。例如,OpenAI 聊天完成模型有自己的选项,如 presencePenaltyfrequencyPenaltybestOf 等。

6.ChatResponse

ChatResponse是AI的输出,每个 Generation 实例都包含由单个提示生成的可能多个输出之一。ChatResponse 类还带有有关 AI 模型响应的 ChatResponseMetadata 元数据。

public class ChatResponse implements ModelResponse<Generation> {

    private final ChatResponseMetadata chatResponseMetadata;
	private final List<Generation> generations;

	@Override
	public ChatResponseMetadata getMetadata() {...}

    @Override
	public List<Generation> getResults() {...}

    // other methods omitted
}

7.Generation

Generation类则是从 ModelResult 扩展,以表示AI输出消息的响应的元数据。不理解,后面再说吧。

public class Generation implements ModelResult<AssistantMessage> {

	private AssistantMessage assistantMessage;
	private ChatGenerationMetadata chatGenerationMetadata;

	@Override
	public AssistantMessage getOutput() {...}

	@Override
	public ChatGenerationMetadata getMetadata() {...}

    // other methods omitted
}

3.2 OpenAI

Spring AI支持OpenAI的ChatGPT,ChatGPT是一个文本生成的模型。

1. 依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>

2.Chat Properties

  1. Retry Properties
    OpenAI重试机制的相关配置
  • spring.ai.retry.max-attempts: 重试的最大次数, 默认为10次
  • spring.ai.retry.on-client-error: 如果为 false,则引发 NonTransientAiException,并且不要尝试重试 4xx 客户端错误代码
  • spring.ai.retry.backoff.initial-interval: 重试尝试的最大次数
  • spring.ai.retry.backoff.multiplier: 回退间隔乘数
  • spring.ai.retry.backoff.max-interval: 最大回退持续时间
  • spring.ai.retry.exclude-on-http-codes:不应触发重试的 HTTP 状态代码列表(例如,抛出 NonTransientAiException)
  • spring.ai.retry.on-http-codes: 应触发重试的 HTTP 状态代码列表(例如,抛出 TransientAiException)
  1. Connection Properties
  • spring.ai.openai.base-url: Oneapi Url
  • spring.ai.openai.api-key: API key
  1. Configuration Properties
    OpenAI Chat相关配置
  • spring.ai.openai.chat.enabled: 启用OpenAI聊天模型
  • spring.ai.openai.chat.base-url: 用于覆盖spring.ai.openai.base-url,给Chat提供特定的url
  • spring.ai.openai.chat.api-key:用于覆盖spring.ai.openai.api-key,给Chat提供特定的key
  • spring.ai.openai.chat.options.model: 聊天模型的选择(gpt-4o, gpt-4-turbo, gpt-4-turbo-2024-04-09, gpt-4-0125-preview, gpt-4-turbo-preview, gpt-4-vision-preview, gpt-4-32k, gpt-3.5-turbo, gpt-3.5-turbo-0125, gpt-3.5-turbo-1106)
  • spring.ai.openai.chat.options.temperature: 用于控制创造力,越大,越具有创造力,默认为0.8。
  • spring.ai.openai.chat.options.frequencyPenalty: 介于 -2.0 和 2.0 之间的数字,目前为止,正值会根据新标记在文本中的现有频率来惩罚新标记,从而降低模型逐字重复同一行的可能性。不理解,试试吧
  • spring.ai.openai.chat.options.maxTokens: 聊天完成时要生成的最大令牌数。输入令牌和生成的令牌的总长度受模型上下文长度的限制。
  • spring.ai.openai.chat.options.n: 要为每个输入消息生成多少个聊天完成选项。请注意,您将根据所有选项中生成的令牌数量付费。将 n 保持为 1 以最大程度地降低成本。
  • spring.ai.openai.chat.options.presencePenalty:介于 -2.0 和 2.0 之间的数字。正值会根据新标记到目前为止是否出现在文本中来惩罚它们,从而增加了模型谈论新主题的可能性。
  • … 还有好多,自己去官网看吧

3.RuntTime Options

OpenAiChatOptions.java提供了模型配置,例如使用的模型、温度、频率损失等。可以通过配置spring.ai.openai.chat.options.*或者使用OpenAiChatModel构造函数配置默认项。
可以在代码中通过向Prompt调用,添加自己的Option以覆盖默认的设置。

spring:
  ai:
    openai:
      chat:
        options:
          model: gpt-4-turbo
          temperature: 0.9

使用OpenAiChatModel build:

@GetMapping("/chat02")
public ChatResponse chat02(@RequestParam("message") String message) {
    return chatModel
            .call(
                    new Prompt(
                            message,
                            OpenAiChatOptions.builder()
                                    .withModel("gpt-3.5-turbo")
                                    .withTemperature(0.4F)
                                    .build()
                    ));
}

4. Function Calling

可以使用 OpenAiChatModel 注册自定义 Java 方法,并让 OpenAI 模型智能地选择输出包含参数的 JSON 对象,以调用一个或多个已注册的方法。

可以使用 OpenAiChatModel 注册自定义 Java 方法,并让 OpenAI 模型智能地选择输出包含参数的 JSON 对象,以调用一个或多个已注册的函数。我们可以将功能LLM与外部工具和 API 连接起来。OpenAI 模型经过训练,可以检测何时应调用函数,并使用遵循函数签名的 JSON 进行响应。
OpenAI API 不直接调用该函数;相反,模型会生成 JSON,可以使用该 JSON 在代码中调用函数,并将结果返回给模型以完成对话。

Spring AI 提供了灵活且用户友好的方法来注册和调用自定义函数。通常,自定义函数需要提供函数名称描述和函数调用签名(作为 JSON 模式),以便让模型知道函数需要哪些参数。该描述可帮助模型了解何时调用函数。

我们在使用时候只需要将该方法作为参数告诉AI即可。就像定义Funcation一样,注入一个Bean,然后在调用ChatModel时,将该Bean名称作为Option即可。

下面以获取各个城市的温度为例:

1)实现方法

apply将用于调用天气API,这里全都默认设置为30。

public class MockWeatherService implements Function<Request, Response> {

	public enum Unit { C, F }
	public record Request(String location, Unit unit) {}
	public record Response(double temp, Unit unit) {}

	public Response apply(Request request) {
		return new Response(30.0, Unit.C);
	}
}
2)将方法注册为Bean
  1. 使用@Bean直接注入方法
@Configuration
public class Config {

	@Bean
	@Description("Get the weather in location") // function description
        public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction1() {
		return new MockWeatherService();
	}
	...
}

其中@Description是可选的,用于提供方法描述,可以帮助AI理解何时调用该方法。
除了通过注解的方式帮助AI理解何时调用该方法,还可以在Request中增加注解来提供方法的描述:

@JsonClassDescription("Get the weather in location") // 等价于使用@Description
public record Request(String location, Unit unit) {}
  1. 使用FunctionCallbackWrapper注入方法

FunctionCallbackback主要是创建一个FunctionCallbackWrapper,然后注入该方法。

@Configuration
public class Config {
	@Bean
	public FunctionCallback weatherFunctionInfo() {
		return new FunctionCallbackWrapper<>("CurrentWeather",
				"Get the weather in location", 
				(response) -> "" + response.temp() + response.unit(), 
				new MockWeatherService());
	}
	...
}
3)将Bean作为Option
@GetMapping("/chat03")
public String chat03(@RequestParam("message") String message) {
    UserMessage userMessage = new UserMessage("哈尔滨、三亚和巴黎的天气怎么样?");
    return chatModel.call(new Prompt(List.of(userMessage),
                    OpenAiChatOptions
                            .builder()
                            .withFunction("WeatherInfo").build()))
            .getResult().getOutput().getContent(); // (1) Enable the function

}

就像上文提到的Prompt中封装了ChatOptions一样,将设置Funtions。

4)动态注册回调方法

通过动态的注册方法,我们可以根据用户输入动态的选择要调用的不同发方法。

@GetMapping("/chat04")
public ChatResponse chat04(@RequestParam("message") String message) {


    UserMessage userMessage = new UserMessage("哈尔滨、三亚和巴黎的天气怎么样?");

    OpenAiChatOptions promptOptions = OpenAiChatOptions.builder()
            .withFunctionCallbacks(List.of(FunctionCallbackWrapper
                    .builder(new MockWeatherService())
                    .withName("WeatherInfo")
                    .withDescription("获取当地温度")
                    .withResponseConverter((response) -> "" + response.temp() + response.unit())
                    .build()))
            .build();

    return chatModel.call(new Prompt(List.of(userMessage), promptOptions));

}

5. Multimodal 多模态

多模态是指AI同时理解和处理不同来源的信息的能力,包括文本、图像和音频等。目前OpenAI支持多模态的模型有:gpt-4-visual-previewgpt-4o
OpenAI 用户消息 API可以将 base64 编码的图像列表或图像 url 与消息合并。Spring AI 的 Message接口通过引入Media 类型来促进多模态 AI 模型。此类型包含有关消息中媒体附件的数据和详细信息,利用 Spring 的 org.springframework.util.MimeTypejava.lang.Object 来获取原始媒体数据。

@GetMapping("/chat05")
public ChatResponse chat05(@RequestParam("message") String message) throws IOException {

    ClassPathResource classPathResource = new ClassPathResource("/multimodal.test.png");

    var userMessage = new UserMessage("Explain what do you see on this picture?",
            List.of(new Media(MimeTypeUtils.IMAGE_PNG, classPathResource)));

    return chatModel.call(new Prompt(List.of(userMessage),
       OpenAiChatOptions.builder().withModel(OpenAiApi.ChatModel.GPT_4_VISION_PREVIEW.getValue()).build()));

}

GPT_4_O没买,就先不试了,要注意new Media(MimeTypeUtils.IMAGE_PNG, imageData)1.0.0 M1就弃用了,可以用其他方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值