人工智能(六)- Spring AI 调用Ollama本地大模型

人工智能(五)- OpenAI 调用Ollama本地大模型

一、准备环境

Spring AI 最低支持 Spring Boot 3.4.x + JDK 17 以上版本。如果在使用 Spring Boot 2.x + JDK 8,需要先升级环境,可参考Spring Boot 2.7 + JDK 8 升级至 Spring Boot 3.4.13 + JDK 17 手册。


二、本地安装大模型

本地安装大模型,可参考本地部署DeepSeek大模型,文章里安装的是deepseek-r1:7b大模型,不支持 tool 工具。

我们需要安装一个新的模型,这里我们使用 qwen3.5 大模型

arduino 复制代码
ollama run qwen3.5

Ollama提供了两个主要的API端点,它们在功能和协议兼容性上有所区别。原生接口是 /api/generate,它使用 prompt 字段,并且不支持 messages 这种对话列表格式。

特性 /api/generate (Ollama原生接口) /v1/completions (OpenAI兼容接口)
协议标准 Ollama自定义协议 遵循OpenAI API规范
兼容性 适用于Ollama专属功能 便于与现有OpenAI生态工具(如LangChain)集成
上下文管理 需显式传递context字段维护多轮对话状态 通常通过messages数组传递历史对话
流式响应 默认支持,通过stream: true启用 通过stream: true参数启用
适用场景 需要深度控制模型参数、低延迟流式输出 项目从云端OpenAI迁移至本地Ollama,希望最小化代码改动

注意:Ollama 的 OpenAI 兼容端点是 http://localhost:11434/v1/,SDK 会自动拼接出完整的http://localhost:11434/v1/completions。


三、实战案例:实现智能客服 AI 助手

2.1 创建Maven工程

使用IntelliJ IDEA创建一个Maven工程,完成工程初始化。

2.2 引入核心依赖
xml 复制代码
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.13</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>org.devpotato</groupId>
    <artifactId>chat-service</artifactId>
    <version>1.0-SNAPSHOT</version>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <packaging>jar</packaging>

    <name>ChatService</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <jackson.version>2.15.0</jackson.version>
        <spring-ai.version>1.0.0-M1</spring-ai.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!--        spring-boot        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring AI -->
        <!-- Source: https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-ollama -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-ollama</artifactId>
            <version>${spring-ai.version}</version>
            <scope>compile</scope>
        </dependency>

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


        <!--        jackson        -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>

        <!--        guava        -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>21.0</version>
        </dependency>

        <!--        lombok        -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

    </dependencies>

    <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>
</project>
2.3 解决跨域问题

由于前端页面与后端服务可能存在跨域(CORS)问题,导致前端无法正常调用后端API,因此需要添加跨域过滤器。创建MyFilter类,实现Filter接口,具体代码如下:

java 复制代码
import org.springframework.stereotype.Component;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class MyFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 解决Chrome等浏览器访问本地服务的跨域问题
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        // 允许所有域名跨域访问(生产环境建议指定具体域名,提升安全性)
        httpResponse.setHeader("Access-Control-Allow-Origin", "*");
        // 允许的请求方式
        httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        // 允许的请求头
        httpResponse.setHeader("Access-Control-Allow-Headers", "Content-Type");
        // 继续执行过滤链
        chain.doFilter(request, response);
    }
}
2.4 工程配置文件

在src/main/resources目录下创建application.yml文件,配置服务端口(默认8080,可根据需求修改):

yaml 复制代码
server:
  port: 8080

spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: qwen3.5  # 本地部署的大模型
2.5 核心对话控制器
java 复制代码
package org.devpotato.ollama;

import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/chat")
public class OllamaChatController {

    /**
     * 系统提示词
     */
    private static final String SYSTEM_PROMPT = "作为一个专业的电商售后支持客服,你具备深厚的问题解决能力。请针对用户提出的问题,提供详细的解答步骤或有效的解决方案,并且考虑到用户的水平可能有所不同,请尽可能地简化语言。";

    @Autowired
    private OllamaChatModel chatModel;

