Spring AI 学习篇(十七)| 生产环境部署与性能优化

Spring AI 学习篇(十七)| 生产环境部署与性能优化

一、本章核心学习目标

学完本章,你将能够:

  1. 独立完成Ollama生产级环境的配置与性能调优
  2. 实现模型调用缓存与请求合并,大幅降低API成本
  3. 掌握Spring AI应用的负载均衡与水平扩展方案
  4. 使用Docker Compose一键部署完整的AI应用技术栈
  5. 了解Kubernetes集群部署方案与最佳实践
  6. 实现混合部署架构:核心敏感数据走本地模型,通用任务走商业API
  7. 让你的智能办公Agent具备生产级的高可用和高性能能力

二、前置知识准备

  • 已经完成前16篇的学习,拥有完整的智能办公Agent和监控安全体系
  • 了解Docker和Docker Compose的基本使用
  • 了解Linux服务器的基本操作
  • 了解微服务部署的基本概念

三、生产环境部署全景架构

在正式动手之前,先建立对生产环境全局架构的认知。我们的智能办公Agent是一个典型的AI应用,包含多个异构服务组件:

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      接入层 (Nginx)                         │
│           反向代理  SSL终结  限流  静态资源                │
├─────────────────────────────────────────────────────────────┤
│                 应用层 (Spring Boot × N)                    │
│   应用实例1    应用实例2    应用实例3  (水平扩展)          │
├──────────────┬──────────────┬──────────────────────────────┤
│  大模型服务  │  向量数据库  │      基础设施                │
│  Ollama      │  Milvus/     │  PostgreSQL  Redis  MinIO   │
│  DeepSeek-R1 │  Qdrant      │  (业务数据) (缓存) (文件)   │
│  BGE-M4      │              │                              │
└──────────────┴──────────────┴──────────────────────────────┘

核心设计原则

  1. 无状态应用层:Spring Boot应用本身无状态,可以随意水平扩展
  2. 有状态服务独立部署:Ollama(模型运行)、向量数据库、PostgreSQL各有状态,需要独立管理
  3. 混合模型策略:敏感数据走本地Ollama,通用任务走商业API,兼顾安全与成本

四、Ollama生产环境配置

Ollama是本地大模型部署的事实标准,但默认配置只是为开发设计的。要在生产环境中稳定运行,需要全面的配置优化。

1. Ollama安装与基础配置(Linux生产环境)

bash 复制代码
## 1. 安装Ollama
curl -fsSL https://ollama.com/install.sh | sh

## 2. 修改模型存储位置(避免系统盘被占满)
# 默认模型存储在 /usr/share/ollama/.ollama/models
# 生产环境建议使用独立的数据盘
sudo systemctl stop ollama
sudo mkdir -p /data/ollama/models
sudo chown ollama:ollama /data/ollama/models

# 编辑Ollama服务配置
sudo mkdir -p /etc/systemd/system/ollama.service.d
sudo tee /etc/systemd/system/ollama.service.d/override.conf << 'EOF'
[Service]
Environment="OLLAMA_MODELS=/data/ollama/models"
Environment="OLLAMA_HOST=0.0.0.0"           # 允许远程访问
Environment="OLLAMA_NUM_PARALLEL=4"          # 最大并行请求数
Environment="OLLAMA_MAX_LOADED_MODELS=2"     # 同时加载的模型数
Environment="OLLAMA_KEEP_ALIVE=5m"           # 模型空闲5分钟后卸载
EOF

sudo systemctl daemon-reload
sudo systemctl restart ollama

## 3. 下载生产所需模型
ollama pull deepseek-r1:7b      # 推理模型(复杂任务)
ollama pull qwen2.5:7b           # 通用聊天模型
ollama pull bge-m4               # 嵌入模型(RAG)

## 4. 验证安装
ollama list

2. Ollama性能调优关键参数

bash 复制代码
# Ollama环境变量完整配置
Environment="OLLAMA_MODELS=/data/ollama/models"
Environment="OLLAMA_HOST=0.0.0.0"
Environment="OLLAMA_PORT=11434"

