AI应用部署上线:Docker打包+API服务+监控告警,我踩了4个坑

前11篇都在本地跑AI应用------python app.py,终端里看输出,调试完事。但产品说: "下周一上线,2000用户同时用。"

本地跑和上线是完全不同的两件事:

维度 本地开发 生产上线
运行环境 你的电脑 服务器(Linux)
并发 1个人用 2000人同时用
稳定性 挂了就重启 挂了要告警+自动恢复
安全 API Key写在代码里 必须加密+权限控制
依赖 pip装在本机 Docker镜像打包

我第一次把AI应用部署到服务器,踩了4个坑。每个坑都是"本地能跑≠线上能用"的经典教训。


先说结论

一句话
环境不一致 Docker打包,别裸奔部署
API并发扛不住 异步+队列+限流,别让LLM调用成为瓶颈
API Key泄露 环境变量+密钥管理,别写死在代码里
线上出问题不知道 日志+监控+告警,别等用户投诉才发现

用Java人的理解:AI应用部署 = Spring Boot应用部署 + 一个特别慢的外部服务(LLM)。所有Spring Boot部署踩过的坑,AI应用一样要踩,还要额外处理LLM的延迟和成本问题。


坑1:本地能跑,服务器跑不起来------环境不一致

翻车现场

bash

bash 复制代码
# 本地
python app.py  # ✅ 正常运行

# 服务器
pip install -r requirements.txt  # ❌ 编译chromadb报错:C++编译器版本不对
python app.py                    # ❌ ModuleNotFoundError: No module named '_sqlite3'

Python的依赖管理是玄学。 不同操作系统、不同Python版本、不同系统库,都可能让同样的代码跑不起来。尤其像chromadb、langchain这种依赖链很长的包,在Linux服务器上装一堆C++扩展,编译失败是家常便饭。

正确做法:Docker打包,环境跟着代码走

dockerfile

bash 复制代码
# Dockerfile
FROM python:3.11-slim

# 系统依赖(chromadb等需要)
RUN apt-get update && apt-get install -y \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# 先复制依赖文件,利用Docker缓存
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 复制代码
COPY . .

# 暴露端口
EXPOSE 8000

# 启动命令
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

yaml

yaml 复制代码
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY}
    env_file:
      - .env
    depends_on:
      - chromadb

  chromadb:
    image: chromadb/chroma:latest
    ports:
      - "8001:8000"
    volumes:
      - chromadb_data:/chroma/chroma

volumes:
  chromadb_data:

关键要点:

要点 说明
用slim镜像 python:3.11-slimpython:3.11小5倍
先复制requirements.txt Docker分层缓存,依赖不变时不用重新pip install
系统依赖显式安装 chromadb等C++扩展需要的库要手动装
环境变量不写死 .env文件或环境变量注入API Key
数据持久化 chromadb数据挂载到Docker Volume,容器重建不丢数据

用Java人的理解:Docker ≈ 你的WAR包 + Tomcat + JDK全打包成一个镜像。不用担心服务器的JDK版本不对------镜像里自带。


坑2:2000用户同时调LLM,API直接打挂

翻车现场

用FastAPI搭了个RAG服务:

python

python 复制代码
from fastapi import FastAPI

app = FastAPI()

@app.get("/chat")
def chat(question: str):
    result = rag.query(question)  # 同步调LLM,3-5秒
    return {"answer": result}

压测结果:

erlang 复制代码
10个并发用户:✅ 平均响应3.2秒
50个并发用户:⚠️ 平均响应15秒,部分超时
200个并发用户:❌ 90%超时,服务器CPU 100%

问题在哪? LLM每次调用3-5秒,同步等待。200个并发=200个线程同时等LLM返回------线程池爆了,CPU也爆了。而且LLM的API有速率限制,并发太高直接429 Too Many Requests。

正确做法:异步+队列+限流

python

python 复制代码
import asyncio
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import redis
import uuid

app = FastAPI()

