SpringBoot系列之实现一个AI聊天助手

发布于:2025-08-15 ⋅ 阅读:(20) ⋅ 点赞:(0)

SpringBoot系列之实现一个AI聊天助手

在当今数字化时代,人工智能(AI)技术正逐渐融入我们生活的方方面面。从智能语音助手到自动化客服,AI的应用场景日益丰富。今天,我们将通过一个实际项目,介绍如何使用Spring Boot框架实现一个AI聊天助手。这个项目基于GitHub上的开源代码

系列博客专栏:

项目简介

这是一个基于Spring Boot框架实现的AI聊天助手应用程序。它使用了Qwen3-8B模型,并通过SiliconFlow API提供服务。该聊天助手具备以下功能特点:

  • 基于Spring AI框架实现的聊天功能
  • 使用OpenAI兼容的API接口
  • 响应式Web界面
  • 聊天历史记录管理

技术栈

在开始之前,我们先了解一下该项目所使用的技术栈:

  • Spring Boot 3.2.3:作为核心框架,提供快速开发和部署的能力。
  • Spring AI 1.0.0-M5:用于集成AI功能。
  • Thymeleaf:用于构建响应式Web界面。
  • HTML/CSS/JavaScript:用于前端页面的开发。

快速开始

前提条件

在开始之前,请确保你的开发环境满足以下条件:

  • JDK 17或更高版本
  • Maven 3.6或更高版本
  • OpenAI API密钥或兼容的API密钥

配置

首先,需要在application.yml文件中配置你的API密钥:

spring:
  ai:
    openai:
      api-key: your-api-key-here
      base-url: https://api.siliconflow.cn
      chat:
        options:
          model: deepseek-ai/DeepSeek-R1-Distill-Llama-8B

或者,你也可以通过环境变量设置API密钥:

export OPENAI_API_KEY=your-api-key-here

构建和运行

完成配置后,可以通过以下命令构建和运行项目:

mvn clean package
java -jar target/springboot-ai-chatbot-0.0.1-SNAPSHOT.jar

或者直接使用Maven Spring Boot插件启动项目。项目运行后,访问http://localhost:8080即可看到聊天助手的Web界面。

核心代码解析

1. MODEL类(ChatMessage .java

自定义的聊天消息Model类,用于保存用户和AI的聊天记录

package com.example.chatbot.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
    private String role; // 'user' or 'assistant'
    private String content;
}