# GPU相关(如果有NVIDIA显卡)
Environment="OLLAMA_NUM_PARALLEL=8"          # GPU场景可以设更高
Environment="OLLAMA_MAX_LOADED_MODELS=3"

# 内存相关
Environment="OLLAMA_KEEP_ALIVE=10m"          # 模型在内存中保持10分钟
Environment="OLLAMA_MAX_VRAM=80"             # 最多使用GPU显存的80%

# 并发相关
Environment="OLLAMA_NUM_PARALLEL=4"          # CPU场景建议4,GPU场景建议8-16
Environment="OLLAMA_MAX_QUEUE=128"           # 请求队列最大长度

# 日志相关
Environment="OLLAMA_DEBUG=0"                 # 生产环境关闭DEBUG
Environment="OLLAMA_LOG_LEVEL=info"

3. Ollama硬件配置参考

这是从我们之前聊天中反复讨论的各种模型的实际部署数据:

场景 模型组合 GPU要求 显存要求 内存要求 预估QPS
轻量级 Qwen2.5-7B + BGE-M4 无(纯CPU) - 16GB 1-2
标准级 DeepSeek-R1-7B + BGE-M4 RTX 3060 12GB 8GB 32GB 5-10
企业级 DeepSeek-R1-14B + BGE-M4 RTX 4090 24GB 16GB 64GB 10-20
高并发 DeepSeek-R1-32B + BGE-M4 A100 80GB 40GB 128GB 20-50

成本估算(2026年6月)

  • 轻量级方案:月均电费约50元,一次性硬件投入约3000元(配置中等的服务器)
  • 标准级方案:月均电费约200元,一次性硬件投入约8000元(含二手RTX 3060)
  • 企业级方案:月均电费约500元,一次性硬件投入约25000元
  • 对比商业API:按每天10万token计算,DeepSeek API月成本约30元。但企业核心数据不能上传,本地部署是不可替代的

4. 新兴架构对部署成本的影响:MoE混合专家模型

在选择模型进行生产部署时,除了参数大小,模型的底层架构 对推理成本和硬件需求有巨大影响。2026年最重要的架构趋势是 MoE(Mixture of Experts,混合专家模型)

MoE的核心思想:将模型分成多个"专家"子网络,每次推理只激活其中一部分专家(通常是总参数量的1/4到1/2),而不是全部参数都参与计算。

对生产部署的直接意义

复制代码
普通稠密模型(如DeepSeek-R1-7B):
  模型大小 = 7B参数 → 需要约14GB存储
  推理时 → 全部7B参数参与计算 → GPU显存占用高

MoE模型(如DeepSeek-V4,总参数236B):
  模型大小 = 236B参数 → 需要约472GB存储
  推理时 → 只激活约20B参数 → GPU显存占用仅约40GB
  → 236B的"大脑",20B的"算力消耗"

推理成本对比

架构类型 代表模型 总参数 每次推理激活参数 显存需求 推理速度 单次调用成本(估算)
稠密小模型 Qwen2.5-7B 7B 7B (100%) 14GB ~0.001元
稠密中模型 DeepSeek-R1-14B 14B 14B (100%) 28GB ~0.003元
MoE大模型 DeepSeek-V4 236B ~20B (8%) 40GB ~0.005元
稠密大模型 Llama 3.3-70B 70B 70B (100%) 140GB ~0.02元

对Java AI开发者的影响

  • MoE模型让"在有限硬件上运行超大模型"成为可能------用一张RTX 4090(24GB)就能运行总参数量200B+的MoE模型
  • 如果你在选型时看到 "DeepSeek-V4" 或 "GPT-5",它们几乎都是MoE架构
  • Ollama 从 0.4 版本开始支持 MoE 模型,部署方式和普通模型完全一样:ollama pull deepseek-v4

预告式提及:MoE是目前AI模型架构的主流方向,了解它有助于你在面试中展现对前沿技术的理解。第18篇的面试题中也会有相关考察。

5. Ollama健康检查与监控

java 复制代码
package com.example.ai.infrastructure.ollama;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

@Component
public class OllamaHealthIndicator implements HealthIndicator {