# ============ 1. 异步LLM调用 ============

async def async_llm_call(prompt: str) -> str:
    """异步调LLM,不阻塞其他请求"""
    from langchain_openai import ChatOpenAI
    llm = ChatOpenAI(model="qwen-plus")
    # 使用ainvoke异步调用
    result = await llm.ainvoke(prompt)
    return result.content


# ============ 2. 限流:控制LLM并发数 ============

MAX_LLM_CONCURRENT = 10  # 最多10个并发LLM调用
llm_semaphore = asyncio.Semaphore(MAX_LLM_CONCURRENT)

async def rate_limited_llm_call(prompt: str) -> str:
    """限流的LLM调用"""
    async with llm_semaphore:
        return await async_llm_call(prompt)


# ============ 3. 异步API ============

class ChatRequest(BaseModel):
    question: str

@app.post("/chat")
async def chat(request: ChatRequest):
    """异步聊天接口"""
    answer = await rate_limited_llm_call(request.question)
    return {"answer": answer}


# ============ 4. 长任务走队列 ============

@app.post("/report/submit")
async def submit_report(request: ChatRequest):
    """提交报告生成任务(长任务,走队列)"""
    task_id = str(uuid.uuid4())
    # 实际项目:把任务丢进Redis队列/Celery
    # redis_client.lpush("report_queue", json.dumps({"task_id": task_id, "question": request.question}))
    return {"task_id": task_id, "status": "processing"}


@app.get("/report/{task_id}")
async def get_report(task_id: str):
    """查询报告生成进度"""
    # 实际项目:从Redis/数据库查任务状态
    return {"task_id": task_id, "status": "processing", "progress": 60}

3层防御:

层次 策略 作用
异步调用 ainvoke代替invoke 一个请求等LLM时不阻塞其他请求
信号量限流 asyncio.Semaphore(10) 最多10个LLM并发,防止打爆API
队列异步 长任务丢队列,返回task_id 报告生成30秒+,不能让用户等

用Java人的理解:异步 ≈ CompletableFuture,信号量 ≈ Semaphore,队列 ≈ RabbitMQ/Kafka。Spring Boot里怎么处理慢接口,AI应用就怎么处理LLM调用。


坑3:API Key写死在代码里,推到GitHub被人偷了

翻车现场

python

ini 复制代码
# main.py
OPENAI_API_KEY = "sk-xxxxxxxxxxxx"  # 写死在代码里
DASHSCOPE_API_KEY = "sk-yyyyyyyyyyyy"

llm = ChatOpenAI(api_key=OPENAI_API_KEY)

代码推到GitHub,2小时后收到邮件: "你的API Key已被用于消费$200+。"

这不是段子------这是真实发生过无数次的惨案。

正确做法:环境变量 + .env文件

python

ini 复制代码
# main.py
import os
from dotenv import load_dotenv

load_dotenv()  # 从.env文件加载环境变量

llm = ChatOpenAI(
    api_key=os.getenv("OPENAI_API_KEY"),  # 从环境变量读取
    model="qwen-plus",
)

bash

ini 复制代码
# .env(不入Git!)
OPENAI_API_KEY=sk-xxxxxxxxxxxx
DASHSCOPE_API_KEY=sk-yyyyyyyyyyyy

bash

bash 复制代码
# .gitignore
.env
.env.*

3条铁律:

铁律 说明
代码里不写Key 所有密钥用os.getenv()读取
.env不入Git .gitignore里加.env
服务器用系统环境变量 生产环境不要用.env文件,用Docker/K8s环境变量或密钥管理服务

用Java人的理解:这和Spring的application.yml里不放数据库密码一样------用环境变量${DB_PASSWORD}注入。AI应用的API Key = 数据库密码,不能写死在代码里。


坑4:线上AI回答质量下降,一周后才发现

翻车现场

RAG上线运行2周,一切看起来正常。直到有用户投诉: "你们AI的回答越来越离谱了,之前还能答对,现在全是废话。"