2. 服务类(ChatService.java

服务类是聊天助手的核心逻辑部分,负责处理用户输入的消息,并调用AI模型获取响应。以下是服务类的核心代码:

package com.example.chatbot.service;

import com.example.chatbot.exception.ChatException;
import com.example.chatbot.model.ChatMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;

@Service
public class ChatService {

    private static final Logger log = LoggerFactory.getLogger(ChatService.class);
    private final ChatClient chatClient;
    private final String systemPrompt;
    private final int maxHistorySize;
    private final List<Message> chatHistory = new ArrayList<>();
    private final ReentrantLock historyLock = new ReentrantLock();


    public ChatService(ChatClient.Builder builder,
                       @Value("${spring.ai.openai.system-prompt:你是一个友好、乐于助人的AI助手。请提供简洁、准确的回答。}") String systemPrompt,
                       @Value("${spring.ai.chat.history.max-size:50}") int maxHistorySize) {
        this.chatClient = builder.defaultSystem(systemPrompt).build();
        this.systemPrompt = systemPrompt;
        this.maxHistorySize = maxHistorySize;
        initializeChatHistory();
    }


    /**
     * 初始化聊天历史,添加系统消息
     */
    private void initializeChatHistory() {
        historyLock.lock();
        try {
            chatHistory.clear();
            chatHistory.add(new SystemMessage(systemPrompt));
            log.info("聊天历史已初始化,系统提示: {}", systemPrompt);
        } finally {
            historyLock.unlock();
        }
    }

    /**
     * 处理聊天请求
     * @param userInput 用户输入内容
     * @return AI响应内容
     */
    public String chat(String userInput) {
        if (userInput == null || userInput.trim().isEmpty()) {
            throw new ChatException("输入内容不能为空");
        }

        try {
            String processedInput = userInput.trim();
            log.info("收到用户输入: {}", processedInput);

            // 添加用户消息到历史记录
            UserMessage userMessage = new UserMessage(processedInput);
            addToHistory(userMessage);

            // 检查历史记录大小,超过限制则清理
            trimHistoryIfNeeded();

            // 创建提示并获取响应(直接使用框架Message列表)
            Prompt prompt = new Prompt(new ArrayList<>(getFrameworkChatHistory()));
            log.info("发送请求到AI模型,历史消息数量: {}", prompt.getInstructions().size());

            String responseContent = chatClient.prompt(prompt)
                    .call()
                    .chatResponse()
                    .getResult()
                    .getOutput()
                    .getContent();

            // 处理空响应
            if (responseContent == null || responseContent.trim().isEmpty()) {
                responseContent = "抱歉,未能生成有效响应,请尝试重新提问。";
                log.warn("AI返回空响应,使用默认回复");
            }

            // 添加助手响应到历史记录
            addToHistory(new AssistantMessage(responseContent));
            log.info("AI响应处理完成,响应长度: {}", responseContent.length());

            return responseContent;
        } catch (Exception e) {
            log.error("处理聊天请求失败", e);
            throw new ChatException("处理聊天请求失败: " + e.getMessage(), e);
        }
    }

    /**
     * 获取前端需要的聊天历史(不含系统消息)
     * @return 聊天历史列表
     */
    public List<ChatMessage> getChatHistory() {
        historyLock.lock();
        try {
            return chatHistory.stream()
                    .skip(1) // 跳过系统消息
                    .map(message -> {
                        String role = message instanceof UserMessage ? "user" : "assistant";
                        return new ChatMessage(role, message.getContent());
                    })
                    .collect(Collectors.toList());
        } finally {
            historyLock.unlock();
        }
    }

    /**
     * 获取框架需要的完整聊天历史(包含系统消息)
     * @return 框架消息列表
     */
    private List<Message> getFrameworkChatHistory() {
        historyLock.lock();
        try {
            return new ArrayList<>(chatHistory);
        } finally {
            historyLock.unlock();
        }
    }

    /**
     * 清除聊天历史
     */
    public void clearHistory() {
        initializeChatHistory();
        log.info("聊天历史已清除");
    }

    /**
     * 添加消息到历史记录
     */
    private void addToHistory(Message message) {
        historyLock.lock();
        try {
            chatHistory.add(message);
            log.debug("添加消息到历史,角色: {}, 内容长度: {}",
                    message.getClass().getSimpleName(),
                    message.getContent().length());
        } finally {
            historyLock.unlock();
        }
    }

    /**
     * 如果历史记录超过最大限制,清理部分历史
     */
    private void trimHistoryIfNeeded() {
        historyLock.lock();
        try {
            // 保留系统消息,只清理用户和助手的消息
            if (chatHistory.size() > maxHistorySize) {
                int messagesToRemove = chatHistory.size() - maxHistorySize;
                for (int i = 0; i < messagesToRemove; i++) {
                    chatHistory.remove(1); // 从索引1开始删除(保留系统消息)
                }
                log.info("聊天历史已修剪,移除了 {} 条消息,当前大小: {}",
                        messagesToRemove, chatHistory.size());
            }
        } finally {
            historyLock.unlock();
        }
    }
}
  • OpenAIClient:这是Spring AI提供的客户端,用于与OpenAI API进行交互。
  • ChatRequest:这是发送给AI的请求对象,包含用户的消息。
  • ChatResponse:这是从AI返回的响应对象,包含AI的回复。

3. 控制器类(ChatController.java

控制器类用于处理HTTP请求,将用户输入的消息传递给服务类,并返回AI的响应。以下是控制器类的核心代码:

package com.example.chatbot.controller;

import com.example.chatbot.model.ChatMessage;
import com.example.chatbot.service.ChatService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final ChatService chatService;

    @Autowired
    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    @PostMapping
    public ResponseEntity<ChatMessage> chat(@RequestBody Map<String, String> request) {
        String userMessage = request.get("message");
        String response = chatService.chat(userMessage);
        return ResponseEntity.ok(new ChatMessage("assistant", response));
    }

    @GetMapping("/history")
    public ResponseEntity<List<ChatMessage>> getChatHistory() {
        return ResponseEntity.ok(chatService.getChatHistory());
    }

    @PostMapping("/clear")
    public ResponseEntity<Void> clearHistory() {
        chatService.clearHistory();
        return ResponseEntity.ok().build();
    }
}

  • chat:处理POST请求,接收用户消息并返回AI响应。
  • getChatHistory:处理GET请求,返回聊天历史记录。
  • clearHistory:处理POST请求,清除聊天历史记录。

4. 前端页面(index.html

前端页面使用Thymeleaf模板引擎构建,提供了一个简单的聊天界面。以下是前端页面的核心代码:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI 聊天助手</title>
    <!-- 引入Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- 引入Font Awesome -->
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">

    <!-- 配置Tailwind自定义颜色和字体 -->
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: '#3B82F6',
                        secondary: '#10B981',
                        neutral: '#F3F4F6',
                        dark: '#1F2937',
                        light: '#F9FAFB'
                    },
                    fontFamily: {
                        inter: ['Inter', 'system-ui', 'sans-serif'],
                    },
                }
            }
        }
    </script>

    <style type="text/tailwindcss">
        @layer utilities {
            .content-auto {
                content-visibility: auto;
            }
            .message-appear {
                animation: fadeIn 0.3s ease-out forwards;
            }
            .typing-pulse span {
                animation: pulse 1.4s infinite ease-in-out both;
            }
            .typing-pulse span:nth-child(1) { animation-delay: -0.32s; }
            .typing-pulse span:nth-child(2) { animation-delay: -0.16s; }
        }

        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(10px); }
            to { opacity: 1; transform: translateY(0); }
        }

        @keyframes pulse {
            0%, 80%, 100% { transform: scale(0); }
            40% { transform: scale(1); }
        }
    </style>
