Spring AI 实战秘籍:从入门到精通 第一章:环境搭建

904 阅读9分钟

一、引言

如果你渴望全方位掌握 Spring AI,从最初的基础搭建,到复杂场景下的精妙运用,直至成长为高手应对各类难题,这本教程将是你的不二之选。它会像一位贴心导师,手把手带你走过每一个关键阶段,开启 Spring AI 实战之旅。

Spring AI介绍:

docs.spring.io/spring-ai/r…

在当今数字化时代,人工智能(AI)技术的蓬勃发展正深刻地改变着各个领域的运作模式。Spring AI 作为一款极具创新性的开发工具,应运而生,为 Java 开发者们搭建起了通往 AI 应用开发的便捷桥梁。它融合了一系列先进的技术和理念,助力开发者轻松驾驭复杂的 AI 开发场景。

Spring AI 的核心在于对多种 AI 模型的强大支持。这些模型如同智慧的引擎,能够处理和生成丰富多样的信息,从文本到图像,再到音频,无所不能。以 ChatGPT 为代表的语言模型,通过强大的预训练机制,能够实现流畅的人机对话交互;而 Midjourney 和 Stable Diffusion 等图像生成模型,则可以根据文本描述创造出令人惊叹的艺术作品。Spring AI 将这些不同类型的模型整合在一起,让开发者可以根据具体的业务需求灵活选择和运用。

在与 AI 模型交互的过程中,提示(Prompts)和提示模板(Prompt Templates)扮演着至关重要的角色。提示就像是与模型沟通的 “密码”,精心设计的提示能够引导模型产生符合预期的输出。Spring AI 借助像 StringTemplate 这样的库,实现了提示模板的灵活运用。开发者可以通过设定模板,轻松替换其中的参数,从而快速生成个性化的提示内容。这种方式不仅提高了开发效率,还使得提示的管理和维护更加便捷。

嵌入(Embeddings)技术是 Spring AI 的又一亮点。它将文本、图像等数据转化为数值向量,通过计算向量之间的距离,能够精准地衡量数据之间的相似性。在实际应用中,这一特性被广泛用于检索增强生成(RAG)场景。例如,在智能客服系统中,RAG 技术可以从大量的知识库中快速检索出与用户问题相似的内容,并将其融入到提示中,让模型给出更加准确和丰富的回答。

标记(Tokens)作为 AI 模型处理数据的基本单元,在 Spring AI 中也得到了充分的考虑。开发者需要了解模型对标记数量的限制,也就是 “上下文窗口” 的概念。Spring AI 提供了相应的工具和策略,帮助开发者合理地处理数据,确保在模型的限制范围内充分发挥其性能。同时,由于在实际使用中,标记数量与成本密切相关,Spring AI 的优化措施也有助于开发者降低使用成本。

对于 AI 模型的输出,Spring AI 致力于实现结构化输出(Structured Output)。传统的 AI 模型输出往往是简单的字符串形式,即使要求以 JSON 格式输出,也并非真正的 JSON 数据结构。Spring AI 通过精心设计的提示和转换机制,能够将模型输出转换为便于应用集成的结构化数据,大大提高了数据的可用性。

在将外部数据和 API 引入 AI 模型方面,Spring AI 提供了多种灵活的解决方案。微调(Fine Tuning)技术可以根据特定的数据集对模型进行优化,但该方法对技术要求较高且资源消耗大。提示填充(Prompt Stuffing)则是一种更为实用的方式,它将相关数据嵌入到提示中,Spring AI 为这一技术的实现提供了有力支持。此外,函数调用(Function Calling)机制允许开发者连接大语言模型与外部系统的 API,获取实时数据并进行处理,极大地拓展了模型的功能边界。

为了确保 AI 模型输出的准确性和实用性,Spring AI 还提供了完善的评估机制。通过评估 AI 响应(Evaluating AI responses),开发者可以从相关性、连贯性和事实正确性等多个维度对模型的输出进行衡量。利用预训练模型自身以及向量数据库中的信息,能够更全面地评估模型的表现,为进一步优化提供有力依据。

Spring AI 凭借其丰富的功能和强大的性能,为 Java 开发者提供了一个全面、高效的 AI 应用开发平台。无论是构建智能聊天机器人、图像生成应用,还是进行数据分析和预测,Spring AI 都能成为开发者的得力助手,助力开发者在人工智能领域创造出更加精彩的应用。

二、创建项目

我们一起来体验一下Spring AI的魅力,首先来创建项目:

  1. 创建项目:使用IDEA创建spring项目,注意官方要求版本号为3.2.x 和 3.3.x。本教程中我们使用maven来作为项目管理工具。

image.png

勾选spring-web选项