    private final RestTemplate restTemplate;
    private final String ollamaBaseUrl;

    public OllamaHealthIndicator(OllamaConfig config) {
        this.restTemplate = new RestTemplate();
        this.ollamaBaseUrl = config.getBaseUrl();
    }

    @Override
    public Health health() {
        try {
            // 调用Ollama的模型列表API验证服务状态
            String url = ollamaBaseUrl + "/api/tags";
            restTemplate.getForObject(url, String.class);
            return Health.up()
                    .withDetail("url", ollamaBaseUrl)
                    .withDetail("status", "connected")
                    .build();
        } catch (Exception e) {
            return Health.down()
                    .withDetail("url", ollamaBaseUrl)
                    .withDetail("error", e.getMessage())
                    .build();
        }
    }
}

五、模型调用优化:缓存与请求合并

1. 智能缓存:相同问题不重复调用模型

很多场景下,用户会问相同或高度相似的问题。通过缓存可以大幅减少模型调用次数和成本:

java 复制代码
package com.example.ai.performance.cache;

import org.springframework.ai.embedding.EmbeddingClient;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.List;

@Component
public class SemanticAiCache {

    private final EmbeddingClient embeddingClient;
    private final RedisTemplate<String, CacheEntry> redisTemplate;

    // 语义相似度阈值(0-1),超过这个值认为两个问题是"同一个问题"
    private static final double SIMILARITY_THRESHOLD = 0.95;

    public SemanticAiCache(EmbeddingClient embeddingClient,
                            RedisTemplate<String, CacheEntry> redisTemplate) {
        this.embeddingClient = embeddingClient;
        this.redisTemplate = redisTemplate;
    }

    /**
     * 尝试从缓存中获取相似问题的答案
     * 使用语义匹配而非精确匹配
     */
    public CacheResult tryGetFromCache(String query) {
        // 1. 生成查询向量
        float[] queryVector = embeddingClient.embed(query);

        // 2. 从Redis获取最近的缓存条目(简化实现)
        // 生产环境应使用向量数据库(如Redis Stack的向量检索)
        List<CacheEntry> recentEntries = getRecentCacheEntries(20);

        // 3. 计算余弦相似度,找到最相似的问题
        CacheEntry bestMatch = null;
        double bestSimilarity = 0;

        for (CacheEntry entry : recentEntries) {
            double similarity = cosineSimilarity(queryVector, entry.getQueryVector());
            if (similarity > bestSimilarity) {
                bestSimilarity = similarity;
                bestMatch = entry;
            }
        }

        // 4. 如果相似度超过阈值,返回缓存答案
        if (bestMatch != null && bestSimilarity >= SIMILARITY_THRESHOLD) {
            return CacheResult.hit(bestMatch.getAnswer(), bestMatch.getOriginalQuery(), bestSimilarity);
        }

        return CacheResult.miss();
    }

    /**
     * 将问答结果存入缓存
     */
    public void putToCache(String query, float[] queryVector, String answer) {
        CacheEntry entry = new CacheEntry();
        entry.setQuery(query);
        entry.setQueryVector(queryVector);
        entry.setAnswer(answer);
        entry.setTimestamp(System.currentTimeMillis());

        String key = "ai:cache:" + hashQuery(query);
        redisTemplate.opsForValue().set(key, entry, Duration.ofHours(1));
    }

    private double cosineSimilarity(float[] a, float[] b) {
        double dotProduct = 0, normA = 0, normB = 0;
        for (int i = 0; i < a.length; i++) {
            dotProduct += a[i] * b[i];
            normA += a[i] * a[i];
            normB += b[i] * b[i];
        }
        return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
    }

    private List<CacheEntry> getRecentCacheEntries(int count) {
        // 从Redis获取最近的N条缓存
        // 简化实现,生产环境使用Redis的Sorted Set或Lua脚本
        return List.of();
    }

    private String hashQuery(String query) {
        return Integer.toHexString(query.hashCode());
    }

    @Data
    public static class CacheResult {
        private boolean hit;
        private String answer;
        private String matchedQuery;
        private double similarity;

