Spring AI项目旨在让开发含AI功能的应用变得更简单,去除不必要的复杂环节。它从知名的Python项目(如LangChain和LlamaIndex)中学习了经验,但并非简单复制,而是有自己独特之处。我们相信,未来的AI应用不会仅限于Python开发者,而是会广泛支持多种编程语言。 Spring AI的核心目标是解决AI集成的关键问题——顺畅地将企业数据、API与AI模型连接起来,让这一切变得更加容易实现。
刚接触springAI的时候,它还不支持国内大模型,只能文字生成,既没有chatMemory也没有向量支持,现如今稳定版即将发布,该有的都有了,文生图,读图,文生语音,语音转文字,function等,比起langchain4j,api用起来舒服些,关键它是spring出品的。
官网文档:https://docs.spring.io/spring-ai/reference/getting-started.html
1、引入相关包
1.1 指定版本
听说这个月会发布稳定的1.0版本,敬请期待,目前最新的是快照版,1.0.0-M2好多功能还没有。jdk版本需要17+
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-SNAPSHOT</spring-ai.version>
<!-- <spring-ai.version>1.0.0-M2</spring-ai.version>-->
</properties>
1.2 引入bom
Spring AI材料清单(BOM)声明了给定Spring AI发行版所使用的所有依赖项的推荐版本。使用来自应用程序构建脚本的BOM避免了您自己指定和维护依赖版本的需要。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
1.3 引入自动配置模块
引入对应大模型的依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI Open AI 自动配置模块 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
</dependencies>
如果是百度千帆
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-qianfan-spring-boot-starter</artifactId>
</dependency>
如果是智普
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
</dependency>
1.4 配置仓库
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
1.5 在application.properties配置key
server.port=8080
#Open AI的配置
spring.ai.openai.base-url=your model url
spring.ai.openai.api-key=your key
spring.ai.openai.chat.options.model=gpt-3.5-turbo
# 调用api接口失败后的重试次数
spring.ai.retry.max-attempts=1
#spring.ai.openai.image.options.model=dall-e-3
# 打印详细日志
logging.level.org.springframework.ai.chat.client.advisor=DEBUG
2、hello world
ChatModel是一个接口,每个大模型都有它的实现类,open AI对应的是OpenAiChatModel。spring boot会帮我们自动实例化它,直接使用即可。
// @Autowired
// private OpenAiChatModel chatModel;
@Autowired
private ChatModel chatModel;
@GetMapping("/hello")
public String helloWorld(@RequestParam("message") String message) {
return chatModel.call(message);
}
在浏览器输入:http://localhost:8080/hello?message=你是谁
浏览器显示如下
我是一个AI智能助手,可以帮助您回答问题和提供信息。您有什么需要我帮忙的吗?
3、chatClient
直接使用chatModle来编写复杂的业务是繁琐的, ChatClient提供了一个流畅的API来与AI模型进行通信 。它封装了chatModel,chatClient实例使用ChatClient.Builder来获取,我们使用chatClient来改写上面的代码
@RestController
public class HelloWorld {
private final ChatClient chatClient1;
public HelloWorld(ChatClient.Builder builder) {
this.chatClient1 = builder.build();
}
@GetMapping("/hello")
public String helloWorld(@RequestParam("message") String message) {
return chatClient1.prompt().user(message).call().content();
}
}
好像还变复杂了,对吧
流式输出,就是文字一个一个的输出,提升等待体验
@GetMapping("/test3")
public Flux<String> test3(@RequestParam("message") String message) {
return chatClient1.prompt().user(message).stream().content();
}
3.1 设置系统提示词
chatModel有4类消息,系统消息(配置模型能力),用户消息,AI回复消息,工具扩展消息(调用本地方法)
关于这些基础知识参考另一篇文章:https://blog.csdn.net/dan_seek/article/details/140755726
或者官网:https://docs.spring.io/spring-ai/reference/concepts.html
使用bean方式来实例化chatClient
@Configuration
public class Config {
@Bean
ChatClient chatClient2(ChatClient.Builder builder){
return builder
// 指定系统提示词
.defaultSystem("你是位资深诗人").build();
}
}
控制器
private final ChatClient chatClient2;
public HelloWorld(ChatClient chatClient2) {
this.chatClient2 = chatClient2;
}
@GetMapping("/test1")
public String test1(@RequestParam("message") String message) {
return chatClient2.prompt().user(message).call().content();
}
浏览器输入:http://localhost:8080/test1?message=写首歌颂小草的短诗
浏览器显示如下
小小的草儿绿叶茂,生长在田野间小道。 风吹过,摇曳舞,自由自在展翅飞。 细腻的叶片婀娜姿,静静地守护大地。 无声无息默默劳,润泽万物生生不息。 小草啊,你是大自然的守护神, 你的美丽与坚强,让人心生敬仰。
使用占位符传参
如果系统提示词是动态的,可以通过传参的方式
@Bean
ChatClient chatClient3(ChatClient.Builder builder){
return builder.defaultSystem("请模仿{name}的口吻回答问题,并告诉用户你的名字").build();
}
控制器
@Autowired
private ChatClient chatClient3;
@GetMapping("/test2")
public String test2(@RequestParam("message") String message) {
String name = "庄子";
return chatClient3.prompt()
// 设置系统提示词参数
.system(sp -> sp.param("name", name))
.user(message).call().content();
}
浏览器输入:http://localhost:8080/test2?message=你的梦想是什么
浏览器显示如下
吾乃庄周也。梦境乃人生之一部分,吾之梦想乃与天地相融,如风如云自在游荡,无拘无束,无忧无虑,与万物共舞共鸣。愿世人亦能放下烦恼,追寻内心最深处的宁静与自由。
3.2 记住历史会话
上面的对话中,你告诉大模型你的名字,然后马上问大模型我是谁,大模型是不知道的。因为大模型是无状态的,想让大模型知道上下文,你需要把历史记录一并发给大模型,借助chatMemory很容易实现。
chatMemory有一个默认的实现类,也可以自己实现一个保存至数据库的实现
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
}
@Bean
ChatClient chatClient4(ChatClient.Builder builder, ChatMemory chatMemory){
return builder.defaultSystem("你是一位友好的助手")
.defaultAdvisors(
// new PromptChatMemoryAdvisor(chatMemory) // 历史记录添加到系统文本中
new MessageChatMemoryAdvisor(chatMemory) // 历史记录添加到消息列表中
)
.build();
}
控制器
@Autowired
private ChatClient chatClient4;
@GetMapping("/test4")
public String test4(@RequestParam("message") String message) {
// memory id 用于区分会话
String chatId = "123456";
return chatClient4.prompt()
.user(message)
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
// 设置记忆的会话长度 10条会话
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
.call().content();
}
3.3 输出详细日志
如果想要查看响应体里的数据结构,需要如下配置,稍作修改上面的chatClient
@Bean
ChatClient chatClient4(ChatClient.Builder builder, ChatMemory chatMemory){
return builder.defaultSystem("你是一位作家")
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory) // 历史记录添加到消息列表中
// 启用日志,需要在配置文件配置 logging.level.org.springframework.ai.chat.client.advisor=DEBUG
,new SimpleLoggerAdvisor(
// 自定义日志输出
// request -> "Custom request: " + request.userText(),
// response -> "Custom response: " + response.getResult()
)
)
.build();
}
控制台日志如下
request: AdvisedRequest[chatModel=OpenAiChatModel [defaultOptions=OpenAiChatOptions: {“streamUsage”:false,“model”:“gpt-3.5-turbo”,“temperature”:0.7}], userText=我是小飞, systemText=你是一位作家, chatOptions=OpenAiChatOptions: {“streamUsage”:false,“model”:“gpt-3.5-turbo”,“temperature”:0.7}, media=[], functionNames=[], functionCallbacks=[], messages=[], userParams={}, systemParams={}, advisors=[org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor@27d1c903, SimpleLoggerAdvisor], advisorParams={chat_memory_response_size=2, chat_memory_conversation_id=123456}]
2024-09-12T00:26:33.525+08:00 DEBUG 560 — [ai-spring] [nio-8080-exec-1] o.s.a.c.c.advisor.SimpleLoggerAdvisor : response: {“result”:{“output”:{“messageType”:“ASSISTANT”,“metadata”:{“refusal”:“”,“finishReason”:“STOP”,“index”:0,“id”:“chatcmpl-A6KSmRO8aLFYHBemBI5Am78dfg7oG”,“role”:“ASSISTANT”,“messageType”:“ASSISTANT”},“toolCalls”:[],“content”:“你好,小飞!很高兴认识你。你喜欢写作吗?”},“metadata”:{“finishReason”:“STOP”,“contentFilterMetadata”:null}},“results”:[{“output”:{“messageType”:“ASSISTANT”,“metadata”:{“refusal”:“”,“finishReason”:“STOP”,“index”:0,“id”:“chatcmpl-A6KSmRO8aLFYHBemBI5Am78dfg7oG”,“role”:“ASSISTANT”,“messageType”:“ASSISTANT”},“toolCalls”:[],“content”:“你好,小飞!很高兴认识你。你喜欢写作吗?”},“metadata”:{“finishReason”:“STOP”,“contentFilterMetadata”:null}}],“metadata”:{“id”:“chatcmpl-A6KSmRO8aLFYHBemBI5Am78dfg7oG”,“model”:“gpt-3.5-turbo-0125”,“rateLimit”:{“requestsLimit”:5000,“requestsRemaining”:4999,“tokensLimit”:2000000,“tokensRemaining”:1999973,“requestsReset”:0.012000000,“tokensReset”:0.0},“usage”:{“generationTokens”:27,“promptTokens”:22,“totalTokens”:49},“promptMetadata”:[],“empty”:false}}
4、生成图片
可以在application.properties全局配置模型的相关参数,也可以在调用的时候指定
@Autowired
ImageModel imageModel;
// @Autowired
// OpenAiImageModel imageModel2;
@GetMapping("/testImage")
public String test1() {
ImageResponse response = imageModel.call(
new ImagePrompt("给spring ai框架生成一个涂鸦" )
);
// 自定义图片参数
// ImageOptions options = OpenAiImageOptions.builder()
// .withModel("dall-e-3")
// .withQuality("hd")
// // 图片数量
// .withN(1)
// // 图片高度 Must be one of 256, 512, or 1024
// .withHeight(512)
// .withWidth(512)
// .build();
//
// ImageResponse response = imageModel.call(
// new ImagePrompt("给spring ai框架生成一个涂鸦", options )
// );
String url =response.getResult().getOutput().getUrl();
System.out.println(url);
return url;
}
5、生成向量
创建知识库时需要把文字转为向量存储到向量数据库中
@Autowired
EmbeddingModel embeddingModel;
@GetMapping("/testEmbed")
public String test1() {
float[] f = embeddingModel.embed("hello world");
System.out.println(Arrays.toString(f));
return Arrays.toString(f);
}
浏览器显示如下
[-0.014903838, 0.0013317588, -0.018488139, -0.031072723, -0.024273094, 0.0075046294, -0.023034403, -0.0010262037, -0.012795426, -0.022441411, 0.025801694, 0.010884678, -0.033075716, -0.0037193708, 0.0058178995, 0.013849632, 0.01941057, -0.022863094, 0.01836954, 0.011326127, -0.0061605168, …
6、大模型调用私有API
通过Function Calling API可以让大模型调用本地的接口,比如问大模型苹果的库存是多少,这个时候就需要大模型调用企业内部接口查询。目前这个功能还不完善,这个API还不支持智普和百度千帆
6.1 定义给大模型调用的服务
固定写法,照着写即可,Request和Response的参数根据实际情况增减。@JsonClassDescription(“获取天气信息”) 注解是给大模型看的,需要描述清楚这个方法是做什么的;@JsonPropertyDescription是描述参数的用法,有时不写也可以,大模型根据上下文能猜对,但是下面的例子如果不写,request.location的值有时是中文有时是拼音
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import java.util.function.Function;
public class MockWeatherService implements Function<MockWeatherService.Request, MockWeatherService.Response> {
/**
* 温度
*/
public enum Unit {
/**
* 摄氏度
*/
C("摄氏度"),
/**
* 华氏度
*/
F("华氏度");
/**
* 人类可读的单位名称
*/
public final String unitName;
private Unit(String text) {
this.unitName = text;
}
}
@JsonClassDescription("获取天气信息") // 工具描述
public record Request(@JsonProperty(required = true,
value = "location") @JsonPropertyDescription("城市名,如北京") String location
) {}
public record Response(double temp, Unit unit) {}
public Response apply(Request request) {
System.out.println("---request.location:" + request.location);
// 调用天气API,只是模拟
if (request.location.contains("北京")){
return new Response(32.0, Unit.C);
} else if (request.location.contains("长沙")) {
return new Response(38.0, Unit.F);
}
return new Response(25.0, Unit.C);
}
}
6.2 定义function
使用bean注入,bean的名字(weatherFunction1)就是函数名
// 天气工具类
@Bean
// @Description("获取天气信息") // 工具的描述,与MockWeatherService.Request的描述二选一
public Function<MockWeatherService.Request, MockWeatherService.Response> weatherFunction1() { // bean的名字就是function的名字
return new MockWeatherService();
}
也可以使用包装类定义function
// 包裹的天气工具类
@Bean
public FunctionCallback weatherFunctionInfo() {
return FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("currentWeatherByWrap") // (1) function 名字,区分大小写
.withDescription("获取天气信息") // (2) function 描述
.build();
}
}
6.3 模型调用函数
使用大模型调用内部定义的函数,有下面几种写法
6.3.1 chatModel
@Autowired
private ChatModel chatModel;
@GetMapping("/tool1")
public String tool1(@RequestParam("message") String message) {
message = message.isEmpty() ? "北京和长沙的天气?" : message;
UserMessage userMessage = new UserMessage(message);
Prompt prompt = new Prompt(List.of(userMessage),
OpenAiChatOptions.builder().withFunction("weatherFunction1").build());
// chatMobile--function用法
ChatResponse response = chatModel.call(prompt);
return response.getResult().getOutput().getContent();
}
在浏览器输入:http://localhost:8080/tool1?message=长沙的天气
浏览器显示如下
长沙现在的温度是38°F。
在浏览器输入:http://localhost:8080/tool1?message=长沙和北京的天气
浏览器显示如下
长沙的天气温度为38°F,北京的天气温度为32°C。
控制台会打印(说明大模型调用了两次MockWeatherService接口)
—request.location:长沙
—request.location:北京
在浏览器输入:http://localhost:8080/tool1?message=敦贝煌的天气
浏览器显示如下(故意写错地名,大模型也能识别)
敦煌目前的气温是25摄氏度。
6.3.2 chatClient
当业务复杂时,chatClient的优势就体现出来了
private final ChatClient chatClient;
public ToolController(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@GetMapping("/tool2")
public String tool2(@RequestParam("message") String message) {
// chatClient--function的用法
String result = chatClient.prompt()
.functions("weatherFunction1").user(message).call().content();
log.info("result: {}", result);
return result;
}
/**
* FunctionCallbackWrapper.builder创建的function
* @param message user message
* @return
*/
@GetMapping("/tool4")
public String tool4(@RequestParam("message") String message) {
// chatClient--function的用法
String result = chatClient.prompt()
// currentWeatherByWrap在Config.java中定义
.functions("currentWeatherByWrap").user(message).call().content();
log.info("result: {}", result);
return result;
}
6.3.3 使用包装类直接定义function
function不事先定义也可以,不过写法有点绕
/**
* 使用包装类FunctionCallbackWrapper直接定义function
* @param message user message
* @return
*/
@GetMapping("/tool3")
public String tool3(@RequestParam("message") String message) {
UserMessage userMessage = new UserMessage(message);
var promptOptions = OpenAiChatOptions.builder()
// .withModel(OpenAiApi.ChatModel.GPT_4_O.getValue()) // 指定模型
.withFunctionCallbacks(List.of(FunctionCallbackWrapper.builder(new MockWeatherService())
.withName("getCurrentWeather") // function的名字,可随意取名
.withDescription("获取城市的天气") // function的描述
// .withResponseConverter((response) -> "" + response.temp() + response.unit())
.build()))
.build();
// chatModel--function用法
ChatResponse response = chatModel.call(new Prompt(List.of(userMessage), promptOptions));
return response.getResult().getOutput().getContent();
// // chatClient--function的用法
// return chatClient.prompt(new Prompt(userMessage, promptOptions)).call().content();
}