6. 实战(二):用Spring AI+OpenAI构建企业级智能客服
1、引言
前面几篇已经加深了我们对Spring Ai的体系结构,核心概念,以及也有初步集成实现了一个简单demo。今天,我们通过使用Spring AI框架与OpenAI API集成,构建一个功能完善的智能对话系统,加深我们对Spring AI从概念到实际代码实现的理解,最终完成一个可运行的智能对话应用。
2、所属环境
- IntelliJ IDEA 2024.3
- JDK 17+
- 硅基流动API,这里需要提前注册申请。如果获取API Key这里就不赘述了,可以看我以往的文章搜索查看。
- SpringBootI 3.4.2
3、代码集成
再次赘述一遍,Spring AI所需要的JDK,必须为17+,我这里使用的是Java 21进行演示。
3.1、Spring Boot添加依赖
如果构建一个初始的Spring Boot项目,这里就不赘述了。默认大家应该都会了。添加Spring Ai相关依赖,以及Spring-web相关依赖:
<dependencies><dependency><groupId>group.springframework.ai</groupId><artifactId>spring-ai-openai-spring-boot-starter</artifactId><version>1.1.0</version><exclusions><exclusion><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-function-context</artifactId></exclusion><exclusion><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-function-core</artifactId></exclusion></exclusions></dependency><dependency><groupId>group.springframework.ai</groupId><artifactId>spring-ai-spring-boot-autoconfigure</artifactId><version>1.1.0</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Boot DevTools (Optional for auto-reloading during development) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>group.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>1.1.0</version><type>pom</type></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.36</version></dependency></dependencies></dependencyManagement>
3.2、配置application.yml
spring:application:name: spring-ai-demoai:openai:# 聊天模型chat:options:# 这里使用deepseek模型 model: deepseek-ai/DeepSeek-V2.5# openai 供应商申请下来的api key api-key: xxxx# 调用openai的接口地址base-url: https://api.siliconflow.cn/ server:servlet:encoding:charset: UTF-8 # 这里强制设置servlet编码为utf-8,避免后续流式输出中文乱码enabled: trueforce: true
3.3、普通对话模式
这个是最常见的对话模式,没有任何的语境前提,没有任何的上下文,就是最简单的一问一答的形式。先来实现Service代码:
@Service
public class ChatService {@Autowiredprivate OpenAiChatModel openAiChatModel;/*** 普通对话* @param message* @return*/public String chat(String message) {// 简单的单轮对话return openAiChatModel.call(new Prompt(message)).getResult().getOutput().getContent();}
}
controller相关代码:
@RestController
@RequestMapping("/api/chat")
public class ChatController {private final ChatService chatService;public ChatController(ChatService chatService) {this.chatService = chatService;}@GetMapping("/simple")public String simpleChat(@RequestParam String message) {return chatService.chat(message);}
}
直接运行看下回显:
curl -i -X GET \'http://localhost:8080/api/chat/simple?message=你是谁'
3.4、上下文对话
上下文对话,需要在对话的时候引入上下文,作为和AI交互的语境。Service相关代码:
public String chatWithContext(String message, String context) {// 带上下文的对话SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate("""你是一个专业的AI助手。请根据以下上下文回答问题:{context}""");Message systemMessage = systemPromptTemplate.createMessage(Map.of("context", context));UserMessage userMessage = new UserMessage(message);Prompt prompt = new Prompt(List.of(systemMessage, userMessage));return openAiChatModel.call(prompt).getResult().getOutput().getContent();
}
Controller代码:
@GetMapping("/with-context")
public String chatWithContext(@RequestParam String message,@RequestParam String context) {return chatService.chatWithContext(message, context);
}
查看结果:
# 当我们在不同的地点询问几点了的时候,看看ai的回答
curl -i -X GET \'http://localhost:8080/api/chat/with-context?message=几点了&context=现在在北京'curl -i -X GET \'http://localhost:8080/api/chat/with-context?message=几点了&context=现在在纽约'
3.5、多轮对话
多轮对话,需要AI大模型记住我们前面的对话记录。多轮对话中,回答和结果会受到前面历史记录的影响。Service代码:
// 用于保存多轮的会话记录
private final List<Message> conversationHistory = new ArrayList<>();public String multiTurnChat(String message) {// 添加用户消息到历史conversationHistory.add(new UserMessage(message));// 多轮对话Prompt prompt = new Prompt(conversationHistory);String aiResponse = openAiChatModel.call(prompt).getResult().getOutput().getContent();// 添加AI回复到历史conversationHistory.add(new MyAssistantMessage(aiResponse));return aiResponse;
}
controller代码:
@GetMapping("/multi-turn")
public String multiTurnChat(@RequestParam String message) {return chatService.multiTurnChat(message);
}
运行结果:
# 第一轮会话,我先告诉他我叫小明
curl -i -X GET \'http://localhost:8080/api/chat/multi-turn?message=我叫小明'# 第二轮会话,我再问他我是谁
curl -i -X GET \'http://localhost:8080/api/chat/multi-turn?message=我是谁'
这个实现其实Spring AI提供了相应的支持,我们等下后面会讲到。
3.6、流式输出
流式输出有两种不同的方式,一种是Spring AI本身提供的流式调用方式,另一种是常见的SSE的获取方式。
3.6.1、Stream输出
我们先使用Spring AI提供的流式调用方式,Service方法:
/*** 流式对话* @param message* @return*/
public Flux<String> chatWithStream(String message) {return openAiChatModel.stream(message);
}
controller直接调用即可:
@GetMapping(value = "/with-stream")public Flux<String> chatWithStream(@RequestParam String message) {return chatService.chatWithStream(message).doOnNext(System.out::println)
// .delayElements(Duration.ofMillis(500)) // 设置流速.doOnComplete(() -> System.out.println("Flux 对话结束"));}```
> Flux是spring webflux提供的流式响应类。想要了解更多,可以去看下Spriing WebFlux。我们直接浏览器运行这个接口,方便查看。如果自己运行的话,会发现浏览器正在一段一段的流式输出,而不是一下子全部内容显示出来。通过控制台的打印,我们也能看到他并不是一次性的渲染出来结果。> 这里我用Flux输出的时候,浏览器一直中文乱码。就算设置了produces的编码格式也不行,最后通过前面application.yml里配置了servlet编码格式才解决。
原因是http响应编码默认是iso-8859-1,而非utf-8,因此导致中文显示乱码。### 3.6.2、SSE实现
除了上面flux的实现方式外,我们可以按需采用sse的输出方式来实现:
```java
@GetMapping(value = "/with-sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitterUTF8 chatWithSSE(@RequestParam String message) {SseEmitterUTF8 emitter = new SseEmitterUTF8(5000L);chatService.getOpenAiChatModel().stream(new Prompt(message)).subscribe(chunk -> {try {emitter.send(chunk.getResult().getOutput().getContent());} catch (IOException e) {emitter.completeWithError(e);}},emitter::completeWithError,emitter::complete);return emitter;
}
同样的,这里需要注意中文乱码问题。默认的SseEmitter编码默认为ISO-8859-1,因此中文是会乱码的。这里的解决方式是重新定义SseEmmiter的响应编码格式;
class SseEmitterUTF8 extends SseEmitter {@Overrideprotected void extendResponse(ServerHttpResponse outputMessage) {super.extendResponse(outputMessage);HttpHeaders headers = outputMessage.getHeaders();headers.setContentType( new MediaType("text", "event-stream", StandardCharsets.UTF_8));}public SseEmitterUTF8(Long timeout) {super(timeout);}
}
使用sse的输出方式,需要指定事件协议,否则会被当作纯文本输出。
produces = MediaType.TEXT_EVENT_STREAM_VALUE
查看结果:
3.7、实现上下记忆
上面提到了多轮对话,其实就是上下文记忆能力。只是上文中自己实现了一个List集合来存储会话记录。这里只是简单的演示示例,这么实现无可厚非。但是当我们项目中的对话可能不止一个语境,需要根据我们的会话记录来区分上下文,这时候这个List集合就可能显得力不从心。
很幸运的是,Spring AI支持了这样的上下记忆能力:ChatMemory。
我们先来使用他,后续再来介绍他是如何实现的。
首先我们需要定义一个简单的会话记忆的管理器,Spring AI提供了关于ChatMemory的内存实现,也就是类似与我们上文中的list。只不过为了区分不同的会话,必然采用了Map来实现,这里我们只需要声明注入即可:
@Configuration
class AiConfig {/*** 会话记忆管理器* @return*/@Beanpublic ChatMemory chatMemory() {return new InMemoryChatMemory();}
}
接下来定义一个简单的带有记忆能力的Service:
@Service
public class ChatMemoryService {@Autowiredprivate OpenAiChatModel openAiChatModel;private final ChatMemory chatMemory;public ChatMemoryService(ChatMemory chatMemory) {this.chatMemory = chatMemory;}public Flux<String> chatWithMemoryStream(String conversationId, String message) {ChatClient.StreamResponseSpec resp = ChatClient.builder(openAiChatModel)// 设置历史对话的保存方式,这里我们使用内存保存.defaultAdvisors(new PromptChatMemoryAdvisor(chatMemory)).build().prompt().user(message).advisors(advisor ->// 设置保存的历史对话IDadvisor.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)// 设置需要保存几轮的历史对话,用于避免内存溢出,因为这里我们没做持久化.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 50)).stream();return resp.content();}}
Controller直接调用:
@GetMapping(value = "/with-memory")
public Flux<String> chatWithMemoryStream(@RequestParam String requestId, @RequestParam String message) {return chatMemoryService.chatWithMemoryStream(requestId, message);
}
查看效果,这里我区分两个会话,一个会话requestId为1,告诉他我是小明。第二个会话requestId为2,告诉他我是小白。接着两个会话,我分别问他我是谁。
会话requestId=1:
会话requestId=2,当我换了个会话ID时,由于记忆根据会话进行了隔离。他已经无法根据识别到我是谁:
当我们再次告诉他,我叫小红:
4、小结
通过本文,我们详细介绍了如何使用Spring AI与OpenAI集成构建智能对话系统。从基础配置到高级功能,我们涵盖了实现一个生产级对话系统所需的关键组件。Spring AI的抽象层使得与OpenAI的集成变得简单而灵活,同时保持了Spring开发者熟悉的编程模型。
随着AI技术的不断发展,这种集成方式将为应用程序带来更多创新的可能性。读者可以在此基础上进一步探索,如实现多模态交互、结合企业知识库构建专业领域助手等。
此外,代码我已经上传Github,地址:https://github.com/Shamee99/spring-ai-demo。需要的可以自取。