        public static CacheResult hit(String answer, String matchedQuery, double similarity) {
            CacheResult result = new CacheResult();
            result.hit = true;
            result.answer = answer;
            result.matchedQuery = matchedQuery;
            result.similarity = similarity;
            return result;
        }

        public static CacheResult miss() {
            CacheResult result = new CacheResult();
            result.hit = false;
            return result;
        }
    }

    @Data
    public static class CacheEntry implements Serializable {
        private String query;
        private float[] queryVector;
        private String answer;
        private long timestamp;
    }
}

2. 请求合并:减少并发模型调用

当多个用户同时提问时,将相似的请求合并为一次批量调用,可以大幅提升吞吐量:

java 复制代码
package com.example.ai.performance.batching;

import org.springframework.stereotype.Component;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.function.Consumer;

@Component
public class EmbeddingRequestBatcher {

    // 批处理时间窗口:收集请求的时间
    private static final Duration BATCH_WINDOW = Duration.ofMillis(100);
    // 批处理最大大小
    private static final int MAX_BATCH_SIZE = 32;

    private final BlockingQueue<BatchRequest> pendingRequests = new LinkedBlockingQueue<>();
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    private volatile boolean running = true;

    public EmbeddingRequestBatcher() {
        // 定期检查并处理批处理队列
        scheduler.scheduleWithFixedDelay(this::processBatch, 50, 50, TimeUnit.MILLISECONDS);
    }

    /**
     * 提交一个嵌入请求,等待批处理完成
     */
    public CompletableFuture<float[]> submitBatchEmbed(String text) {
        CompletableFuture<float[]> future = new CompletableFuture<>();
        pendingRequests.offer(new BatchRequest(text, future, System.currentTimeMillis()));
        return future;
    }

    private void processBatch() {
        if (pendingRequests.isEmpty()) {
            return;
        }

        List<BatchRequest> batch = new ArrayList<>();
        long now = System.currentTimeMillis();

        // 收集时间窗口内的请求
        while (batch.size() < MAX_BATCH_SIZE) {
            BatchRequest request = pendingRequests.poll();
            if (request == null) break;

            // 如果请求已经等待超过时间窗口,立即处理
            if (batch.isEmpty() || (now - request.submitTime) <= BATCH_WINDOW.toMillis()) {
                batch.add(request);
            } else {
                // 把这个请求放回去
                pendingRequests.offer(request);
                break;
            }
        }

        if (!batch.isEmpty()) {
            // 批量调用嵌入模型(具体实现依赖于EmbeddingClient的批量接口)
            processBatchEmbeddings(batch);
        }
    }

    private void processBatchEmbeddings(List<BatchRequest> batch) {
        try {
            List<String> texts = batch.stream().map(BatchRequest::getText).toList();
            // 调用批量嵌入API
            // List<float[]> embeddings = embeddingClient.embed(texts);
            // 将结果返回给各自的Future
            // for (int i = 0; i < batch.size(); i++) {
            //     batch.get(i).getFuture().complete(embeddings.get(i));
            // }
        } catch (Exception e) {
            batch.forEach(req -> req.getFuture().completeExceptionally(e));
        }
    }

    private record BatchRequest(String text, CompletableFuture<float[]> future, long submitTime) {}
}

3. 模型路由:智能分发,降本增效

并非所有问题都需要用最强大(也最贵、最慢)的模型。我们可以根据问题的复杂度,动态路由到不同的模型:

java 复制代码
package com.example.ai.performance.routing;

import org.springframework.stereotype.Component;

@Component
public class ModelRouter {

    /**
     * 根据问题特征选择合适的模型
     */
    public ModelRoute route(String query) {
        // 第1层:短问答 → 商业API小模型(最快、最便宜)
        if (isSimpleQuery(query)) {
            return new ModelRoute("deepseek-chat", RouteStrategy.CHEAPEST, "简单问答路由");
        }

        // 第2层:中等复杂度 → 本地通用模型(数据安全、零成本)
        if (isMediumQuery(query)) {
            return new ModelRoute("qwen2.5:7b", RouteStrategy.LOCAL, "通用问答路由");
        }

        // 第3层:复杂推理 → 本地推理模型(最强的能力)
        if (isComplexQuery(query)) {
            return new ModelRoute("deepseek-r1:7b", RouteStrategy.LOCAL_REASONING, "复杂推理路由");
        }

        // 第4层:数据敏感 → 强制本地模型
        if (containsSensitiveContext(query)) {
            return new ModelRoute("qwen2.5:7b", RouteStrategy.LOCAL_FORCED, "数据安全强制本地路由");
        }

        // 默认:本地通用模型
        return new ModelRoute("qwen2.5:7b", RouteStrategy.LOCAL, "默认路由");
    }

