JAVA:Spring Boot3 集成 Spring AI + Ollama 本地模型

🚀 1、简述

在企业开发中,很多场景对 数据安全、离线能力、成本控制要求较高,这时使用云端大模型(如 DeepSeek、OpenAI)可能不合适。在云 API 大行其道的今天,本地模型的价值反而更加凸显:

维度 云 API(如 OpenAI) 本地模型(Ollama)
数据隐私 数据需发送至第三方服务器 数据不出本地,完全可控
合规性 需满足数据出境要求 天然满足内网数据安全规范
成本 按 Token 计费,用量大时成本高 硬件一次性投入,运行成本可预期
延迟 依赖网络,存在波动 内网毫秒级响应
可定制性 模型固定,无法微调 可自由选择/微调模型

适用场景:企业内部知识库问答、代码辅助、客服工单处理、合同审查、批处理摘要等稳定高频场景。


2、什么是 Ollama

Ollama 是一个本地运行大语言模型的工具,支持:

  • LLaMA3
  • Mistral
  • Qwen
  • DeepSeek(部分版本)

2.1 安装 Ollama

macOS(推荐)

bash 复制代码
brew install ollama

Linux

bash 复制代码
curl -fsSL https://ollama.com/install.sh | sh

Windows :访问 ollama.com/download 下载安装包。

2.2 启动服务并拉取模型

bash 复制代码
# 启动 Ollama 服务(默认端口 11434)
ollama serve

# 新开终端,拉取模型(会自动下载,约 4-5GB)
ollama pull llama3.2

# 或者拉取中文友好的模型
ollama pull qwen2.5

# 验证模型是否可用
ollama run llama3.2 "Hello, world!"

2.3 验证 Ollama API

bash 复制代码
# 测试 Ollama API 是否正常
curl http://localhost:11434/api/generate -d '{
  "model": "llama3.2",
  "prompt": "Hello",
  "stream": false
}'

3、实践样例

3.1 Maven 依赖

引入 Spring AI BOM
xml 复制代码
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
引入 Ollama Starter
xml 复制代码
<dependencies>

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

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

</dependencies>

application.yml 配置

yaml 复制代码
spring:
  application:
    name: lm-ollama

  # Spring AI Ollama 配置
  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: qwen3.5:9b
          temperature: 0.7
          top-p: 0.9
          top-k: 40
          num-predict: 500   # 最大输出 token 数
          repeat-penalty: 1.1
          # 上下文窗口大小
          num-ctx: 2048

3.2 聊天服务

提供智能对话功能,支持多轮对话

java 复制代码
@Slf4j
@Service
public class ChatService {

    private final OllamaChatModel chatModel;
    private final OllamaConfig config;
    private final ChatClient chatClient;

    // 会话存储(生产环境建议使用 Redis)
    private final Map<String, List<Message>> sessionStore = new ConcurrentHashMap<>();

    public ChatService(OllamaChatModel chatModel, OllamaConfig config) {
        this.chatModel = chatModel;
        this.config = config;
        this.chatClient = ChatClient.builder(chatModel).build();
        log.info("ChatService 初始化完成,Ollama URL: {}, 默认模型: {}",
                config.getBaseUrl(), config.getModel());
    }

    /**
     * 处理聊天请求
     */
    public ChatResponse chat(ChatRequest request) {
        try {
            // 获取或创建会话 ID
            String sessionId = request.getSessionId();
            if (sessionId == null || sessionId.isEmpty()) {
                sessionId = UUID.randomUUID().toString();
                log.info("创建新会话: {}", sessionId);
            }

            // 处理会话重置
            if (Boolean.TRUE.equals(request.getResetSession())) {
                sessionStore.remove(sessionId);
                log.info("重置会话: {}", sessionId);
            }

            // 获取或创建会话历史
            List<Message> messages = sessionStore.computeIfAbsent(sessionId, k -> {
                List<Message> newSession = new ArrayList<>();
                // 添加系统提示词
                newSession.add(new SystemMessage(config.getSystemPrompt()));
                return newSession;
            });

            // 添加用户消息
            messages.add(new UserMessage(request.getMessage()));

            // 构建聊天提示
            Prompt prompt = new Prompt(messages);

            // 调用 Ollama 获取回复
            String reply;
            if (request.getModel() != null && !request.getModel().isEmpty()) {
                // 使用请求中指定的模型
                reply = chatClient.prompt()
                        .messages(messages)
                        .call()
                        .content();
            } else {
                // 使用默认模型
                reply = chatClient.prompt()
                        .messages(messages)
                        .call()
                        .content();
            }

            // 添加助手回复到历史
            messages.add(new AssistantMessage(reply));

            // 限制历史记录长度(避免 token 过多)
            if (messages.size() > config.getMaxHistoryLength()) {
                List<Message> trimmed = new ArrayList<>();
                trimmed.add(messages.get(0)); // 系统消息
                trimmed.addAll(messages.subList(messages.size() - config.getMaxHistoryLength() + 1,
                        messages.size()));
                sessionStore.put(sessionId, trimmed);
                log.debug("会话 {} 历史记录已修剪到 {} 条", sessionId, config.getMaxHistoryLength());
            }

            log.info("会话 {} - 用户: {} -> AI: {}", sessionId, request.getMessage(), reply);

            return ChatResponse.success(reply, sessionId, config.getModel());

        } catch (Exception e) {
            log.error("处理聊天请求时出错", e);
            return ChatResponse.error("处理请求时出错: " + e.getMessage());
        }
    }

