Ollama: 项目部署与优化:智能客服系统

部署优化是 SupportBot 的毕业典礼——从开发到上线,从能用到好用。

⚠️ 注意:部署前必须完成安全检查清单——①无 API Key 请求返回 401;②超速请求返回 429;③外部无法直接访问 11434 端口;④SSL 证书有效且 HTTPS 强制;⑤输入输出过滤生效。安全配置"看起来正确"不等于"实际生效",务必用 curl 逐项实测。

📋 前置知识:需要先掌握以下内容

1. 你将学到


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) 生产架构

100%
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 数据可从备份恢复。

📖 小节


📝 作业

  1. 基础题(难度⭐):使用 Docker Compose 部署 SupportBot 最小版本(Ollama + FastAPI),验证 /health 端点返回正常。
  2. 进阶题(难度⭐⭐):运行基准测试脚本,记录 P95 延迟、吞吐、错误率,与目标对比。如不达标,调整 OLLAMA_NUM_PARALLEL 或 num_ctx 后重测。
  3. 挑战题(难度⭐⭐⭐):完成 SupportBot 完整生产部署——包含 Nginx 安全配置 + Prometheus/Grafana 监控 + 告警规则 + 运维手册,并执行上线 Checklist 全部验证。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