跳到主要内容

SpringAI

SpringAI介绍

官网:https://docs.spring.io/spring-ai/reference/

  • Spring AI项目旨在简化包含人工智能功能的应用程序的开发,避免不必要的复杂性。

  • Spring AI 提供了抽象,作为开发 AI 应用程序的基础。这些抽象有多种实现,可以通过最少的代码更改轻松进行组件交换。

关于ChatClient和ChatModel:

  • ChatClient是SpringAI 0.8.0版本的概念,到1.0.0版本变成了ChatModel,但同时保留了ChatClient,
  • ChatClient底层还是调用ChatModel,ChatClient支持Fluent Api,ChatModel不支持。两者都是表示某个模型,具体是什么模型,需要看配置。

可以理解为ChatClient是在ChatModel的基础上进一步封装,所以我们尽量使用ChatClient即可

模型接入

统一管理各大厂商的依赖:

<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.0-M2</spring-ai.version>
</properties>

<repositories>
<repository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<enabled>true</enabled>
</releases>
</repository>
<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>
<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>

统一配置使用

比如我现在又有OpenAI的,又有通义千问的,那么可以去定义多个Client,按照名称来注入,这样使用的时候,也是按照名称去注入对应的Client

package com.yunfei.ai.controller.config;

import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author houyunfei
*/
@Configuration
public class ChatConfig {

@Resource(name = "dashscopeChatModel")
private ChatModel dashscopeChatModel;


@Resource(name = "openAiChatModel")
private ChatModel openAiChatModel;

@Bean("openAiChatClient")
public ChatClient chatClient() {
return ChatClient.create(openAiChatModel);
}

@Bean("dashscopeChatClient")
public ChatClient dashscopeChatClient() {
return ChatClient.create(dashscopeChatModel);
}
}

整合OpenAI

官网:https://docs.spring.io/spring-ai/reference/api/chat/openai-chat.html

免费Key网站:https://api.xty.app/register?aff=5tgH

导入依赖

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

导入配置

spring.ai.openai.base-url=https://api.xty.app
spring.ai.openai.api-key=xxx
spring.ai.openai.chat.options.model=gpt-3.5-turbo

使用

使用的时候只需要注入,然后就可以使用里面提供的方法了

整合灵积(通义千问)

获取api-key:https://dashscope.console.aliyun.com/apiKey

文档:https://sca.aliyun.com/ai/get-started/

导入依赖

<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>

导入配置

spring.ai.dashscope.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
spring.ai.dashscope.api-key=ccc
spring.ai.dashscope.chat.options.model=qwen-long

实现聊天功能

阻塞式实现

构建一个Prompt,然后调用call即可实现阻塞式等待拿到结果,content就是输出的内容。这种方式非常简单,但是会等待很久拿不到结果

@Resource(name = "dashscopeChatClient")
private ChatClient chatClient;
@GetMapping("/generate")
public String generate(@RequestParam(value = "message", defaultValue = "给我讲个笑话") String message) {
// 使用chatClient生成对话
Prompt prompt = new Prompt(message);
String res = chatClient.prompt(prompt).call().content();
return res;
}

也可以用下面的方式:

chatClient.prompt().messages(new UserMessage(message)).call().content();

流式实现

@GetMapping("/generateStream")
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "给我讲个笑话") String message) {
// 使用chatClient生成对话
Prompt prompt = new Prompt(message);
Flux<String> stream = chatClient.prompt(prompt).stream().content();
return stream;
}
  • Spring WebFlux 会等到 Flux<String> 流的数据完全生成后,将其合并为一个响应,并作为一个完整的 HTTP 响应返回给前端(类似于JSON数组)。
  • 这种行为对前端来说,无法实现逐条接收和实时显示流数据。
  • 想要前端实现ChatGPT官网的效果,就需要使用SSE推流

SSE推流