    private boolean isSimpleQuery(String query) {
        // 判断是否为简单问题:问候、基础FAQ、简单翻译等
        return query.length() < 30
                || query.matches("(?i)^(你好|谢谢|再见|是的|好的|可以|不错).*");
    }

    private boolean isMediumQuery(String query) {
        // 判断是否为中等复杂度问题
        return query.length() < 200;
    }

    private boolean isComplexQuery(String query) {
        // 判断是否为复杂问题:需要推理、多步骤、专业知识
        return query.length() >= 200
                || query.contains("分析") || query.contains("为什么")
                || query.contains("评估") || query.contains("优化")
                || query.contains("设计") || query.contains("规划");
    }

    private boolean containsSensitiveContext(String query) {
        // 判断上下文是否涉及敏感数据
        return query.contains("用户数据") || query.contains("客户信息")
                || query.contains("财务") || query.contains("合同");
    }

    record ModelRoute(String modelName, RouteStrategy strategy, String reason) {}

    enum RouteStrategy {
        CHEAPEST,        // 最便宜的商业API
        LOCAL,           // 本地通用模型
        LOCAL_REASONING, // 本地推理模型
        LOCAL_FORCED     // 强制本地(数据安全原因)
    }
}

六、Docker容器化部署

1. Spring Boot应用Dockerfile

dockerfile 复制代码
# Dockerfile - Spring AI 应用
FROM eclipse-temurin:17-jre-alpine

# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# 创建应用目录
RUN mkdir -p /app/logs /app/data
WORKDIR /app

# 复制应用JAR
COPY target/ai-office-agent-1.0.0.jar app.jar

# 安全加固:使用非root用户运行
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
RUN chown -R appuser:appgroup /app
USER appuser

# JVM优化参数
ENV JAVA_OPTS="-Xms512m -Xmx1024m \
    -XX:+UseG1GC \
    -XX:MaxGCPauseMillis=200 \
    -XX:+HeapDumpOnOutOfMemoryError \
    -XX:HeapDumpPath=/app/logs/heapdump.hprof \
    -Djava.security.egd=file:/dev/./urandom"

EXPOSE 8080

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

2. 完整技术栈Docker Compose

yaml 复制代码
# docker-compose.yml - 完整AI应用技术栈
version: '3.8'

