Ollama: 项目部署与优化:智能客服系统
部署优化是 SupportBot 的毕业典礼——从开发到上线,从能用到好用。
⚠️ 注意:部署前必须完成安全检查清单——①无 API Key 请求返回 401;②超速请求返回 429;③外部无法直接访问 11434 端口;④SSL 证书有效且 HTTPS 强制;⑤输入输出过滤生效。安全配置"看起来正确"不等于"实际生效",务必用
curl 逐项实测。
📋 前置知识:需要先掌握以下内容
- 第23课:项目设计与开发:智能客服系统
1. 你将学到
- Docker Compose 生产配置:Ollama + FastAPI + Chroma + Nginx
- 性能基准测试:TTFT / Tokens/s / 并发容量优化
- 安全加固:API Key 认证、速率限制、输出过滤
- 监控集成:Prometheus 指标采集 + Grafana 仪表盘
- 上线 Checklist 与运维手册
2. 项目背景
💡 提示:生产环境的环境变量管理最佳实践——所有密钥和敏感配置通过
.env 文件或 Secrets Manager 注入,绝不硬编码在代码或 Docker Compose 文件中。.env 文件加入 .gitignore,仓库中只提供 .env.example 模板。
(1) 部署前的关键问题
Alice 的 SupportBot 已完成开发,但还面临部署挑战:
| 问题 | 风险 | 解决 |
|---|---|---|
| 单机部署无冗余 | 服务中断 | Docker Compose 多服务 |
| 无认证暴露 API | 安全漏洞 | Nginx + API Key |
| 无监控盲区 | 故障发现慢 | Prometheus + Grafana |
| 性能未验证 | 延迟超标 | 基准测试 + 调优 |
| 无运维文档 | 故障恢复慢 | 运维手册 |
3. Docker Compose 生产配置
⚠️ 警告: 生产环境中 Ollama 的
OLLAMA_HOST=0.0.0.0 仅在 Docker 内网安全,端口 11434 绝不可直接映射到公网。Nginx 是唯一的对外入口,必须配置 API Key 认证和 HTTPS。
(1) 生产架构
flowchart TD
A[Internet<br/>:443] --> B[Nginx<br/>SSL + Auth + Rate Limit]
B --> C[FastAPI SupportBot<br/>:8000]
C --> D[Ollama<br/>:11434<br/>GPU Accelerated]
C --> E[Chroma<br/>:8001<br/>Vector Store]
F[Prometheus<br/>:9090] --> G[All Services<br/>Metrics Collection]
G --> H[Grafana<br/>:3001<br/>Dashboards]
(2) 服务清单
| 服务 | 镜像 | 端口 | 资源 | 持久化 |
|---|---|---|---|---|
| nginx | nginx:alpine | 80/443 | 1 vCPU, 512MB | 配置文件 |
| supportbot | custom | 8000 | 2 vCPU, 4GB | — |
| ollama | ollama/ollama | 11434 | 8 vCPU, 16GB, 1x GPU | ollama_data |
| chroma | chromadb/chroma | 8001 | 2 vCPU, 4GB | chroma_data |
| prometheus | prom/prometheus | 9090 | 1 vCPU, 2GB | prometheus_data |
| grafana | grafana/grafana | 3001 | 1 vCPU, 1GB | grafana_data |
▶ 示例 1: 生产 Docker Compose
YAML
# docker-compose.prod.yml
version: "3.8"
services:
nginx:
image: nginx:alpine
container_name: supportbot-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/ssl/certs:ro
depends_on:
supportbot:
condition: service_healthy
restart: unless-stopped
supportbot:
build:
context: ./app
dockerfile: Dockerfile
container_name: supportbot-api
ports:
- "8000:8000"
environment:
- OLLAMA_HOST=http://ollama:11434
- CHROMA_HOST=http://chroma:8000
- API_KEY=${SUPPORTBOT_API_KEY}
- CHAT_MODEL=qwen2.5
- CLASSIFIER_MODEL=llama3.2:3b
- EMBED_MODEL=nomic-embed-text
depends_on:
ollama:
condition: service_healthy
chroma:
condition: service_started
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 15s
timeout: 5s
retries: 3
restart: unless-stopped
ollama:
image: ollama/ollama
container_name: supportbot-ollama
ports:
- "11434:11434"
volumes:
- ollama_data:/root/.ollama
environment:
- OLLAMA_HOST=0.0.0.0:11434
- OLLAMA_NUM_PARALLEL=4
- OLLAMA_KEEP_ALIVE=30m
- OLLAMA_MAX_LOADED_MODELS=2
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:11434/api/tags"]
interval: 30s
timeout: 10s
retries: 5
restart: unless-stopped
chroma:
image: chromadb/chroma
container_name: supportbot-chroma
ports:
- "8001:8000"
volumes:
- chroma_data:/chroma/chroma
environment:
- ANONYMIZED_TELEMETRY=FALSE
restart: unless-stopped
prometheus:
image: prom/prometheus
container_name: supportbot-prometheus
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
restart: unless-stopped
grafana:
image: grafana/grafana
container_name: supportbot-grafana
ports:
- "3001:3000"
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
depends_on: [prometheus]
restart: unless-stopped
volumes:
ollama_data:
chroma_data:
prometheus_data:
grafana_data:
⚠️ 安全警告:Grafana 默认管理员密码为
admin,必须在首次登录后立即修改!生产环境请通过环境变量 GRAFANA_PASSWORD 设置强密码,例如:GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD},并在 .env 文件中配置随机生成的密码值。
输出:
TEXT
📖 仅展示
Docker Compose 生产部署成功,所有服务健康
▶ 示例 2: Nginx 生产配置
NGINX
# nginx/nginx.conf
worker_processes auto;
events {
worker_connections 1024;
}
http {
# Rate limiting zone
limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m;
upstream supportbot {
server supportbot:8000;
}
# Redirect HTTP to HTTPS
server {
listen 80;
return 301 https://$host$request_uri;
}
# HTTPS server
server {
listen 443 ssl;
server_name ai.globalshop.example.com;
ssl_certificate /etc/ssl/certs/server.crt;
ssl_certificate_key /etc/ssl/certs/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
# SupportBot API
location /v1/ {
limit_req zone=api burst=20 nodelay;
# API Key authentication
if ($http_x_api_key = "") {
return 401 '{"error": "API key required"}';
}
proxy_pass http://supportbot;
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;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
# Health check (no auth)
location /health {
proxy_pass http://supportbot/health;
}
}
}
输出:
TEXT
📖 仅展示
// 执行成功
4. 性能基准测试与优化
💡 提示: 基准测试前务必先"预热"——发送 1-2 个请求让模型加载到 GPU。首次请求包含模型加载时间(冷启动 5-30 秒),后续请求才是真实推理延迟。
OLLAMA_KEEP_ALIVE 控制模型驻留时间。
(1) 关键性能目标
| 指标 | 目标 | 测试方法 |
|---|---|---|
| TTFT | < 500ms | 首词延迟测量 |
| P95 延迟 | < 3s | 100 次请求取 P95 |
| 吞吐 | > 20 QPS | 并发测试 |
| 自动解决率 | > 80% | 100 个真实问题评估 |
▶ 示例 3: 基准测试脚本
PYTHON
# benchmark.py
import asyncio
import aiohttp
import time
import json
import statistics
import os
API_URL = "http://localhost:8000/v1/chat"
API_KEY = os.environ.get("SUPPORTBOT_API_KEY", "your-api-key-here")
TEST_QUESTIONS = [
"What is the return policy?",
"How long does shipping take?",
"Combien coûte la livraison?",
"I received a damaged item, what do I do?",
"Compare the wireless headphones models",
"Where is order #88765?",
"退货政策是什么?",
"My product broke after 2 weeks, I want a refund!",
"Do you ship to Germany?",
"What is the warranty for electronics?",
]
async def single_request(session: aiohttp.ClientSession, question: str) -> dict:
start = time.time()
try:
async with session.post(
API_URL,
json={"message": question},
headers={"X-Api-Key": API_KEY},
timeout=aiohttp.ClientTimeout(total=30)
) as response:
data = await response.json()
elapsed = time.time() - start
return {
"question": question[:40],
"latency_s": round(elapsed, 2),
"intent": data.get("intent", "?"),
"handler": data.get("handler", "?"),
"language": data.get("language", "?"),
"status": "success" if response.status == 200 else "error"
}
except Exception as e:
return {"question": question[:40], "latency_s": round(time.time() - start, 2),
"status": "error", "error": str(e)}
async def run_benchmark(concurrency: int = 1, total_requests: int = 50) -> dict:
async with aiohttp.ClientSession() as session:
# Warm up
await single_request(session, "Hello")
# Run benchmark
start = time.time()
tasks = []
for i in range(total_requests):
q = TEST_QUESTIONS[i % len(TEST_QUESTIONS)]
tasks.append(single_request(session, q))
if len(tasks) >= concurrency:
results = await asyncio.gather(*tasks)
tasks = []
if tasks:
results += await asyncio.gather(*tasks)
total_time = time.time() - start
latencies = [r["latency_s"] for r in results if r["status"] == "success"]
errors = sum(1 for r in results if r["status"] == "error")
return {
"concurrency": concurrency,
"total_requests": total_requests,
"total_time_s": round(total_time, 1),
"errors": errors,
"error_rate": round(errors / total_requests * 100, 1),
"avg_latency_s": round(statistics.mean(latencies), 2) if latencies else 0,
"p50_latency_s": round(statistics.median(latencies), 2) if latencies else 0,
"p95_latency_s": round(sorted(latencies)[int(len(latencies) * 0.95)], 2) if latencies else 0,
"max_latency_s": round(max(latencies), 2) if latencies else 0,
"throughput_rps": round(total_requests / total_time, 1)
}
if __name__ == "__main__":
print("=== SupportBot Benchmark ===")
for c in [1, 4, 8]:
result = asyncio.run(run_benchmark(concurrency=c, total_requests=20))
print(f"\nConcurrency {c}:")
for k, v in result.items():
print(f" {k}: {v}")
输出:
TEXT
📖 仅展示
=== SupportBot Benchmark ===
5. 安全加固
⚠️ 警告: 上线前必须逐项验证安全配置——无 API Key 请求应返回 401、超速请求应返回 429、外部无法直接访问 11434 端口。安全配置"看起来正确"不等于"实际生效",务必用
curl 实测。
(1) 安全检查清单
| 检查项 | 配置 | 状态 |
|---|---|---|
| API Key 认证 | Nginx X-Api-Key 校验 | ✅ |
| HTTPS 加密 | Nginx SSL 配置 | ✅ |
| 速率限制 | 60 req/min/IP | ✅ |
| 输入过滤 | Prompt 注入检测 | ✅ |
| 输出过滤 | 敏感内容过滤 | ✅ |
| 数据脱敏 | PII 自动移除 | ✅ |
| Ollama 内网 | 127.0.0.1 绑定 | ✅ |
▶ 示例 4: 集成安全中间件
PYTHON
# security.py - FastAPI security middleware
from fastapi import Request, HTTPException
from fastapi.responses import JSONResponse
import re
import time
from collections import defaultdict
class SecurityMiddleware:
def __init__(self, api_key: str, rate_limit: int = 60):
self.api_key = api_key
self.rate_limit = rate_limit
self.request_counts: dict = defaultdict(list)
self.injection_patterns = [
r"ignore\s+(previous|all)\s+instructions",
r"you\s+are\s+now\s+DAN",
r"show\s+(me\s+)?(your\s+)?system\s+prompt",
r"reveal\s+(your|the)\s+(initial|system)",
]
self.output_patterns = [
r"system\s*prompt[:\s]",
r"RETURN_POLICY_INTERNAL",
r"INTERNAL_PRICING",
]
def check_api_key(self, request: Request) -> bool:
key = request.headers.get("X-Api-Key", "")
return key == self.api_key
def check_rate_limit(self, client_ip: str) -> bool:
now = time.time()
self.request_counts[client_ip] = [
t for t in self.request_counts[client_ip]
if now - t < 60
]
if len(self.request_counts[client_ip]) >= self.rate_limit:
return False
self.request_counts[client_ip].append(now)
return True
def check_input(self, text: str) -> tuple[bool, str]:
for pattern in self.injection_patterns:
if re.search(pattern, text, re.IGNORECASE):
return False, "Potential prompt injection detected"
return True, ""
def check_output(self, text: str) -> tuple[bool, str]:
for pattern in self.output_patterns:
if re.search(pattern, text, re.IGNORECASE):
return False, "Sensitive content in response filtered"
return True, ""
def sanitize_pii(self, text: str) -> str:
text = re.sub(r"[\w.-]+@[\w.-]+\.\w+", "[EMAIL_REDACTED]", text)
text = re.sub(r"\b\d{3}[-.]?\d{3}[-.]?\d{4}\b", "[PHONE_REDACTED]", text)
text = re.sub(r"\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b", "[CC_REDACTED]", text)
return text
输出:
TEXT
📖 仅展示
# 函数定义成功
6. 监控集成
ℹ️ 信息: GPU 监控推荐使用 DCGM Exporter(NVIDIA 官方),可采集 GPU 利用率、VRAM 使用量、温度、功耗等指标。配合 Grafana 的 GPU Dashboard 模板可快速搭建可视化面板,提前发现 OOM 和过热风险。
(1) Grafana 仪表盘指标
| 面板 | 指标 | 单位 | 告警 |
|---|---|---|---|
| 请求延迟 | supportbot_request_duration_seconds | s | P95 > 3s |
| 请求速率 | supportbot_requests_total | req/min | < 5 req/min |
| 错误率 | supportbot_errors_total | % | > 5% |
| GPU 利用率 | DCGM_FI_DEV_GPU_UTIL | % | > 95% |
| VRAM 使用 | DCGM_FI_DEV_FB_USED | MB | > 95% |
| 意图分布 | supportbot_intent_count | count | — |
▶ 示例 5: Prometheus 配置
YAML
# monitoring/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "supportbot"
static_configs:
- targets: ["supportbot:8000"]
metrics_path: /metrics
- job_name: "ollama"
static_configs:
- targets: ["ollama:11434"]
metrics_path: /metrics
scheme: http
- job_name: "node"
static_configs:
- targets: ["node-exporter:9100"]
rule_files:
- "alert_rules.yml"
# alert_rules.yml
groups:
- name: supportbot_alerts
rules:
- alert: HighLatency
expr: histogram_quantile(0.95, rate(supportbot_request_duration_seconds_bucket[5m])) > 3
for: 5m
labels:
severity: warning
annotations:
summary: "SupportBot P95 latency above 3s"
- alert: ServiceDown
expr: up{job="supportbot"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "SupportBot service is down"
- alert: HighErrorRate
expr: rate(supportbot_errors_total[5m]) / rate(supportbot_requests_total[5m]) > 0.05
for: 3m
labels:
severity: warning
annotations:
summary: "SupportBot error rate above 5%"
输出:
TEXT
📖 仅展示
# Prometheus 配置加载成功
# 目标状态:3 个 scrape 目标均 UP
# supportbot (UP) | ollama (UP) | node-exporter (UP)
7. 综合示例:上线 Checklist 与运维手册
💡 提示: 运维手册(Runbook)的价值在故障发生时体现。建议每季度进行一次"消防演习"——故意关闭某个服务,按 Runbook 流程恢复,验证文档的准确性和可操作性,及时更新过时步骤。
PYTHON
# ============================================
# Comprehensive: Production deployment checklist
# Final verification before going live
# ============================================
PRODUCTION_CHECKLIST = {
"infrastructure": {
"items": [
"Docker Compose services all running (6/6)",
"Ollama health check passing",
"Chroma health check passing",
"FastAPI health check passing",
"Nginx SSL certificate valid (not expired)",
"GPU acceleration verified (nvidia-smi inside container)",
],
"commands": [
"docker compose ps",
"curl -f http://localhost:11434/api/tags",
"curl -f http://localhost:8001/api/v1/heartbeat",
"curl -f http://localhost:8000/health",
"curl -f https://ai.globalshop.example.com/health",
]
},
"models": {
"items": [
"qwen2.5:7b pulled and verified",
"llama3.2:3b pulled and verified",
"nomic-embed-text pulled and verified",
"supportbot Modelfile created and tested",
],
"commands": [
"docker exec supportbot-ollama ollama list",
"docker exec supportbot-ollama ollama run supportbot 'Hello'",
]
},
"security": {
"items": [
"API Key authentication working (unauthorized request returns 401)",
"Rate limiting working (burst above limit returns 429)",
"Input injection filter tested",
"Output content filter tested",
"PII sanitization verified",
"Ollama not accessible from internet (port 11434 blocked)",
]
},
"performance": {
"items": [
"Single request latency < 3s (P95)",
"Concurrent 4 requests latency < 5s (P95)",
"Throughput > 10 QPS",
"RAG retrieval relevant (top-3 precision > 80%)",
"Intent classification accuracy > 85%",
]
},
"monitoring": {
"items": [
"Prometheus scraping all services",
"Grafana dashboards created and visible",
"Alert rules configured and tested",
"Log aggregation working",
]
},
"disaster_recovery": {
"items": [
"All configs in Git repository",
"Chroma data backup scheduled (daily)",
"Model files backed up on NFS/S3",
"Recovery procedure documented and tested",
]
}
}
def generate_runbook() -> str:
"""Generate operations runbook."""
runbook = [
"# SupportBot Operations Runbook\n",
"## Service Overview",
"- SupportBot API: https://ai.globalshop.example.com/v1/chat",
"- Health Check: https://ai.globalshop.example.com/health",
"- Grafana: http://localhost:3001 ⚠️ Change default admin password immediately!",
"- Prometheus: http://localhost:9090\n",
"## Common Operations\n",
"### Restart a Service",
"```bash",
"docker compose restart supportbot # Restart API",
"docker compose restart ollama # Restart Ollama",
"```\n",
"### Pull New Model",
"```bash",
"docker exec supportbot-ollama ollama pull model_name",
"```\n",
"### Rebuild Knowledge Base",
"```bash",
"docker exec supportbot-api python indexer.py /app/product_docs",
"```\n",
"### Check Logs",
"```bash",
"docker compose logs -f supportbot # API logs",
"docker compose logs -f ollama # Ollama logs",
"```\n",
"### Emergency: Service Down",
"1. Check: `docker compose ps`",
"2. Restart: `docker compose restart `<service>``",
"3. Verify: `curl https://ai.globalshop.example.com/health`",
"4. If Ollama OOM: restart with `OLLAMA_NUM_PARALLEL=2`",
"5. Escalate: notify ops team\n",
"## Performance Baseline",
"| Metric | Target | Current |",
"|:-------|:-------|:--------|",
"| P95 Latency | < 3s | _fill after benchmark_ |",
"| Throughput | > 10 QPS | _fill after benchmark_ |",
"| Error Rate | < 1% | _fill after benchmark_ |",
"| GPU Util | < 85% | _fill after benchmark_ |",
]
return "\n".join(runbook)
# Generate and save
if __name__ == "__main__":
print(generate_runbook())
with open("RUNBOOK.md", "w") as f:
f.write(generate_runbook())
print("\nRunbook saved: RUNBOOK.md")
❓ 常见问题
Q 部署后首次启动要多久?
A 模型拉取约 10-30 分钟(取决于网速和模型大小)。后续重启约 30 秒(模型已缓存在 Volume 中)。建议提前拉取模型。
Q 生产环境 SSL 证书怎么获取?
A 用 certbot 获取 Let's Encrypt 免费证书,或用 Caddy 自动 HTTPS。内网可用自签名证书。
Q 如何验证安全配置是否生效?
A 1) 无 API Key 请求应返回 401;2) 超速请求应返回 429;3) 外部无法直接访问 11434;4) 注入攻击被拦截。逐一测试。
Q 监控仪表盘需要配置多久?
A 基础仪表盘约 1 小时(Import Node Exporter 模板 + 自定义 Ollama 面板)。告警规则约 30 分钟。总计约 2 小时。
Q SupportBot 上线后如何持续优化?
A 1) 分析日志找低质量回答;2) 补充知识库文档;3) 调优意图分类;4) 扩充 Modelfile MESSAGE 示例;5) 定期重跑基准测试。
Q 如何回滚到上一版本?
A Git checkout 配置文件 → docker compose down → docker compose up -d。模型数据在 Volume 中不受影响。Chroma 数据可从备份恢复。
📖 小节
- Docker Compose 生产配置包含 6 个服务:Nginx + FastAPI + Ollama + Chroma + Prometheus + Grafana
- 性能目标:P95 < 3s、吞吐 > 10 QPS、自动解决率 > 80%
- 安全加固:API Key + HTTPS + 限流 + 输入输出过滤 + 数据脱敏
- 监控集成:Prometheus 采集 + Grafana 可视化 + Alertmanager 告警
- 上线 Checklist 覆盖基础设施、模型、安全、性能、监控、灾备 6 大领域
- 运维手册包含日常操作、故障排查、性能基线、紧急处理流程
📝 作业
- 基础题(难度⭐):使用 Docker Compose 部署 SupportBot 最小版本(Ollama + FastAPI),验证 /health 端点返回正常。
- 进阶题(难度⭐⭐):运行基准测试脚本,记录 P95 延迟、吞吐、错误率,与目标对比。如不达标,调整 OLLAMA_NUM_PARALLEL 或 num_ctx 后重测。
- 挑战题(难度⭐⭐⭐):完成 SupportBot 完整生产部署——包含 Nginx 安全配置 + Prometheus/Grafana 监控 + 告警规则 + 运维手册,并执行上线 Checklist 全部验证。