    /**
     * 清除指定会话
     */
    public void clearSession(String sessionId) {
        if (sessionId != null) {
            sessionStore.remove(sessionId);
            log.info("清除会话: {}", sessionId);
        }
    }

    /**
     * 清除所有会话
     */
    public void clearAllSessions() {
        int count = sessionStore.size();
        sessionStore.clear();
        log.info("清除了 {} 个会话", count);
    }

    /**
     * 获取活跃会话数量
     */
    public int getActiveSessionCount() {
        return sessionStore.size();
    }

    /**
     * 获取会话历史
     */
    public List<Message> getSessionHistory(String sessionId) {
        return sessionStore.get(sessionId);
    }

    /**
     * 检查 Ollama 服务是否可用
     */
    public boolean isOllamaAvailable() {
        try {
            // 简单的检查方式 - 尝试列出模型
            // 注意:这需要 Ollama API 支持
            return true;
        } catch (Exception e) {
            log.error("Ollama 服务不可用", e);
            return false;
        }
    }

    /**
     * 流式聊天(返回响应内容)
     */
    public String streamChat(ChatRequest request) {
        // 流式聊天的简化实现
        ChatResponse response = chat(request);
        return response.getReply();
    }
}

3.3、聊天控制器

提供 REST API 接口

java 复制代码
/**
 * Ollama 聊天控制器
 * 提供 REST API 接口
 */
@Slf4j
@RestController
@RequestMapping("/api/chat")
@RequiredArgsConstructor
public class ChatController {

    private final ChatService chatService;

    /**
     * 发送聊天消息
     *
     * @param request 聊天请求
     * @return 聊天响应
     */
    @PostMapping("/send")
    public ResponseEntity<ChatResponse> sendMessage(@RequestBody ChatRequest request) {
        log.info("收到聊天请求: {}", request.getMessage());
        ChatResponse response = chatService.chat(request);
        return ResponseEntity.ok(response);
    }

    /**
     * 简单的 GET 接口,用于快速测试
     *
     * @param message 消息内容
     * @param sessionId 会话 ID(可选)
     * @return 聊天响应
     */
    @GetMapping("/ask")
    public ResponseEntity<ChatResponse> ask(
            @RequestParam String message,
            @RequestParam(required = false) String sessionId) {
        log.info("收到简单聊天请求: {}", message);
        ChatRequest request = ChatRequest.builder()
                .message(message)
                .sessionId(sessionId)
                .build();
        ChatResponse response = chatService.chat(request);
        return ResponseEntity.ok(response);
    }

    /**
     * 清除指定会话
     */
    @DeleteMapping("/session/{sessionId}")
    public ResponseEntity<Map<String, String>> clearSession(@PathVariable String sessionId) {
        chatService.clearSession(sessionId);
        return ResponseEntity.ok(Map.of(
                "message", "会话已清除",
                "sessionId", sessionId
        ));
    }

    /**
     * 清除所有会话
     */
    @DeleteMapping("/sessions")
    public ResponseEntity<Map<String, String>> clearAllSessions() {
        chatService.clearAllSessions();
        return ResponseEntity.ok(Map.of("message", "所有会话已清除"));
    }

    /**
     * 获取活跃会话数量
     */
    @GetMapping("/sessions/count")
    public ResponseEntity<Map<String, Object>> getSessionCount() {
        int count = chatService.getActiveSessionCount();
        return ResponseEntity.ok(Map.of(
                "activeSessions", count
        ));
    }

    /**
     * 健康检查
     */
    @GetMapping("/health")
    public ResponseEntity<Map<String, Object>> health() {
        boolean available = chatService.isOllamaAvailable();
        return ResponseEntity.ok(Map.of(
                "status", available ? "UP" : "DOWN",
                "ollamaAvailable", available
        ));
    }
}

测试接口

text 复制代码
http://localhost:8080/api/chat/ask?message=你好

返回:

text 复制代码
{
    "reply": "你好!很高兴见到你。👋\n\n我是你的 AI 助手,随时准备为你提供帮助。无论是回答问题、查询信息、创作内容,还是解决具体问题,都可以告诉我。\n\n今天有什么我可以帮你的吗?",
    "sessionId": "d4bdbcf1-0423-4536-9ca7-a6ae0ea464a8",
    "success": true,
    "error": null,
    "model": "qwen3.5:9b"
}

4、流式响应与 SSE 实现