前端可以使用EventSource来实现SSE,但是这种方式只支持GET请求,由于用户一次性可能会输入很多内容,GET请求的URL有限制,所以我们最好要去找到一种支持Post请求的库,可以使用微软的库:

如果要使用html直接使用,可以使用别人改过的:

后端实现:

    @Resource(name = "dashscopeChatClient")
private ChatClient chatClient;
/**
* 使用SSE(Server-Sent Events)实现聊天
*
* @param chatRequest 用户请求
* @return 聊天响应
*/
@PostMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<ChatResponse>> sse(@RequestBody ChatRequest chatRequest) {
// 使用chatClient生成对话
Prompt prompt = new Prompt(chatRequest.getUserText());
Flux<ServerSentEvent<ChatResponse>> stream = chatClient
.prompt(prompt)
.stream()
.chatResponse()
.map(chatResponse -> {
ServerSentEvent<ChatResponse> sentEvent = ServerSentEvent
.builder(chatResponse)
.event("message")
.build();
return sentEvent;
});
return stream;
}

前端实现index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ChatGPT</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/@sentool/fetch-event-source/dist/index.min.js"></script>
</head>
<body class="bg-gray-100 text-gray-800">
<div class="max-w-4xl mx-auto p-6">
<h1 class="text-4xl font-bold text-center mb-4">聊天响应流</h1>
<div class="bg-white p-4 rounded-lg shadow-md">
<h2 class="text-2xl font-semibold mb-4">用户输入:</h2>
<input id="userText" type="text" class="w-full p-2 border rounded-lg mb-4" placeholder="请输入您的问题"
value="hello"/>
<button id="startButton" class="w-full bg-blue-500 text-white p-2 rounded-lg">开始聊天</button>
<h2 class="text-2xl font-semibold mt-6 mb-4">聊天响应:</h2>
<div id="chatOutput" class="h-60 overflow-y-scroll bg-gray-50 p-4 rounded-lg border border-gray-200"></div>
</div>
</div>

<script>
document.getElementById('startButton').addEventListener('click', async () => {
const {fetchEventSource} = FetchEventSource;
const userText = document.getElementById('userText');
const chatOutput = document.getElementById('chatOutput');
chatOutput.innerHTML = ''; // 清空聊天区域
fetchEventSource("/ai/sse", {
method: 'POST',
body: JSON.stringify({userText: userText.value}),
headers: {
'Content-Type': 'application/json'
},
onopen(response) {
console.log('Request has been opened.');
},
onmessage(event) {
if (event.data) {
const data = event.data //这是一个json格式的字符串 包括metadata result results
console.log('data', data);
console.log("data.content", data.result.output.content);
chatOutput.innerHTML += `${data.result.output.content}`;
}
},
onerror(error) {
console.error('Error:', error);
},
done() {
console.log('Stream has completed.');
},
});
});
</script>
</body>
</html>

对话存储

  • 可以自定义一些系统提示词system
  • 使用ChatMemory可以附带一些聊天的历史记录,但是不易过多,会浪费很多token
/**
* 使用SSE(Server-Sent Events)实现聊天 (可以添加上下文)
* @param chatRequest 用户请求
* @return 聊天响应
*/
@PostMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<ChatResponse>> sse2(@RequestBody ChatRequest chatRequest) {
ChatMemory chatMemory = new InMemoryChatMemory();
String sessionId = "1"; // todo 从请求中获取
int maxMessages = 10;
MessageChatMemoryAdvisor advisor = new MessageChatMemoryAdvisor(chatMemory, sessionId, maxMessages);
Flux<ServerSentEvent<ChatResponse>> stream = chatClient
.prompt()
.user(chatRequest.getUserText())
.system(chatRequest.getSystemText())
.advisors(advisor)
.stream()
.chatResponse()
.map(chatResponse -> {
ServerSentEvent<ChatResponse> sentEvent = ServerSentEvent
.builder(chatResponse)
.event("message")
.build();
return sentEvent;
});
return stream;
}

参考资料