从0到1:用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相关依赖:

xml 复制代码
<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

yml 复制代码
spring:
  application:
    name: spring-ai-demo
  ai:
    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: true
      force: true

3.3、普通对话模式

这个是最常见的对话模式,没有任何的语境前提,没有任何的上下文,就是最简单的一问一答的形式。先来实现Service代码:

java 复制代码
@Service
public class ChatService {

    @Autowired
    private OpenAiChatModel openAiChatModel;

    /**
     * 普通对话
     * @param message
     * @return
     */
    public String chat(String message) {
        // 简单的单轮对话
        return openAiChatModel
                .call(new Prompt(message))
                .getResult().getOutput().getContent();
    }
}

controller相关代码:

java 复制代码
@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);
    }
}

直接运行看下回显:

bash 复制代码
curl -i -X GET \
 'http://localhost:8080/api/chat/simple?message=你是谁'

3.4、上下文对话

上下文对话,需要在对话的时候引入上下文,作为和AI交互的语境。Service相关代码:

java 复制代码
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代码:

java 复制代码
@GetMapping("/with-context")
public String chatWithContext(
        @RequestParam String message,
        @RequestParam String context) {
    return chatService.chatWithContext(message, context);
}

查看结果:

bash 复制代码
# 当我们在不同的地点询问几点了的时候,看看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代码:

java 复制代码
// 用于保存多轮的会话记录
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代码:

java 复制代码
@GetMapping("/multi-turn")
public String multiTurnChat(@RequestParam String message) {
    return chatService.multiTurnChat(message);
}

运行结果:

bash 复制代码
# 第一轮会话,我先告诉他我叫小明
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方法:

java 复制代码
/**
 * 流式对话
 * @param message
 * @return
 */
public Flux<String> chatWithStream(String message) {
    return openAiChatModel.stream(message);
}

controller直接调用即可:

java 复制代码
@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的响应编码格式;

java 复制代码
class SseEmitterUTF8 extends SseEmitter {
    @Override
    protected 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的输出方式,需要指定事件协议,否则会被当作纯文本输出。

ini 复制代码
produces = MediaType.TEXT_EVENT_STREAM_VALUE

查看结果:

3.7、实现上下记忆

上面提到了多轮对话,其实就是上下文记忆能力。只是上文中自己实现了一个List集合来存储会话记录。这里只是简单的演示示例,这么实现无可厚非。但是当我们项目中的对话可能不止一个语境,需要根据我们的会话记录来区分上下文,这时候这个List集合就可能显得力不从心。

很幸运的是,Spring AI支持了这样的上下记忆能力:ChatMemory。

我们先来使用他,后续再来介绍他是如何实现的。

首先我们需要定义一个简单的会话记忆的管理器,Spring AI提供了关于ChatMemory的内存实现,也就是类似与我们上文中的list。只不过为了区分不同的会话,必然采用了Map来实现,这里我们只需要声明注入即可:

java 复制代码
@Configuration
class AiConfig {

    /**
     * 会话记忆管理器
     * @return
     */
    @Bean
    public ChatMemory chatMemory() {
        return new InMemoryChatMemory();
    }
}

接下来定义一个简单的带有记忆能力的Service:

java 复制代码
@Service
public class ChatMemoryService {

    @Autowired
    private 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 ->
                        // 设置保存的历史对话ID
                        advisor.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, conversationId)
                                // 设置需要保存几轮的历史对话,用于避免内存溢出,因为这里我们没做持久化
                                .param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 50)
                ).stream();
        return resp.content();
    }

}

Controller直接调用:

java 复制代码
@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,地址:github.com/Shamee99/sp...。需要的可以自取。

相关推荐
xxjiaz4 分钟前
二分查找-LeetCode
java·数据结构·算法·leetcode
nofaluse27 分钟前
JavaWeb开发——文件上传
java·spring boot
爱的叹息1 小时前
【java实现+4种变体完整例子】排序算法中【插入排序】的详细解析,包含基础实现、常见变体的完整代码示例,以及各变体的对比表格
java·算法·排序算法
爱的叹息1 小时前
【java实现+4种变体完整例子】排序算法中【快速排序】的详细解析,包含基础实现、常见变体的完整代码示例,以及各变体的对比表格
java·算法·排序算法
6v6-博客1 小时前
2024年网站开发语言选择指南:PHP/Java/Node.js/Python如何选型?
java·开发语言·php
Miraitowa_cheems2 小时前
[Java EE] Spring AOP 和 事务
java·java-ee·aop·spring 事务
Goldchenn2 小时前
kafka服务端和springboot中使用
spring boot·分布式·kafka
光头小小强0072 小时前
致远OA——自定义开发rest接口
java·经验分享·spring·tomcat
淬渊阁2 小时前
Hello world program of Go
开发语言·后端·golang
Pandaconda2 小时前
【新人系列】Golang 入门(十五):类型断言
开发语言·后端·面试·golang·go·断言·类型