查了日志发现:知识库的向量索引损坏了一部分,检索结果不对,AI拿到的上下文就是错的------但应用没有报错,只返回了低质量回答。

没有任何监控和告警,线上问题全靠用户投诉发现。

正确做法:3层监控

python

python 复制代码
import logging
import time
from datetime import datetime

# ============ 1. 结构化日志 ============

logger = logging.getLogger("ai_app")
logger.setLevel(logging.INFO)

# 结构化日志格式
formatter = logging.Formatter(
    '{"time": "%(asctime)s", "level": "%(levelname)s", "event": "%(message)s"}'
)

class AILogger:
    """AI应用专用日志"""

    @staticmethod
    def log_query(question: str, answer: str, latency: float, tokens: int = 0):
        """记录查询日志"""
        logger.info(
            f'query_completed | '
            f'question="{question[:50]}" | '
            f'answer_len={len(answer)} | '
            f'latency={latency:.2f}s | '
            f'tokens={tokens}'
        )

    @staticmethod
    def log_error(question: str, error: str):
        """记录错误日志"""
        logger.error(
            f'query_failed | '
            f'question="{question[:50]}" | '
            f'error="{error}"'
        )

    @staticmethod
    def log_llm_call(model: str, prompt_tokens: int, completion_tokens: int, latency: float):
        """记录LLM调用日志"""
        logger.info(
            f'llm_call | '
            f'model={model} | '
            f'prompt_tokens={prompt_tokens} | '
            f'completion_tokens={completion_tokens} | '
            f'latency={latency:.2f}s'
        )


# ============ 2. 健康检查端点 ============

@app.get("/health")
async def health_check():
    """健康检查:检查各组件是否正常"""
    checks = {}

    # 检查LLM
    try:
        start = time.time()
        await rate_limited_llm_call("ping")
        checks["llm"] = {"status": "ok", "latency": f"{time.time()-start:.2f}s"}
    except Exception as e:
        checks["llm"] = {"status": "error", "detail": str(e)}

    # 检查向量库
    try:
        docs = vector_store.similarity_search("test", k=1)
        checks["vector_store"] = {"status": "ok", "doc_count": len(docs)}
    except Exception as e:
        checks["vector_store"] = {"status": "error", "detail": str(e)}

    # 检查Redis(如果用了队列)
    try:
        # redis_client.ping()
        checks["redis"] = {"status": "ok"}
    except Exception as e:
        checks["redis"] = {"status": "error", "detail": str(e)}

    all_ok = all(c["status"] == "ok" for c in checks.values())
    return {"status": "ok" if all_ok else "degraded", "checks": checks}


# ============ 3. 关键指标统计 ============

from collections import defaultdict
from threading import Lock

class Metrics:
    """简单的指标收集器"""

    def __init__(self):
        self.data = defaultdict(list)
        self.lock = Lock()

    def record(self, name: str, value: float):
        with self.lock:
            self.data[name].append({
                "value": value,
                "timestamp": datetime.now().isoformat(),
            })
            # 只保留最近1000条
            if len(self.data[name]) > 1000:
                self.data[name] = self.data[name][-1000:]

    def get_stats(self, name: str) -> dict:
        values = [d["value"] for d in self.data.get(name, [])]
        if not values:
            return {"count": 0}
        return {
            "count": len(values),
            "avg": sum(values) / len(values),
            "min": min(values),
            "max": max(values),
        }

metrics = Metrics()

# 在查询接口中记录指标
@app.post("/chat")
async def chat(request: ChatRequest):
    start = time.time()
    try:
        answer = await rate_limited_llm_call(request.question)
        latency = time.time() - start
        metrics.record("query_latency", latency)
        metrics.record("answer_length", len(answer))
        AILogger.log_query(request.question, answer, latency)
        return {"answer": answer}
    except Exception as e:
        metrics.record("query_error", 1)
        AILogger.log_error(request.question, str(e))
        return {"error": "服务暂时不可用,请稍后重试"}, 503