services:
  # ========== 应用服务 ==========
  ai-app:
    build: .
    container_name: ai-office-agent
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - SPRING_AI_OLLAMA_BASE_URL=http://ollama:11434
      - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/ai_agent
      - SPRING_DATASOURCE_USERNAME=ai_agent
      - SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD}
      - SPRING_DATA_REDIS_HOST=redis
      - SPRING_AI_VECTORSTORE_QDRANT_HOST=qdrant
      - SPRING_AI_VECTORSTORE_QDRANT_PORT=6334
      - SPRING_AI_DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY}
      - JAVA_OPTS=-Xms512m -Xmx1024m
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      qdrant:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - ai-network
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "10"

  # ========== 大模型服务 ==========
  ollama:
    image: ollama/ollama:0.4.0
    container_name: ollama
    ports:
      - "11434:11434"
    environment:
      - OLLAMA_KEEP_ALIVE=10m
      - OLLAMA_NUM_PARALLEL=4
      - OLLAMA_MAX_LOADED_MODELS=2
    volumes:
      - ollama_data:/root/.ollama
      # GPU直通(如果有NVIDIA显卡)
      # 需要先安装 nvidia-container-toolkit
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    restart: unless-stopped
    networks:
      - ai-network
    # 启动后自动拉取模型
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        ollama serve &
        sleep 5
        ollama pull qwen2.5:7b
        ollama pull bge-m4
        wait

  # ========== 向量数据库 ==========
  qdrant:
    image: qdrant/qdrant:v1.9
    container_name: qdrant
    ports:
      - "6333:6333"
      - "6334:6334"
    volumes:
      - qdrant_data:/qdrant/storage
    environment:
      - QDRANT__SERVICE__GRPC_PORT=6334
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:6333/health"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    networks:
      - ai-network

  # ========== 关系型数据库 ==========
  postgres:
    image: postgres:16-alpine
    container_name: postgres
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_DB=ai_agent
      - POSTGRES_USER=ai_agent
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./sql/init:/docker-entrypoint-initdb.d  # 初始化SQL脚本
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ai_agent"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    networks:
      - ai-network

  # ========== 缓存 ==========
  redis:
    image: redis:7-alpine
    container_name: redis
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    networks:
      - ai-network

  # ========== 反向代理 ==========
  nginx:
    image: nginx:alpine
    container_name: nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      - ai-app
    restart: unless-stopped
    networks:
      - ai-network

  # ========== 监控(可选)==========
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=30d'
    restart: unless-stopped
    networks:
      - ai-network

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
    volumes:
      - grafana_data:/var/lib/grafana
    depends_on:
      - prometheus
    restart: unless-stopped
    networks:
      - ai-network

volumes:
  ollama_data:
    driver: local
  qdrant_data:
    driver: local
  postgres_data:
    driver: local
  redis_data:
    driver: local
  prometheus_data:
    driver: local
  grafana_data:
    driver: local

networks:
  ai-network:
    driver: bridge

3. Nginx反向代理配置

nginx 复制代码
# nginx.conf
upstream ai_app_backend {
    least_conn;  # 最少连接负载均衡
    server ai-app-1:8080 weight=1 max_fails=3 fail_timeout=30s;
    server ai-app-2:8080 weight=1 max_fails=3 fail_timeout=30s;
    server ai-app-3:8080 weight=1 max_fails=3 fail_timeout=30s;
    keepalive 32;
}

server {
    listen 80;
    server_name ai-agent.yourcompany.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name ai-agent.yourcompany.com;

    ssl_certificate /etc/nginx/ssl/ai-agent.crt;
    ssl_certificate_key /etc/nginx/ssl/ai-agent.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # 请求体大小限制(文档上传需要较大体积)
    client_max_body_size 50M;

    # 普通API请求
    location /api/ {
        proxy_pass http://ai_app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 超时设置(AI调用可能较慢)
        proxy_read_timeout 120s;
        proxy_connect_timeout 10s;
        proxy_send_timeout 60s;
    }

    # 流式响应(SSE)- 需要特殊配置
    location /api/chat/stream {
        proxy_pass http://ai_app_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # 关闭缓冲以支持SSE
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 300s;  # 流式响应可能持续较长时间
        chunked_transfer_encoding on;
    }

    # 健康检查
    location /actuator/health {
        proxy_pass http://ai_app_backend;
        access_log off;
    }
}

4. 一键启动脚本

bash 复制代码
#!/bin/bash
# deploy.sh - 一键部署脚本

set -e

echo "======================================"
echo "  智能办公Agent - 生产环境部署脚本"
echo "======================================"

## 1. 检查环境
echo "[1/5] 检查环境..."
command -v docker >/dev/null 2>&1 || { echo "请先安装Docker"; exit 1; }
command -v docker-compose >/dev/null 2>&1 || { echo "请先安装Docker Compose"; exit 1; }
echo "✅ Docker环境检查通过"

## 2. 检查配置文件
echo "[2/5] 检查配置文件..."
if [ ! -f .env ]; then
    echo "请先创建.env配置文件,参考.env.example"
    echo "必须设置以下环境变量:"
    echo "  DB_PASSWORD=数据库密码"
    echo "  DEEPSEEK_API_KEY=DeepSeek API密钥(可选,如有商业API需求)"
    echo "  GRAFANA_PASSWORD=Grafana管理员密码"
    exit 1