image.png

  1. 添加依赖: 首先添加snapshot需要的依赖库
<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>

接下来添加spring-ai所有的bom,用来锁定依赖版本:

<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>

最后添加对应大模型的依赖:

Spring AI支持的大模型有很多,在官网上有详细的对比,有兴趣的同学们可以详细去看下。 docs.spring.io/spring-ai/r…

今天我们选择的是智谱大模型,不需要科学上网就可以使用。首先去智谱的官网进行注册,然后申请一个API KEY

image.png

接下来我们引入对应的依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
</dependency>

配置刚才申请的api key:

spring.ai.zhipuai.api-key=your_api_key

image.png

三、代码编写

package com.brianxiadong.spring_ai_demo.controller;

import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.util.Map;

@RestController
public class ChatController {

    private final ZhiPuAiChatModel chatModel;

    @Autowired
    public ChatController(ZhiPuAiChatModel chatModel) {
        this.chatModel = chatModel;
    }

    @GetMapping("/ai/generate")
    public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        return Map.of("generation", this.chatModel.call(message));
    }

    @GetMapping("/ai/generateStream")
    public Flux<ChatResponse> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        var prompt = new Prompt(new UserMessage(message));
        return this.chatModel.stream(prompt);
    }
}

运行spring boot服务之后,调用接口进行测试:

curl http://localhost:8080/ai/generate

这个时候我们会遇到一个错误:

{"timestamp":"2025-02-05T07:25:47.090+00:00","status":500,"error":"Internal Server Error","path":"/ai/generate"}

回头去控制台看看

image.png

这说明我们已经将请求发送给了智谱大模型,但是由于Spring AI配置的默认大模型是收费的,同时api-key绑定的账户没有充值,所以无法成功调用。我们可以选择使用免费的模型进行测试:

修改一下配置

spring.ai.zhipuai.chat.options.model=GLM-4-Flash

image.png

使用GLM-4-Flash这个免费模型,再次进行测试:

image.png

成功返回了结果,然后我们换一个问题:

image.png

四、流式响应测试

让我们编写一个页面,测试流式相应的效果,同时实现打字机效果:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI Chat Assistant</title>
    <style>
        /* 全局样式 */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            line-height: 1.6;
            color: #333;
            background-color: #f7f7f8;
        }

        /* 聊天容器 */
        .chat-container {
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            height: 100vh;
            display: flex;
            flex-direction: column;
        }

        /* 消息区域 */
        .messages-container {
            flex: 1;
            overflow-y: auto;
            margin-bottom: 20px;
            padding: 20px;
        }

        /* 消息样式 */
        .message {
            margin-bottom: 20px;
            padding: 15px;
            border-radius: 8px;
        }

        .user-message {
            background-color: #fff;
            margin-left: 20%;
        }

        .assistant-message {
            background-color: #f0f0f0;
            margin-right: 20%;
        }

        /* 输入区域 */
        .input-container {
            position: relative;
            padding: 20px;
            background-color: #fff;
            border-radius: 8px;
            box-shadow: 0 0 10px rgba(0,0,0,0.1);
        }

        #message-input {
            width: 100%;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 4px;
            resize: none;
            height: 50px;
            font-size: 16px;
        }

        #send-button {
            position: absolute;
            right: 30px;
            bottom: 30px;
            padding: 8px 16px;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            transition: background-color 0.3s;
        }

        #send-button:hover {
            background-color: #0056b3;
        }

        #send-button:disabled {
            background-color: #cccccc;
            cursor: not-allowed;
        }

        /* 打字动画 */
        .typing {
            display: inline-block;
            margin-left: 4px;
        }

        .typing span {
            display: inline-block;
            width: 6px;
            height: 6px;
            background-color: #666;
            border-radius: 50%;
            margin: 0 2px;
            animation: typing 1s infinite;
        }

        .typing span:nth-child(2) { animation-delay: 0.2s; }
        .typing span:nth-child(3) { animation-delay: 0.4s; }

        @keyframes typing {
            0%, 100% { transform: translateY(0); }
            50% { transform: translateY(-4px); }
        }
    </style>