@app.get("/metrics")
async def get_metrics():
    """指标端点:供监控系统采集"""
    return {
        "query_latency": metrics.get_stats("query_latency"),
        "answer_length": metrics.get_stats("answer_length"),
        "error_count": metrics.get_stats("query_error"),
    }

3层监控:

层次 监控什么 怎么发现异常
结构化日志 每次查询的详细信息 ELK/Grafana Loki 搜日志
健康检查 各组件是否正常 /health端点 + 外部探针
指标统计 延迟、Token用量、错误率 /metrics端点 + Prometheus + Grafana

必须告警的3个场景:

场景 告警条件 意义
LLM调用延迟飙升 avg latency > 10s API可能被限流或服务异常
错误率上升 error rate > 5% 上游服务可能挂了
回答质量下降 avg answer_length < 20字 AI可能开始答非所问或返回空

用Java人的理解:这就是Spring Boot Actuator + Prometheus + Grafana的AI应用版本。健康检查= /actuator/health,指标= /actuator/metrics,日志= logback结构化日志。


完整项目结构

bash 复制代码
ai-rag-app/
├── app/
│   ├── __init__.py
│   ├── main.py           # FastAPI入口
│   ├── rag.py            # RAG核心逻辑
│   ├── config.py         # 配置(从环境变量读取)
│   ├── logger.py         # 日志工具
│   └── metrics.py        # 指标收集
├── tests/
│   ├── test_unit.py      # 单元测试
│   ├── test_integration.py # 集成测试
│   └── golden_dataset.json # 黄金数据集
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── .env.example          # 环境变量模板(入Git)
├── .env                  # 真实环境变量(不入Git)
└── .gitignore

bash

bash 复制代码
# 部署命令
docker compose up -d

# 查看日志
docker compose logs -f app

# 健康检查
curl http://localhost:8000/health

# 指标查看
curl http://localhost:8000/metrics

4个坑的总结

# 错误做法 正确做法 一句话
1 环境不一致 裸奔部署 Docker打包 本地能跑≠线上能用
2 并发扛不住 同步调LLM 异步+队列+限流 LLM是3-5秒的慢IO,必须异步
3 API Key泄露 写死在代码里 环境变量+.env Key推GitHub=给黑客送钱
4 出问题不知道 无监控无告警 日志+健康检查+指标 别等用户投诉才发现

从开发到上线的Checklist

阶段 检查项
打包 Dockerfile能正常build
打包 docker-compose能正常启动
安全 API Key不在代码里
安全 .env在.gitignore里
性能 异步API
性能 LLM调用限流
可观测 结构化日志
可观测 /health健康检查
可观测 /metrics指标端点
测试 黄金数据集测试通过
测试 质量回归测试通过

你部署AI应用踩过什么坑?评论区聊聊 👇

点赞关注「荣码」,大模型转型系列持续更新~

相关推荐
郝学胜-神的一滴1 小时前
Python 高级编程 026:序列内核深剖
开发语言·python·程序人生·软件工程
神奇霸王龙1 小时前
Qwen3.7-Max屠榜:推理成本仅GPT-5.5的1/25
人工智能·python·gpt·ai·aigc·ai编程
xiaotianyuanma1 小时前
【计算机毕业设计】基于java web的社区养老服务管理系统设计与实现
java·开发语言·课程设计
渣男教父1 小时前
Python-openpyxl操作Excel
python
青山木2 小时前
Hot 100 --- 电话号码的字母组合
java·数据结构·算法·leetcode·逻辑回归
霸道流氓气质2 小时前
Java中集成Weka 技术教程:从入门到工程实践
java·开发语言·数据挖掘
Full Stack Developme2 小时前
Tomcat 如何处理HTTP请求
java·http·tomcat
华研前沿标杆游学2 小时前
2026标杆游学落地实践:某科技企业半年效率提升30%的实操
python
爱敲键盘的猴子2 小时前
Java并发编程 -- synchronized
java·java并发编程