fi
echo "✅ 配置文件检查通过"

## 3. 构建应用镜像
echo "[3/5] 构建应用镜像..."
./mvnw clean package -DskipTests
docker-compose build ai-app
echo "✅ 应用镜像构建完成"

## 4. 初始化数据库
echo "[4/5] 初始化数据库..."
docker-compose up -d postgres redis qdrant
sleep 10  # 等待数据库就绪
echo "✅ 数据库初始化完成"

## 5. 启动全部服务
echo "[5/5] 启动全部服务..."
docker-compose up -d

## 6. 等待Ollama拉取模型
echo "等待Ollama拉取模型(首次部署可能需要10-20分钟)..."
echo "可以使用 'docker logs ollama' 查看模型拉取进度"

## 7. 验证部署
echo ""
echo "======================================"
echo "  部署完成!"
echo "======================================"
echo "应用地址: https://ai-agent.yourcompany.com"
echo "监控面板: http://your-server:3000 (Grafana)"
echo ""
echo "验证命令:"
echo "  curl http://localhost:8080/actuator/health"
echo "  docker-compose ps"
echo ""
echo "查看日志:"
echo "  docker-compose logs -f ai-app"

七、Kubernetes集群部署方案(概要)

Docker Compose适用于单机部署。如果你的应用需要服务数百甚至数千用户,就需要Kubernetes集群。

核心设计要点

  1. 应用层(Deployment + HPA):

    • Spring Boot应用以无状态Deployment方式部署,副本数3-10
    • 使用HPA(Horizontal Pod Autoscaler)根据CPU/内存使用率自动扩缩
    • 添加基于AI调用QPS的自定义HPA指标
  2. 模型服务层(StatefulSet):

    • Ollama以StatefulSet方式部署,每个Pod绑定一块GPU
    • 使用NodeSelector将Ollama Pod调度到有GPU的节点
    • 配置PodAntiAffinity确保每个GPU节点只运行一个Ollama实例
  3. 向量数据库层

    • Qdrant/Milvus以StatefulSet方式部署,使用持久化存储卷
    • 配置读写分离以提升性能
  4. 核心Kubernetes资源配置

yaml 复制代码
# 略------完整的K8s配置需要独立的系列文章
# 本章只给出核心设计思路和架构决策

Kubernetes vs Docker Compose决策

维度 Docker Compose Kubernetes
适用规模 单机,<100并发用户 集群,>100并发用户
运维复杂度
硬件成本 1台服务器 3台以上服务器
自动扩展 手动 自动(HPA)
滚动更新 不支持零停机 原生支持
推荐场景 企业内部应用、开发/测试环境 对外SaaS服务、高并发场景

八、企业级最佳实践

1. 混合部署策略(核心)

这是我们在整个系列中反复强调的核心策略,这里给出最终落地指南:

复制代码
┌─────────────────────────────────────────────────────────┐
│                   混合部署策略                           │
├─────────────────────┬───────────────────────────────────┤
│  敏感数据路径       │  通用任务路径                     │
│                     │                                   │
│  用户输入           │  用户输入                         │
│    ↓                │    ↓                              │
│  脱敏处理           │  脱敏处理                         │
│    ↓                │    ↓                              │
│  本地RAG检索        │  路由判断                         │
│  (Ollama+BGE-M4)    │    ├── 简单 → 商业API小模型       │
│    ↓                │    ├── 通用 → 本地通用模型         │
│  本地大模型生成     │    └── 复杂 → 本地推理模型         │
│  (DeepSeek-R1:7B)   │                                   │
│                     │                                   │
│  数据永不离开本地   │  成本与效果最佳平衡               │
└─────────────────────┴───────────────────────────────────┘

2. 灰度发布策略

AI应用的特殊之处在于,新版本模型或新提示词可能效果反而更差。因此必须支持灰度发布:

  • 10%用户使用新版本,90%使用旧版本
  • 对比两组用户的满意度、任务成功率、响应时间
  • 确认新版本效果更好后再全量发布