</head>
<body>
    <div class="chat-container">
        <div class="messages-container" id="messages">
            <!-- 消息将在这里动态添加 -->
        </div>
        <div class="input-container">
            <textarea 
                id="message-input" 
                placeholder="输入您的问题..."
                rows="1"
                onkeydown="if(event.keyCode === 13 && !event.shiftKey) { event.preventDefault(); sendMessage(); }">
            </textarea>
            <button id="send-button" onclick="sendMessage()">发送</button>
        </div>
    </div>

    <script>
        // DOM 元素
        const messagesContainer = document.getElementById('messages');
        const messageInput = document.getElementById('message-input');
        const sendButton = document.getElementById('send-button');

        // 工具函数:创建消息元素
        function createMessageElement(content, isUser) {
            const messageDiv = document.createElement('div');
            messageDiv.className = `message ${isUser ? 'user-message' : 'assistant-message'}`;
            messageDiv.textContent = content;
            return messageDiv;
        }

        // 创建打字动画元素
        function createTypingIndicator() {
            const typingDiv = document.createElement('div');
            typingDiv.className = 'message assistant-message';
            typingDiv.innerHTML = '正在思考中<div class="typing"><span></span><span></span><span></span></div>';
            return typingDiv;
        }

        // 发送消息
        async function sendMessage() {
            const message = messageInput.value.trim();
            if (!message) return;

            // 禁用输入和发送按钮
            messageInput.disabled = true;
            sendButton.disabled = true;

            // 显示用户消息
            messagesContainer.appendChild(createMessageElement(message, true));
            messageInput.value = '';

            // 显示打字动画
            const typingIndicator = createTypingIndicator();
            messagesContainer.appendChild(typingIndicator);
            messagesContainer.scrollTop = messagesContainer.scrollHeight;

            try {
                // 调用流式API
                const response = await fetch(`/ai/generateStream?message=${encodeURIComponent(message)}`);
                const reader = response.body.getReader();
                const decoder = new TextDecoder();

                // 创建新的助手消息容器
                const assistantMessage = document.createElement('div');
                assistantMessage.className = 'message assistant-message';
                
                // 移除打字动画
                typingIndicator.remove();
                
                // 添加新的消息容器
                messagesContainer.appendChild(assistantMessage);

                // 读取流式响应
                let buffer = ''; // 用于存储未完成的JSON字符串
                while (true) {
                    const {done, value} = await reader.read();
                    if (done) break;

                    // 解码响应数据
                    const chunk = decoder.decode(value);
                    
                    // 去掉数组的开始和结束标志
                    let processedChunk = chunk.replace(/^[/, '').replace(/]$/, '');
                    // 如果chunk中间有数组标志,也去掉
                    processedChunk = processedChunk.replace(/],[/g, ',');
                    
                    buffer += processedChunk;
                    
                    // 查找完整的JSON对象
                    while (true) {
                        // 查找第一个完整的JSON对象
                        const startIndex = buffer.indexOf('{"result"');
                        if (startIndex === -1) break;
                        
                        // 从开始位置查找匹配的结束括号
                        let bracketCount = 0;
                        let endIndex = -1;
                        
                        for (let i = startIndex; i < buffer.length; i++) {
                            if (buffer[i] === '{') bracketCount++;
                            if (buffer[i] === '}') bracketCount--;
                            
                            if (bracketCount === 0) {
                                endIndex = i;
                                break;
                            }
                        }
                        
                        // 如果没有找到匹配的结束括号,说明JSON对象不完整
                        if (endIndex === -1) break;
                        
                        // 提取完整的JSON对象
                        const jsonStr = buffer.substring(startIndex, endIndex + 1);
                        // 更新buffer,移除已处理的部分(包括可能的逗号)
                        buffer = buffer.substring(endIndex + 1).replace(/^,/, '');
                        
                        try {
                            const jsonData = JSON.parse(jsonStr);
                            if (jsonData.result?.output?.text) {
                                // 将文本分成字符数组,逐个显示
                                const text = jsonData.result.output.text;
                                for (let i = 0; i < text.length; i++) {
                                    await new Promise(resolve => setTimeout(resolve, 50)); // 每个字符间隔50ms
                                    assistantMessage.textContent += text[i];
                                    messagesContainer.scrollTop = messagesContainer.scrollHeight;
                                }
                            }
                        } catch (e) {
                            console.error('解析JSON数据失败:', e, jsonStr);
                        }
                    }
                }
            } catch (error) {
                console.error('请求失败:', error);
                const errorMessage = document.createElement('div');
                errorMessage.className = 'message assistant-message';
                errorMessage.textContent = '抱歉,发生了一些错误,请稍后重试。';
                messagesContainer.appendChild(errorMessage);
            } finally {
                // 重新启用输入和发送按钮
                messageInput.disabled = false;
                sendButton.disabled = false;
                messageInput.focus();
            }
        }

        // 页面加载完成后聚焦输入框
        window.onload = () => {
            messageInput.focus();
        };
    </script>
</body>
</html> 

访问页面,查看效果:

image.png

成功实现了类似于ChatGPT的效果。

通过本章系统学习,你将拥有一个完备的 Spring AI 开发环境,为后续深入探索知识、实践创新项目筑牢根基。