Spring AI 学习篇(十七)| 生产环境部署与性能优化
- 一、本章核心学习目标
- 二、前置知识准备
- 三、生产环境部署全景架构
- 四、Ollama生产环境配置
-
- [1. Ollama安装与基础配置(Linux生产环境)](#1. Ollama安装与基础配置(Linux生产环境))
- [2. Ollama性能调优关键参数](#2. Ollama性能调优关键参数)
- [3. Ollama硬件配置参考](#3. Ollama硬件配置参考)
- [4. 新兴架构对部署成本的影响:MoE混合专家模型](#4. 新兴架构对部署成本的影响:MoE混合专家模型)
- [5. Ollama健康检查与监控](#5. Ollama健康检查与监控)
- 五、模型调用优化:缓存与请求合并
-
- [1. 智能缓存:相同问题不重复调用模型](#1. 智能缓存:相同问题不重复调用模型)
- [2. 请求合并:减少并发模型调用](#2. 请求合并:减少并发模型调用)
- [3. 模型路由:智能分发,降本增效](#3. 模型路由:智能分发,降本增效)
- 六、Docker容器化部署
-
- [1. Spring Boot应用Dockerfile](#1. Spring Boot应用Dockerfile)
- [2. 完整技术栈Docker Compose](#2. 完整技术栈Docker Compose)
- [3. Nginx反向代理配置](#3. Nginx反向代理配置)
- [4. 一键启动脚本](#4. 一键启动脚本)
- 七、Kubernetes集群部署方案(概要)
- 八、企业级最佳实践
-
- [1. 混合部署策略(核心)](#1. 混合部署策略(核心))
- [2. 灰度发布策略](#2. 灰度发布策略)
- [3. 灾备方案](#3. 灾备方案)
- [4. 成本优化清单](#4. 成本优化清单)
- 九、常见坑与解决方案
- 十、本章总结与下章预告
- 十一、课后练习
一、本章核心学习目标
学完本章,你将能够:
- 独立完成Ollama生产级环境的配置与性能调优
- 实现模型调用缓存与请求合并,大幅降低API成本
- 掌握Spring AI应用的负载均衡与水平扩展方案
- 使用Docker Compose一键部署完整的AI应用技术栈
- 了解Kubernetes集群部署方案与最佳实践
- 实现混合部署架构:核心敏感数据走本地模型,通用任务走商业API
- 让你的智能办公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 │ │ │
└──────────────┴──────────────┴──────────────────────────────┘
核心设计原则:
- 无状态应用层:Spring Boot应用本身无状态,可以随意水平扩展
- 有状态服务独立部署:Ollama(模型运行)、向量数据库、PostgreSQL各有状态,需要独立管理
- 混合模型策略:敏感数据走本地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集群。
核心设计要点
-
应用层(Deployment + HPA):
- Spring Boot应用以无状态Deployment方式部署,副本数3-10
- 使用HPA(Horizontal Pod Autoscaler)根据CPU/内存使用率自动扩缩
- 添加基于AI调用QPS的自定义HPA指标
-
模型服务层(StatefulSet):
- Ollama以StatefulSet方式部署,每个Pod绑定一块GPU
- 使用NodeSelector将Ollama Pod调度到有GPU的节点
- 配置PodAntiAffinity确保每个GPU节点只运行一个Ollama实例
-
向量数据库层:
- Qdrant/Milvus以StatefulSet方式部署,使用持久化存储卷
- 配置读写分离以提升性能
-
核心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后重启数据就没了。
解决:
- Chroma的本地模式不适合Docker环境,生产环境推荐Qdrant或Milvus
- 务必为向量数据库挂载持久化Volume
- 定期备份向量数据
坑4:大模型并发瓶颈
现象 :Ollama的OLLAMA_NUM_PARALLEL设置过高,导致显存不足(OOM),所有请求都失败。
解决:
- 用
nvidia-smi监控GPU显存使用情况 - 逐步增加
OLLAMA_NUM_PARALLEL,找到最优值 - 设置
OLLAMA_MAX_VRAM限制显存使用上限 - 请求排队机制:超过并行限制的请求进入队列等待
坑5:忽略了Ollama模型"冷启动"时间
现象 :设置了OLLAMA_KEEP_ALIVE=5m,用户隔几分钟问一个问题,每次都要等模型重新加载(3-15秒)。
解决:
- 将
OLLAMA_KEEP_ALIVE设置得长一些(如30分钟或1小时) - 使用定时任务定期发送简单请求"保活"模型
- 或者接受冷启动延迟,在用户体验层增加加载提示
十、本章总结与下章预告
本章总结
- Ollama生产环境配置的核心:修改存储位置、设置并发参数、合理配置Keep Alive
- 硬件选择:7B模型CPU可跑,14B需要GPU,32B需要企业级GPU
- 性能优化三板斧:语义缓存(减少调用)+ 请求合并(提升吞吐)+ 模型路由(降低成本)
- Docker Compose可以一键部署完整的AI应用技术栈(8个服务组件)
- 混合部署策略是最终落地方案:敏感数据走本地Ollama,通用任务走商业API
- Kubernetes适用于高并发SaaS场景,企业内部应用用Docker Compose即可
- 成本优化最有效的手段是语义缓存和模型路由
下章预告
至此,你已经掌握了从环境搭建到生产部署的全部技术栈。只剩下最后一步------找到一份满意的Java AI工程师工作。下一章(也是本系列的最后一章),我会给你整理2026年Java AI工程师的求职全攻略,包括岗位分析、简历优化、面试题汇总、作品集准备和职业发展路径。
十一、课后练习
- 在你的服务器上完成Ollama的生产环境配置,修改模型存储位置
- 使用Docker Compose在本地部署完整的AI应用技术栈,验证所有服务正常运行
- 实现语义缓存功能,测试缓存命中率和对响应时间的影响
- 根据你的实际需求,规划混合部署策略中哪些场景走本地模型,哪些走商业API
- 思考:如果你的应用从100用户增长到1000用户,部署架构需要做哪些调整?