    @RequestMapping(path = "", method = RequestMethod.POST)
    public String chat(@RequestBody Map<String, Object> map) {
        String message = map.get("message").toString();

        // 构建带系统提示词的Prompt
        String fullMessage = SYSTEM_PROMPT + "\n\n用户提问:" + message;

        Prompt prompt = new Prompt(fullMessage);

        ChatResponse response = chatModel.call(prompt);

        return response.getResult().getOutput().getContent();
    }
}

四、前端页面搭建(简易版)

前端采用简单的HTML+JavaScript搭建聊天界面,实现用户输入提问、展示客服回复的功能。创建index.html文件,代码如下(可直接在浏览器中打开使用):

xml 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>电商客服中心</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Microsoft YaHei', sans-serif;
        }

        body {
            background-color: #f5f7fa;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            padding: 20px;
        }

        .chat-container {
            width: 100%;
            max-width: 800px;
            height: 80vh;
            background-color: #fff;
            border-radius: 10px;
            box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }

        .chat-header {
            background-color: #409eff;
            color: #fff;
            padding: 15px 20px;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .chat-header img {
            width: 40px;
            height: 40px;
            border-radius: 50%;
            background-color: #fff;
        }

        .chat-header h2 {
            font-size: 18px;
            font-weight: 600;
        }

        .chat-messages {
            flex: 1;
            padding: 20px;
            overflow-y: auto;
            background-color: #f9f9f9;
        }

        .message {
            margin-bottom: 15px;
            max-width: 70%;
            display: flex;
            animation: fadeIn 0.3s ease;
        }

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

        .user-message {
            margin-left: auto;
            flex-direction: row-reverse;
        }

        .message-avatar {
            width: 36px;
            height: 36px;
            border-radius: 50%;
            margin: 0 8px;
            flex-shrink: 0;
        }

        .user-message .message-content {
            background-color: #409eff;
            color: #fff;
            border-radius: 10px 10px 0 10px;
        }

        .bot-message .message-content {
            background-color: #fff;
            color: #333;
            border-radius: 10px 10px 10px 0;
            border: 1px solid #eee;
        }

        .message-content {
            padding: 10px 15px;
            word-wrap: break-word;
            line-height: 1.4;
        }

        /* 快捷按钮区域样式 */
        .quick-buttons {
            padding: 10px 15px;
            border-top: 1px solid #eee;
            background-color: #fafafa;
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
        }

        .quick-button {
            padding: 6px 15px;
            background-color: #e8f4ff;
            color: #409eff;
            border: 1px solid #d1e9ff;
            border-radius: 20px;
            cursor: pointer;
            font-size: 13px;
            transition: all 0.2s;
        }

        .quick-button:hover {
            background-color: #409eff;
            color: #fff;
            border-color: #409eff;
        }

        .chat-input {
            display: flex;
            padding: 15px;
            border-top: 1px solid #eee;
            background-color: #fff;
        }

        #message-input {
            flex: 1;
            padding: 12px 15px;
            border: 1px solid #ddd;
            border-radius: 25px;
            outline: none;
            font-size: 14px;
            resize: none;
            height: 45px;
            max-height: 120px;
        }

        #message-input:focus {
            border-color: #409eff;
            box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
        }

        #send-button {
            margin-left: 10px;
            padding: 0 20px;
            background-color: #409eff;
            color: #fff;
            border: none;
            border-radius: 25px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.2s;
        }

        #send-button:hover {
            background-color: #337ecc;
        }

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

        .loading {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 3px solid rgba(255,255,255,.3);
            border-radius: 50%;
            border-top-color: white;
            animation: spin 1s ease-in-out infinite;
        }

        @keyframes spin {
            to { transform: rotate(360deg); }
        }

        .empty-hint {
            text-align: center;
            color: #999;
            padding: 50px 0;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <div class="chat-container">
        <div class="chat-header">
            <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMjAiIGZpbGw9IiM0MDllZmYiLz4KPHBhdGggZD0iTTE1IDI1QzE1IDI3LjcxIDE2Ljk5IDI5IDE5IDI5QzIxLjAxIDI5IDIzIDI3LjcxIDIzIDI1QzIzIDIyLjc5IDIxLjAxIDIxIDE5IDIxQzE2Ljk5IDIxIDE1IDIyLjc5IDE1IDI1WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==" alt="客服图标">
            <h2>在线客服中心</h2>
        </div>
        <div class="chat-messages" id="chat-messages">
            <div class="empty-hint" id="empty-hint">欢迎咨询,我是您的专属客服😊</div>
        </div>
        
        <!-- 新增快捷按钮区域 -->
        <div class="quick-buttons" id="quick-buttons">
            <div class="quick-button" onclick="sendQuickMessage('查看订单')">查看订单</div>
            <div class="quick-button" onclick="sendQuickMessage('查看物流')">查看物流</div>
            <div class="quick-button" onclick="sendQuickMessage('申请退款')">申请退款</div>
            <div class="quick-button" onclick="sendQuickMessage('修改收货地址')">修改收货地址</div>
            <div class="quick-button" onclick="sendQuickMessage('商品质量问题')">商品质量问题</div>
        </div>
        
        <div class="chat-input">
            <textarea id="message-input" placeholder="请输入您想咨询的问题..." onkeydown="if(event.keyCode===13&&!event.shiftKey){event.preventDefault();sendMessage();}"></textarea>
            <button id="send-button" onclick="sendMessage()">发送</button>
        </div>
    </div>

    <script>
        // 获取DOM元素
        const messageInput = document.getElementById('message-input');
        const sendButton = document.getElementById('send-button');
        const chatMessages = document.getElementById('chat-messages');
        const emptyHint = document.getElementById('empty-hint');

        // 头像URL(使用base64编码的SVG,也可以替换为实际图片URL)
        const BOT_AVATAR = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMTgiIGN5PSIxOCIgcj0iMTgiIGZpbGw9IiM0MDllZmYiLz4KPHBhdGggZD0iTTEzIDIyQzEzIDI0LjIxIDE0Ljk5IDI2IDE3IDI2QzE5LjAxIDI2IDIxIDI0LjIxIDIxIDIyQzIxIDE5Ljc5IDE5LjAxIDE4IDE3IDE4QzE0Ljk5IDE4IDEzIDE5Ljc5IDEzIDIyWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==';
        const USER_AVATAR = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMTgiIGN5PSIxOCIgcj0iMTgiIGZpbGw9IiNmZmYwMDAiLz4KPHBhdGggZD0iTTEyIDIwQzEyIDIyLjcxIDEzLjk5IDI1IDE2IDI1QzE4LjAxIDI1IDIwIDIyLjcxIDIwIDIwQzIwIDE3Ljc5IDE4LjAxIDE1IDE2IDE1QzEzLjk5IDE1IDEyIDE3Ljc5IDEyIDIwWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTIwIDI5QzIwIDI5IDE3IDMwIDE3IDMwQzE0IDMwIDEyIDI5IDEyIDI5QzEyIDI5IDEyIDI3IDEyIDI3QzEyIDI3IDE0IDI2IDE2IDI2QzE4IDI2IDIwIDI3IDIwIDI3QzIwIDI3IDIwIDI5IDIwIDI5WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==';

        // 自动调整输入框高度
        messageInput.addEventListener('input', function() {
            this.style.height = 'auto';
            this.style.height = (this.scrollHeight > 45 ? this.scrollHeight : 45) + 'px';
        });

        // 快捷消息发送函数
        function sendQuickMessage(message) {
            // 将快捷消息填入输入框
            messageInput.value = message;
            messageInput.style.height = 'auto';
            messageInput.style.height = (messageInput.scrollHeight > 45 ? messageInput.scrollHeight : 45) + 'px';
            
            // 自动发送该消息
            sendMessage();
        }

        // 发送消息函数
        async function sendMessage() {
            const message = messageInput.value.trim();
            
            // 验证输入内容
            if (!message) {
                alert('请输入咨询内容!');
                return;
            }

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

            try {
                // 隐藏空提示
                emptyHint.style.display = 'none';

                // 添加用户消息到聊天窗口
                addMessageToChat(message, 'user');
                
                // 清空输入框并恢复高度
                messageInput.value = '';
                messageInput.style.height = '45px';

                // 添加加载状态
                const loadingId = addLoadingMessage();

                // 调用后端接口
                const response = await fetch('http://127.0.0.1:8080/chat', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ message: message })
                });

                // 移除加载状态
                removeLoadingMessage(loadingId);

                // 处理响应
                if (response.ok) {
                    const data = await response.text();
                    // 添加客服回复到聊天窗口
                    addMessageToChat(data, 'bot');
                } else {
                    addMessageToChat('抱歉,服务器暂时无法响应,请稍后再试!', 'bot');
                    console.error('接口请求失败:', response.status);
                }
            } catch (error) {
                // 移除加载状态
                const loadingElements = document.querySelectorAll('.loading-message');
                loadingElements.forEach(el => el.remove());
                
                addMessageToChat('网络错误,请检查您的网络连接!', 'bot');
                console.error('请求出错:', error);
            } finally {
                // 恢复发送按钮和输入框
                sendButton.disabled = false;
                messageInput.disabled = false;
                messageInput.focus();
                
                // 滚动到最新消息
                scrollToBottom();
            }
        }

        // 添加消息到聊天窗口
        function addMessageToChat(content, sender) {
            const messageDiv = document.createElement('div');
            messageDiv.className = `message ${sender}-message`;
            
            // 创建头像元素
            const avatarImg = document.createElement('img');
            avatarImg.className = 'message-avatar';
            avatarImg.src = sender === 'user' ? USER_AVATAR : BOT_AVATAR;
            avatarImg.alt = sender === 'user' ? '用户头像' : '客服头像';
            
            // 创建消息内容元素
            const contentDiv = document.createElement('div');
            contentDiv.className = 'message-content';
            contentDiv.textContent = content;
            
            // 组装消息元素
            messageDiv.appendChild(avatarImg);
            messageDiv.appendChild(contentDiv);
            
            chatMessages.appendChild(messageDiv);
            
            // 滚动到最新消息
            scrollToBottom();
        }

        // 添加加载中的消息
        function addLoadingMessage() {
            const loadingId = 'loading-' + Date.now();
            const loadingDiv = document.createElement('div');
            loadingDiv.id = loadingId;
            loadingDiv.className = 'message bot-message loading-message';
            
            // 创建客服头像
            const avatarImg = document.createElement('img');
            avatarImg.className = 'message-avatar';
            avatarImg.src = BOT_AVATAR;
            avatarImg.alt = '客服头像';
            
            // 创建加载内容
            const contentDiv = document.createElement('div');
            contentDiv.className = 'message-content';
            contentDiv.innerHTML = '<div class="loading"></div>';
            
            // 组装加载消息
            loadingDiv.appendChild(avatarImg);
            loadingDiv.appendChild(contentDiv);
            chatMessages.appendChild(loadingDiv);
            
            scrollToBottom();
            return loadingId;
        }

        // 移除加载中的消息
        function removeLoadingMessage(loadingId) {
            const loadingDiv = document.getElementById(loadingId);
            if (loadingDiv) {
                loadingDiv.remove();
            }
        }

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

        // 监听输入框回车事件(兼容)
        messageInput.addEventListener('input', function() {
            this.style.height = 'auto';
            this.style.height = (this.scrollHeight > 45 ? this.scrollHeight : 45) + 'px';
        });
        
        messageInput.addEventListener('keypress', function(e) {
            if (e.key === 'Enter' && !e.shiftKey) {
                e.preventDefault();
                sendMessage();
            }
        });
    </script>
</body>
</html>

五、测试

相关推荐
sell-cloud3 小时前
Prompt Engineering要凉了?Fable 5.5能“猜“意图,但银行正在封杀AI Agent——企业AI的真正护城河是什么
人工智能·prompt
Navicat中国5 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI5 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区5 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
XCDoris7 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy8 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V8 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI8 小时前
AI英语教培智能体的开发
人工智能
物联网软硬件开发-轨物科技8 小时前
【技术白皮书】光伏清检一体化机器人系统解决方案技术白皮书
大数据·运维·网络·人工智能·机器人
海宇数据8 小时前
零信任架构实战:基于海宇网上踪迹欺诈行为识别构建自动化信贷资信评估网关
运维·人工智能·架构·自动化