</head>
<body class="font-inter bg-gradient-to-br from-light to-neutral min-h-screen flex flex-col">
<!-- 顶部导航栏 -->
<header class="bg-white shadow-md py-4 px-6 sticky top-0 z-10">
    <div class="max-w-4xl mx-auto flex items-center justify-between">
        <div class="flex items-center gap-2">
            <i class="fa fa-comments text-primary text-2xl"></i>
            <h1 class="text-[clamp(1.25rem,3vw,1.75rem)] font-bold text-dark">AI 聊天助手</h1>
        </div>
        <div class="text-sm text-gray-500">
                <span class="flex items-center gap-1">
                    <span class="h-2 w-2 bg-green-500 rounded-full animate-pulse"></span>
                    在线
                </span>
        </div>
    </div>
</header>

<!-- 主内容区 -->
<main class="flex-grow flex flex-col max-w-4xl w-full mx-auto w-full px-4 py-6 md:py-8">
    <!-- 聊天容器 -->
    <div class="chat-container bg-white rounded-2xl shadow-lg p-4 md:p-6 h-[70vh] overflow-y-auto mb-6">
        <div th:each="message : ${chatHistory}">
            <div th:class="${message.role == 'user' ? 'message user-message' : 'message assistant-message'}"
                 th:utext="${#strings.replace(#strings.escapeXml(message.content), '&#10;', '<br/>')}">
            </div>
        </div>

        <!-- 输入指示器 -->
        <div class="message assistant-message typing opacity-0" id="typing-indicator">
            <div class="flex items-center gap-2">
                <div class="w-2 h-2 bg-gray-400 rounded-full typing-pulse">
                    <span class="absolute w-2 h-2 bg-gray-400 rounded-full"></span>
                    <span class="absolute w-2 h-2 bg-gray-400 rounded-full ml-3"></span>
                    <span class="absolute w-2 h-2 bg-gray-400 rounded-full ml-6"></span>
                </div>
                <span>正在思考...</span>
            </div>
        </div>
    </div>

    <!-- 输入区域 -->
    <div class="input-container bg-white rounded-2xl shadow-md p-3 flex gap-3">
        <input
                type="text"
                id="user-input"
                placeholder="输入你的问题..."
                autocomplete="off"
                class="flex-grow px-4 py-3 rounded-xl border border-gray-200 focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none transition-all"
        >
        <button id="send-btn" class="bg-primary hover:bg-primary/90 text-white p-3 rounded-xl transition-all shadow-md hover:shadow-lg flex items-center justify-center">
            <i class="fa fa-paper-plane"></i>
        </button>
        <button id="clear-btn" class="bg-gray-200 hover:bg-gray-300 text-gray-700 p-3 rounded-xl transition-all shadow-sm hover:shadow">
            <i class="fa fa-trash"></i>
        </button>
    </div>
