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 模型的消息主要有两种类型:
- 用户消息:来自用户端直接输入;
- 系统消息:系统生成以指导对话。
这些消息通常使用占位符,在运行的时候根据用户的输入进行替换,以自定义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 聊天完成模型有自己的选项,如 presencePenalty
、frequencyPenalty
、bestOf
等。
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
- 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)
- Connection Properties
- spring.ai.openai.base-url: Oneapi Url
- spring.ai.openai.api-key: API key
- 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
- 使用@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) {}
- 使用
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-preview
和 gpt-4o
。
OpenAI 用户消息 API可以将 base64 编码的图像列表或图像 url 与消息合并。Spring AI 的 Message接口通过引入Media 类型来促进多模态 AI 模型。此类型包含有关消息中媒体附件的数据和详细信息,利用 Spring 的 org.springframework.util.MimeType
和 java.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
就弃用了,可以用其他方法。