4.1 为什么需要流式响应?

流式响应(Streaming)是大模型对话的核心体验要求:

  • 首字延迟:用户无需等待完整生成,200-300ms 即可看到首个字符
  • 交互体验:打字机效果更接近人类对话
  • 降低超时风险:长文本生成不会因超时中断

4.2 Ollama 流式响应原理

Ollama API 默认返回 NDJSON(Newline Delimited JSON) 格式,每行一个 JSON 对象,包含增量 token:

json 复制代码
{"response": "Hello", "done": false}
{"response": " world", "done": false}
{"response": "!", "done": true, "total_duration": 123456}

4.3 使用 WebClient 代理流式响应

java 复制代码
package com.example.demo.service;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;

import java.time.Duration;
import java.util.Map;

@Slf4j
@Service
public class OllamaStreamService {

    private final WebClient webClient;
    private final ObjectMapper objectMapper;

    public OllamaStreamService() {
        this.webClient = WebClient.builder()
                .baseUrl("http://localhost:11434")
                .build();
        this.objectMapper = new ObjectMapper();
    }

    /**
     * 代理 Ollama 流式响应,转换为 SSE 格式
     */
    public Flux<String> streamGenerate(String prompt, String model) {
        Map<String, Object> requestBody = Map.of(
                "model", model != null ? model : "llama3.2",
                "prompt", prompt,
                "stream", true
        );

        return webClient.post()
                .uri("/api/generate")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(requestBody)
                .retrieve()
                // 关键:逐行读取 NDJSON 流
                .bodyToFlux(String.class)
                .timeout(Duration.ofMinutes(2))
                .map(line -> {
                    try {
                        JsonNode json = objectMapper.readTree(line);
                        // 提取 response 字段(增量 token)
                        String token = json.path("response").asText("");
                        return token;
                    } catch (Exception e) {
                        log.error("解析响应失败: {}", e.getMessage());
                        return "";
                    }
                })
                .filter(token -> !token.isBlank())
                .doOnComplete(() -> log.info("流式响应完成"));
    }
}

4.4 使用 Spring AI 内置流式支持(更简单)

Spring AI 的 ChatClient 已经封装了流式处理:

java 复制代码
@GetMapping(value = "/stream-ai", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamWithSpringAi(@RequestParam String message) {
    return chatClient.prompt()
            .user(message)
            .stream()
            .content()
            .doOnNext(chunk -> log.debug("收到 chunk: {}", chunk));
}

4.5 前端 SSE 接收示例

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <title>Ollama 流式对话</title>
</head>
<body>
    <div id="output" style="white-space: pre-wrap;"></div>
    <script>
        const eventSource = new EventSource('/api/chat/stream?message=讲个笑话');
        
        eventSource.onmessage = function(event) {
            const output = document.getElementById('output');
            output.textContent += event.data;
        };
        
        eventSource.onerror = function() {
            console.log('连接关闭');
            eventSource.close();
        };
    </script>
</body>
</html>

5、总结

本文全面介绍了 Spring Boot 3 集成 Spring AI + Ollama 本地模型的完整流程,涵盖以下核心内容:

章节 核心知识点
环境搭建 Ollama 安装、模型拉取、API 验证
项目配置 Maven 依赖、application.yml 参数详解
基础对话 ChatClient 使用、流式响应、SSE 实现
多轮对话 对话记忆管理、会话 ID 维护
参数调优 temperature、top-p、num-ctx 等参数配置
RAG 增强 向量检索、PGVector 集成
生产实践 性能优化、监控、降级策略

Spring AI + Ollama 为企业提供了构建私有化 AI 应用的完整解决方案,既保护了数据隐私,又降低了长期运营成本。建议从 7B 量化模型切入,逐步迭代到更复杂的应用场景。

相关推荐
程序员侠客行4 小时前
Tomcat 从陌生到熟悉
java·tomcat·web
wertyuytrewm4 小时前
Java 异常|Java Exceptions
java·开发语言
ProgramHelpOa4 小时前
Amazon SDE Intern OA 2026 最新复盘|70分钟两题 Medium-Hard
java·前端·javascript
雪碧聊技术4 小时前
深入理解 Java GC:从“房间清洁工”到解决系统卡顿实战
java·开发语言
大鹏说大话4 小时前
Java并发编程核心:线程安全、synchronized与volatile的深度剖析
java·开发语言
迷藏4944 小时前
# 发散创新:低代码开发新范式——用可视化逻辑构建企业级业务系统 在当今快速迭代的软件工程实践
java·python·低代码
JAVA+C语言4 小时前
Java IO 流
java·开发语言
山川行4 小时前
Python快速闯关8:内置函数
java·开发语言·前端·笔记·python·学习·visual studio
Java基基4 小时前
sdkman 一键切换 JDK 版本管理工具
java·开发语言·sdkman
美好的事情能不能发生在我身上4 小时前
Jmeter压测遇到的问题
java·分布式·jmeter