</main>

<!-- 页脚 -->
<footer class="py-4 px-6 text-center text-gray-500 text-sm">
    <p>© 2025 AI 聊天助手 | 提供智能对话服务</p>
</footer>

<script>
        document.addEventListener('DOMContentLoaded', function() {
            const chatContainer = document.querySelector('.chat-container');
            const userInput = document.getElementById('user-input');
            const sendBtn = document.getElementById('send-btn');
            const clearBtn = document.getElementById('clear-btn');
            const typingIndicator = document.getElementById('typing-indicator');

            // 滚动到底部
            function scrollToBottom() {
                chatContainer.scrollTop = chatContainer.scrollHeight;
            }

            // 初始滚动到底部
            scrollToBottom();

            // 添加消息到聊天界面
            function addMessage(content, isUser) {
                const messageDiv = document.createElement('div');
                messageDiv.className = `message message-appear ${isUser ? 'user-message' : 'assistant-message'} mb-4 opacity-0`;

                // 处理代码块格式
                if (content.includes('```')) {
                    content = content.replace(/```([\s\S]*?)```/g,
                        '<pre class="bg-gray-100 p-3 rounded-lg my-2 overflow-x-auto"><code>$1</code></pre>');
                }

                messageDiv.innerHTML = content.replace(/\n/g, '<br/>');
                chatContainer.appendChild(messageDiv);

                // 触发动画
                setTimeout(() => {
                    messageDiv.classList.remove('opacity-0');
                }, 10);

                scrollToBottom();
            }

            // 发送消息
            function sendMessage() {
                const message = userInput.value.trim();
                if (message === '') return;

                // 添加用户消息
                addMessage(message, true);
                userInput.value = '';

                // 显示正在输入指示器
                typingIndicator.style.display = 'block';
                setTimeout(() => {
                    typingIndicator.classList.remove('opacity-0');
                }, 10);
                scrollToBottom();

                // 发送请求到服务器
                fetch('/api/chat', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ message: message })
                })
                .then(response => response.json())
                .then(data => {
                    // 隐藏正在输入指示器
                    typingIndicator.classList.add('opacity-0');
                    setTimeout(() => {
                        typingIndicator.style.display = 'none';
                    }, 300);

                    // 添加AI响应
                    addMessage(data.content, false);
                })
                .catch(error => {
                    console.error('Error:', error);
                    typingIndicator.classList.add('opacity-0');
                    setTimeout(() => {
                        typingIndicator.style.display = 'none';
                    }, 300);
                    addMessage('发生错误,请重试。', false);
                });
            }

            // 清除聊天历史
            function clearChat() {
                fetch('/api/chat/clear', {
                    method: 'POST'
                })
                .then(() => {
                    // 清除聊天界面
                    const messages = chatContainer.querySelectorAll('.message:not(.typing)');
                    messages.forEach(msg => {
                        msg.classList.add('opacity-0');
                        setTimeout(() => msg.remove(), 300);
                    });
                })
                .catch(error => {
                    console.error('Error:', error);
                });
            }

            // 事件监听器
            sendBtn.addEventListener('click', sendMessage);
            clearBtn.addEventListener('click', clearChat);

            userInput.addEventListener('keypress', function(e) {
                if (e.key === 'Enter') {
                    sendMessage();
                }
            });

            // 样式定义
            const style = document.createElement('style');
            style.textContent = `
                .user-message {
                    background-color: #3B82F6;
                    color: white;
                    margin-left: auto;
                    border-radius: 18px 18px 4px 18px;
                    padding: 12px 18px;
                    max-width: 75%;
                    word-wrap: break-word;
                    box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
                }

                .assistant-message {
                    background-color: #F3F4F6;
                    color: #1F2937;
                    margin-right: auto;
                    border-radius: 18px 18px 18px 4px;
                    padding: 12px 18px;
                    max-width: 75%;
                    word-wrap: break-word;
                    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
                }

                pre {
                    font-family: 'Consolas', 'Monaco', monospace;
                }

                .chat-container::-webkit-scrollbar {
                    width: 6px;
                }

                .chat-container::-webkit-scrollbar-track {
                    background: #f1f1f1;
                    border-radius: 10px;
                }

                .chat-container::-webkit-scrollbar-thumb {
                    background: #c1c1c1;
                    border-radius: 10px;
                }

                .chat-container::-webkit-scrollbar-thumb:hover {
                    background: #a8a8a8;
                }
            `;
            document.head.appendChild(style);
        });
    </script>