3. 灾备方案

  • 应用层:无需特殊灾备,Deployment自动恢复
  • 数据库层:PostgreSQL主从复制,每日全量备份
  • 向量数据库:定期导出向量数据到对象存储(如MinIO),支持恢复重建
  • 模型层:预下载模型到备用服务器,主服务故障时手动切换

4. 成本优化清单

优化项 预期节省 实现难度
语义缓存(相同问题不重复调用) 节省30-50% API调用
模型路由(简单问题用小模型) 节省20-30% 成本
请求合并(批量嵌入) 提升2-3倍嵌入吞吐
Ollama Keep Alive优化 减少GPU内存占用
本地模型离线时段关闭GPU 节省电费(非工作时间)

九、常见坑与解决方案

坑1:Ollama模型把C盘(系统盘)占满了

现象 :默认模型存储在~/.ollama/models(Linux)或%USERPROFILE%\.ollama\models(Windows),每个模型几GB到几十GB,很容易把系统盘占满。

解决 :安装后第一件事就是修改模型存储位置。具体方法见本章第四节第1步。千万不要等到盘满了才改

坑2:容器化Ollama后GPU不可用

现象 :在Docker中启动Ollama后,日志显示"no GPU detected",只能用CPU推理,速度慢10倍以上。

解决 :Docker使用GPU需要安装nvidia-container-toolkit,并在docker-compose中配置GPU资源(见第六节的ollama服务配置)。

坑3:向量数据库在生产环境丢数据

现象 :开发环境用Chroma的本地文件模式没问题,部署到Docker后重启数据就没了。

解决

  1. Chroma的本地模式不适合Docker环境,生产环境推荐Qdrant或Milvus
  2. 务必为向量数据库挂载持久化Volume
  3. 定期备份向量数据

坑4:大模型并发瓶颈

现象 :Ollama的OLLAMA_NUM_PARALLEL设置过高,导致显存不足(OOM),所有请求都失败。

解决

  1. nvidia-smi监控GPU显存使用情况
  2. 逐步增加OLLAMA_NUM_PARALLEL,找到最优值
  3. 设置OLLAMA_MAX_VRAM限制显存使用上限
  4. 请求排队机制:超过并行限制的请求进入队列等待

坑5:忽略了Ollama模型"冷启动"时间

现象 :设置了OLLAMA_KEEP_ALIVE=5m,用户隔几分钟问一个问题,每次都要等模型重新加载(3-15秒)。

解决

  1. OLLAMA_KEEP_ALIVE设置得长一些(如30分钟或1小时)
  2. 使用定时任务定期发送简单请求"保活"模型
  3. 或者接受冷启动延迟,在用户体验层增加加载提示

十、本章总结与下章预告

本章总结

  1. Ollama生产环境配置的核心:修改存储位置、设置并发参数、合理配置Keep Alive
  2. 硬件选择:7B模型CPU可跑,14B需要GPU,32B需要企业级GPU
  3. 性能优化三板斧:语义缓存(减少调用)+ 请求合并(提升吞吐)+ 模型路由(降低成本)
  4. Docker Compose可以一键部署完整的AI应用技术栈(8个服务组件)
  5. 混合部署策略是最终落地方案:敏感数据走本地Ollama,通用任务走商业API
  6. Kubernetes适用于高并发SaaS场景,企业内部应用用Docker Compose即可
  7. 成本优化最有效的手段是语义缓存和模型路由

下章预告

至此,你已经掌握了从环境搭建到生产部署的全部技术栈。只剩下最后一步------找到一份满意的Java AI工程师工作。下一章(也是本系列的最后一章),我会给你整理2026年Java AI工程师的求职全攻略,包括岗位分析、简历优化、面试题汇总、作品集准备和职业发展路径。

十一、课后练习

  1. 在你的服务器上完成Ollama的生产环境配置,修改模型存储位置
  2. 使用Docker Compose在本地部署完整的AI应用技术栈,验证所有服务正常运行
  3. 实现语义缓存功能,测试缓存命中率和对响应时间的影响
  4. 根据你的实际需求,规划混合部署策略中哪些场景走本地模型,哪些走商业API
  5. 思考:如果你的应用从100用户增长到1000用户,部署架构需要做哪些调整?