</body>
</html>

  • chat-container:聊天界面的容器。
  • chat-history:显示聊天历史的区域。
  • chat-input:用户输入消息的输入框。
  • send-button:发送消息的按钮。

API端点

该项目提供了以下API端点,方便开发者进行集成和扩展:

  • POST /api/chat:发送聊天消息并获取AI响应。
  • GET /api/chat/history:获取聊天历史记录。
  • POST /api/chat/clear:清除聊天历史记录。

使用示例

通过Web界面
  1. 打开浏览器,访问http://localhost:8080
  2. 在输入框中输入你的问题。
  3. 点击“发送”按钮或按Enter键。
  4. 查看AI助手的回复。
    在这里插入图片描述
通过API

你也可以通过API与聊天助手进行交互。以下是一些示例命令:

# 发送聊天消息
curl -X POST http://localhost:8080/api/chat \
  -H "Content-Type: application/json" \
  -d '{"message":"你好,请介绍一下自己"}'

# 获取聊天历史
curl -X GET http://localhost:8080/api/chat/history

# 清除聊天历史
curl -X POST http://localhost:8080/api/chat/clear

自定义

如果你希望对聊天助手进行自定义,可以通过修改以下文件来实现:

  • ChatService.java:修改系统提示信息和聊天逻辑。
  • index.html:自定义Web界面。
  • application.yml:调整模型参数和API设置。

总结

通过这个项目,我们展示了如何使用Spring Boot框架快速实现一个AI聊天助手。借助Spring AI和OpenAI的API,我们可以轻松地将AI功能集成到我们的应用程序中。无论是用于客户服务、智能助手还是其他场景,这个项目都提供了一个很好的起点。希望这篇博客能帮助你更好地理解和使用Spring Boot来构建AI应用。


网站公告

今日签到

点亮在社区的每一天
去签到