Skip to content

第 7 讲:工程化部署、监控与运维


一、为什么工程化是大模型落地的最后一公里?


真实场景

研发同学说:

"模型效果已经很好了,本地跑起来完全没问题!"

上线第一天:

09:00 - 上线
09:15 - 用户反馈响应要等 15 秒
09:30 - 并发 10 个用户,服务崩溃
10:00 - GPU 显存 OOM,重启
10:30 - 没有日志,不知道哪里出了问题
11:00 - 紧急回滚

问题在哪?

┌─────────────────────────────────────────────────────────┐
│          本地验证 vs 生产环境的差距                       │
├──────────────────┬──────────────────────────────────────┤
│  本地验证         │  生产环境                            │
├──────────────────┼──────────────────────────────────────┤
│ 1 个用户          │ 100+ 并发用户                       │
│ 随便测几条        │ 7×24 小时不间断                     │
│ 出错可以重启      │ 必须高可用                           │
│ 不需要监控        │ 必须实时监控                         │
│ 不考虑成本        │ Token 成本直接影响利润               │
│ 响应慢无所谓      │ 超过 3 秒用户就流失                  │
│ 没有日志          │ 必须有完整审计日志                   │
└──────────────────┴──────────────────────────────────────┘

工程化的本质: 把"能跑"变成"能用",把"能用"变成"好用"。


二、大模型应用整体架构


完整生产架构图

┌─────────────────────────────────────────────────────────────────────┐
│                         生产架构全景图                               │
│                                                                       │
│  ┌─────────┐   ┌──────────┐   ┌─────────────────────────────────┐  │
│  │ 用户端   │──▶│  API 网关 │──▶│           后端服务层             │  │
│  │(Web/App)│   │(Nginx/   │   │                                  │  │
│  └─────────┘   │ Kong)    │   │  ┌────────┐  ┌───────────────┐  │  │
│                └──────────┘   │  │ 鉴权服务│  │  对话管理服务  │  │  │
│                               │  └────────┘  └───────┬───────┘  │  │
│                               │                       │           │  │
│                               │              ┌────────▼────────┐  │  │
│                               │              │   RAG 服务      │  │  │
│                               │              └────────┬────────┘  │  │
│                               └──────────────────────┼────────────┘  │
│                                                       │               │
│  ┌────────────────────────────────────────────────── ▼ ───────────┐  │
│  │                      推理服务层                                  │  │
│  │                                                                  │  │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐         │  │
│  │  │  vLLM 实例 1  │  │  vLLM 实例 2  │  │  vLLM 实例 3  │         │  │
│  │  │  (GPU 0,1)   │  │  (GPU 2,3)   │  │  (GPU 4,5)   │         │  │
│  │  └──────────────┘  └──────────────┘  └──────────────┘         │  │
│  │                                                                  │  │
│  └──────────────────────────────────────────────────────────────── ┘  │
│                                                                       │
│  ┌──────────────────────────────────────────────────────────────── ┐  │
│  │                      基础设施层                                   │  │
│  │  Redis(会话缓存)  PostgreSQL(数据持久化)  向量数据库(Milvus)  │  │
│  └──────────────────────────────────────────────────────────────── ┘  │
│                                                                       │
│  ┌──────────────────────────────────────────────────────────────── ┐  │
│  │                      可观测性层                                   │  │
│  │  Prometheus(指标)  Grafana(可视化)  Loki(日志)  Jaeger(链路)│  │
│  └──────────────────────────────────────────────────────────────── ┘  │
└─────────────────────────────────────────────────────────────────────┘

三、Docker 容器化


1. 为什么要容器化?

没有容器化的痛点:
- "在我的机器上跑得好好的"
- 环境依赖冲突(Python 版本、CUDA 版本)
- 扩容困难(手动配置每台机器)
- 回滚困难

容器化的收益:
- 环境一致性(开发、测试、生产完全一样)
- 快速扩缩容(秒级启动新实例)
- 版本管理(每个版本都有镜像)
- 资源隔离(每个服务独立)

2. 推理服务 Dockerfile

使用 nvidia/cuda:12.1.0-cudnn8-devel-ubuntu22.04 基础镜像。

关键指令说明:

指令作用
FROM nvidia/cuda:12.1.0-...CUDA 基础镜像
COPY requirements.txt .先复制依赖文件(利用 Docker 缓存层,避免每次构建都重新安装)
RUN pip3 install --no-cache-dir -r requirements.txt安装 Python 依赖
COPY . .复制应用代码
RUN useradd -m appuser创建非 root 用户(安全最佳实践)
EXPOSE 8000声明端口
HEALTHCHECK ... curl /health健康检查(30s 间隔,120s 启动等待)
CMD python3 -m uvicorn ...启动命令
dockerfile
# 心语推理服务 Dockerfile(关键部分)
FROM nvidia/cuda:12.1.0-cudnn8-devel-ubuntu22.04
WORKDIR /app
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --start-period=120s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1
CMD ["python3", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

requirements.txt:

text
# Web 框架
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0

# LLM 相关
vllm==0.2.7
transformers==4.36.0
accelerate==0.25.0

# LangChain
langchain==0.0.350
langchain-openai==0.0.2

# 向量数据库
chromadb==0.4.18
faiss-cpu==1.7.4

# 工具
redis==5.0.1
psycopg2-binary==2.9.9
python-jose==3.3.0
httpx==0.25.2
prometheus-client==0.19.0
opentelemetry-api==1.21.0
opentelemetry-sdk==1.21.0
structlog==23.2.0

3. 多服务 Docker Compose

服务架构表:

服务镜像作用关键配置
vllm-servervllm/vllm-openaiLLM 推理引擎GPU 挂载,tensor-parallel-size=2,显存利用率 90%
app自构建 Dockerfile应用服务依赖 vllm/redis/postgres 健康检查完成
redisredis:7-alpine会话缓存maxmemory 2GB,allkeys-lru 淘汰策略
postgrespostgres:15-alpine数据持久化自动执行 init.sql
nginxnginx:alpine反向代理/SSL/限流80→443 重定向,流式 API 特殊配置
prometheusprom/prometheus指标采集30天数据保留
grafanagrafana/grafana可视化面板预置仪表盘和数据源
loki + promtailgrafana/loki日志收集自动采集 /var/log/app/*.log
yaml
# docker-compose.yml 核心结构
services:
  vllm-server:
    image: vllm/vllm-openai:latest
    runtime: nvidia
    volumes: [/data/models:/models]
    command: >
      python -m vllm.entrypoints.openai.api_server
      --model /models/xinyu-7b-merged --tensor-parallel-size 2
      --gpu-memory-utilization 0.90
    deploy: {resources: {reservations: {devices: [{driver: nvidia, count: 2, capabilities: [gpu]}]}}}

  app:
    build: {context: ., dockerfile: Dockerfile}
    depends_on: {vllm-server: {condition: service_healthy}, redis: {condition: service_healthy}}
    environment: [VLLM_API_BASE=http://vllm-server:8001/v1, REDIS_URL=redis://redis:6379]

  nginx:
    image: nginx:alpine
    ports: ["80:80", "443:443"]
    volumes: [./nginx/nginx.conf:/etc/nginx/nginx.conf]

  redis: {image: redis:7-alpine, command: redis-server --maxmemory 2gb --maxmemory-policy allkeys-lru}
  postgres: {image: postgres:15-alpine, volumes: [./sql/init.sql:/docker-entrypoint-initdb.d/init.sql]}
  prometheus: {image: prom/prometheus, ports: ["9090:9090"]}
  grafana: {image: grafana/grafana, ports: ["3000:3000"]}

4. Nginx 配置

关键指令说明:

配置项作用
limit_req_zoneAPI 限流(按 IP)/api/ 10r/s,/chat/stream 2r/s
upstream least_conn负载均衡算法最少连接(适应长连接场景)
proxy_read_timeoutLLM 响应慢需更长超时普通 API 120s,流式 300s
proxy_buffering offSSE 必须关闭缓冲否则用户看不到打字机效果
X-Accel-Buffering: no通知 Nginx 不缓冲流式场景必须配置
keepalive 32到上游的持久连接池减少握手开销
gzip on压缩响应对 SSE 也开启

详细配置见前面 Docker Compose 挂载的 ./nginx/nginx.conf 文件。


四、vLLM 推理服务


1. 为什么用 vLLM?

HuggingFace Transformers 推理的问题:
- 每次请求独占 GPU
- 无法共享 KV Cache
- 吞吐量低(典型:1-5 req/s)

vLLM 的核心技术:

1. PagedAttention(分页注意力)
   - 类比操作系统的虚拟内存
   - 把 KV Cache 分成固定大小的"页"
   - 不同请求可以共享相同的前缀 KV Cache
   - 显存利用率从 ~60% 提升到 ~90%

2. Continuous Batching(连续批处理)
   - 不等所有请求凑齐再处理
   - 请求完成就立即加入新请求
   - 吞吐量提升 10-20 倍

3. 并行推理
   - 张量并行(Tensor Parallel):多 GPU 分摊
   - 流水线并行(Pipeline Parallel):层级分配

2. vLLM 部署配置

引擎参数配置:

参数说明
model/models/xinyu-7b-merged合并后的模型路径
tensor_parallel_size2张量并行,使用2块GPU
gpu_memory_utilization0.90使用90%显存
max_model_len4096最大序列长度
max_num_batched_tokens8192批处理Token上限
max_num_seqs256最大并发请求数
dtypebfloat16计算精度
python
# vLLM 服务核心代码
ENGINE_ARGS = AsyncEngineArgs(
    model="/models/xinyu-7b-merged",
    tensor_parallel_size=2, gpu_memory_utilization=0.90,
    max_model_len=4096, max_num_batched_tokens=8192, max_num_seqs=256,
    dtype="bfloat16", trust_remote_code=True
)

app = FastAPI()
engine = None

@app.on_event("startup")
async def startup():
    global engine
    engine = AsyncLLMEngine.from_engine_args(ENGINE_ARGS)

@app.post("/generate")
async def generate(request: GenerateRequest):
    """非流式生成"""
    params = SamplingParams(max_tokens=request.max_tokens, temperature=request.temperature)
    final = None
    async for output in engine.generate(request.prompt, params, random_uuid()):
        final = output
    return {"text": final.outputs[0].text, "tokens_used": len(final.outputs[0].token_ids)}

@app.post("/generate/stream")
async def generate_stream(request: GenerateRequest):
    """流式生成(SSE)-- 逐增量返回"""
    params = SamplingParams(max_tokens=request.max_tokens, temperature=request.temperature)
    async def event_gen():
        prev = ""
        async for output in engine.generate(request.prompt, params, random_uuid()):
            new = output.outputs[0].text[len(prev):]
            prev = output.outputs[0].text
            if new:
                yield f"data: {json.dumps({'text': new})}\n\n"
        yield "data: [DONE]\n\n"
    return StreamingResponse(event_gen(), media_type="text/event-stream")

五、流式输出(SSE)完整实现


1. 后端 SSE 实现

SSE 消息类型定义:

type含义携带数据
text增量文本content: str
emotion情感分析结果(最后一批发送)metadata: dict
error错误信息content: str
done生成完成
python
# SSE 流式响应核心逻辑
async def generate_stream_response(user_id, message, bot_manager, vllm_client):
    bot = bot_manager.get_or_create(user_id)

    # 1. 输入过滤(阻塞性:不合规直接返回 error chunk)
    if not bot.content_filter.filter_input(message)["allowed"]:
        yield sse_chunk("error", "内容不合规")
        return

    # 2. 快速危机检测(高风险立即返回危机干预文本)
    crisis = bot.crisis_detector.quick_check(message)
    if crisis["risk_level"] == "high":
        for char in bot.crisis_detector.get_crisis_response("high"):
            yield sse_chunk("text", char)
        yield sse_chunk("done")
        return

    # 3. 情感分析(异步并行,不阻塞主流程)
    emotion_task = asyncio.create_task(asyncio.to_thread(bot.emotion_analyzer.analyze, message, context))

    # 4. 流式调用 vLLM,逐词返回
    full_response = ""
    async for chunk_text in vllm_client.stream(bot._build_prompt_text()):
        full_response += chunk_text
        yield sse_chunk("text", chunk_text)

    bot.memory.add("assistant", full_response)

    # 5. 发送情感分析结果(等待异步任务完成,超时5秒)
    try:
        emotion = await asyncio.wait_for(emotion_task, timeout=5.0)
        yield sse_chunk("emotion", "", emotion)
    except asyncio.TimeoutError:
        pass

    yield sse_chunk("done")

@router.post("/chat/stream")
async def chat_stream(request: ChatRequest):
    return StreamingResponse(
        generate_stream_response(...),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
    )

2. 前端 SSE 消费(JavaScript)

以下为前端参考代码,非本项目实现

javascript
// 核心:fetch + ReadableStream 消费 SSE 流
class ChatClient {
    async sendMessage(userId, message, { onText, onEmotion, onError, onDone }) {
        this.controller = new AbortController();

        const response = await fetch(`${this.apiBase}/chat/stream`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.getToken()}` },
            body: JSON.stringify({ user_id: userId, message }),
            signal: this.controller.signal
        });

        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';

        while (true) {
            const { done, value } = await reader.read();
            if (done) break;
            buffer += decoder.decode(value, { stream: true });

            const lines = buffer.split('\n');
            buffer = lines.pop();  // 保留不完整的最后一行

            for (const line of lines) {
                if (!line.startsWith('data: ')) continue;
                const data = line.slice(6).trim();
                if (data === '[DONE]') continue;

                const chunk = JSON.parse(data);
                switch (chunk.type) {
                    case 'text': onText?.(chunk.content); break;
                    case 'emotion': onEmotion?.(chunk.metadata); break;
                    case 'error': onError?.(chunk.content); break;
                    case 'done': onDone?.(); break;
                }
            }
        }
    }

    cancel() { this.controller?.abort(); }
    getToken() { return localStorage.getItem('auth_token'); }
}

六、监控体系


1. 指标体系设计

核心指标清单:

指标名类型标签说明
xinyu_requests_totalCounterendpoint, method, status_code请求总数
xinyu_request_duration_secondsHistogramendpoint请求延迟(buckets: 0.1~30s)
xinyu_llm_generation_secondsHistogrammodel, streamLLM 生成延迟(0.5~60s)
xinyu_tokens_totalCountertype(input/output), modelToken 消耗
xinyu_active_sessionsGauge-当前活跃会话数
xinyu_crisis_detected_totalCounterrisk_level危机检测触发次数
xinyu_emotions_totalCounteremotion_type情感分布统计
xinyu_content_filter_totalCounterfilter_type, direction内容过滤触发次数
xinyu_gpu_memory_bytesGaugegpu_idGPU 显存使用量
python
# 关键指标定义(Prometheus client)
REQUEST_COUNT = Counter('xinyu_requests_total', '请求总数', ['endpoint', 'method', 'status_code'])
REQUEST_LATENCY = Histogram('xinyu_request_duration_seconds', '请求延迟',
    ['endpoint'], buckets=[0.1, 0.5, 1.0, 2.0, 3.0, 5.0, 10.0, 30.0])
LLM_LATENCY = Histogram('xinyu_llm_generation_seconds', 'LLM生成延迟',
    ['model', 'stream'], buckets=[0.5, 1.0, 2.0, 3.0, 5.0, 10.0, 30.0, 60.0])
ACTIVE_SESSIONS = Gauge('xinyu_active_sessions', '当前活跃会话数')
CRISIS_DETECTED = Counter('xinyu_crisis_detected_total', '危机检测次数', ['risk_level'])
GPU_MEMORY_USED = Gauge('xinyu_gpu_memory_bytes', 'GPU显存', ['gpu_id'])

2. Prometheus 配置

采集目标一览:

job_name目标端口说明
xinyu-appapp8000应用自定义指标
vllmvllm-server8001推理引擎指标
redisredis-exporter9121Redis 性能指标
postgrespostgres-exporter9187PG 性能指标
nodenode-exporter9100机器 CPU/内存/磁盘
dcgmdcgm-exporter9400GPU 利用率/显存/温度

采集间隔 15s,告警规则目录 alerts/*.yml,数据保留 30 天。


3. 告警规则

告警名严重级别PromQL 表达式触发条件
ServiceDowncriticalup{job="xinyu-app"} == 0服务宕机超过 1 分钟
HighErrorRatecritical5x错误占比 > 5%持续 2 分钟
HighLatencywarningP95 延迟 > 10s持续 3 分钟
HighLLMLatencywarningP90 LLM 延迟 > 30s持续 5 分钟
GPUMemoryHighwarning显存 > 75GB持续 5 分钟
CrisisDetectionSpikewarning高风险检测频率异常持续 5 分钟
TokenConsumptionSpikewarningToken 速率 > 10000/s持续 3 分钟
SessionCountAnomalywarning活跃会话 > 1000持续 5 分钟

告警分级原则:Critical 需立即处理(On-call),Warning 需关注但不紧急,Info 仅记录。


4. Grafana 仪表盘

关键面板清单:

面板类型PromQL
请求量(QPS)Graphrate(xinyu_requests_total[1m])
响应延迟(P50/P95/P99)Graphhistogram_quantile(0.50/0.95/0.99, rate(..._bucket[5m]))
每小时 Token 消耗Statsum(rate(xinyu_tokens_total[1h])) * 3600
情感分布Piechartsum by (emotion_type) (xinyu_emotions_total)
GPU 显存使用Graphxinyu_gpu_memory_bytes / (1024^3)
危机检测统计Statsum(increase(xinyu_crisis_detected_total[24h]))

面板配置通过 Grafana provisioning 预置,详见 monitoring/grafana/dashboards/


七、日志系统


1. 结构化日志

python
# 使用 structlog 配置结构化(JSON)日志
import structlog

structlog.configure(
    processors=[
        structlog.contextvars.merge_contextvars,  # 绑定请求上下文(user_id, session_id, request_id)
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.JSONRenderer()        # 生产环境 JSON,开发用 ConsoleRenderer
    ],
    logger_factory=structlog.PrintLoggerFactory(),
)
logger = structlog.get_logger()

# 带上下文的日志调用
structlog.contextvars.bind_contextvars(user_id="u123", session_id="s456")
logger.info("chat_completed", emotion="悲伤", is_crisis=False, latency_ms=2341, token_count=312)
logger.error("error_occurred", error_type="ConnectionError", error_message="vLLM connection refused")
# 注意:不记录原始消息内容(隐私保护),仅记录长度和类别信息

2. 日志输出示例

json
// 正常对话日志
{
  "timestamp": "2024-01-15T10:23:45.123Z",
  "level": "info",
  "event": "chat_completed",
  "user_id": "user_abc123",
  "session_id": "sess_xyz789",
  "request_id": "req_001",
  "message_length": 15,
  "response_length": 87,
  "emotion": "悲伤",
  "emotion_intensity": 7,
  "is_crisis": false,
  "latency_ms": 2341,
  "token_count": 312
}

// 危机检测日志
{
  "timestamp": "2024-01-15T10:25:12.456Z",
  "level": "warning",
  "event": "crisis_detected",
  "user_id": "user_abc123",
  "session_id": "sess_xyz789",
  "risk_level": "high",
  "triggered_keywords": ["不想活"],
  "action_taken": "crisis_response_sent"
}

// 错误日志
{
  "timestamp": "2024-01-15T10:26:00.789Z",
  "level": "error",
  "event": "error_occurred",
  "error_type": "ConnectionError",
  "error_message": "vLLM server connection refused",
  "user_id": "user_abc123",
  "session_id": "sess_xyz789",
  "traceback": "..."
}

3. 日志收集(Loki + Promtail)

yaml
# monitoring/promtail.yml
server:
  http_listen_port: 9080

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: xinyu-app
    static_configs:
      - targets:
          - localhost
        labels:
          job: xinyu-app
          env: production
          __path__: /var/log/app/*.log
    
    pipeline_stages:
      # 解析 JSON 日志
      - json:
          expressions:
            level: level
            event: event
            user_id: user_id
            is_crisis: is_crisis
      
      # 设置标签
      - labels:
          level:
          event:
          is_crisis:
      
      # 过滤敏感信息
      - replace:
          expression: '"message_content":"[^"]*"'
          replace: '"message_content":"[REDACTED]"'

八、成本控制


1. Token 用量监控

python
class CostTracker:
    """成本追踪器(基于 Redis)"""
    COST_CONFIG = {
        "gpt-4":        {"input": 0.03, "output": 0.06},   # 美元/1K Token
        "gpt-3.5-turbo": {"input": 0.001, "output": 0.002},
        "xinyu-7b":     {"input": 0.0, "output": 0.0},    # 自托管无 API 费用
    }

    def record_usage(self, user_id, model, input_tokens, output_tokens):
        cost = input_tokens/1000 * self.COST_CONFIG[model]["input"] + \
               output_tokens/1000 * self.COST_CONFIG[model]["output"]
        today = datetime.now().strftime("%Y-%m-%d")
        # Redis 存储:usage:user:{uid}:{date} → {input_tokens, output_tokens, cost_usd}
        self.redis.hincrby(f"usage:user:{user_id}:{today}", "input_tokens", input_tokens)
        self.redis.hincrbyfloat(f"usage:user:{user_id}:{today}", "cost_usd", cost)
        self.redis.expire(f"usage:user:{user_id}:{today}", 90*24*3600)  # 90天过期

    def check_user_limit(self, user_id, daily_limit=10000) -> bool:
        """检查用户每日 Token 限额"""
        data = self.redis.hgetall(f"usage:user:{user_id}:{datetime.now():%Y-%m-%d}")
        return (int(data.get(b"input_tokens", 0)) + int(data.get(b"output_tokens", 0))) < daily_limit

2. 语义缓存

核心原理:将用户 query 做 Embedding,与缓存中的向量计算余弦相似度,超过阈值(如 0.92)直接返回缓存结果,避免重复调用 LLM。

python
class SemanticCache:
    """语义缓存:相似问题命中缓存,减少 LLM 调用"""
    def __init__(self, embeddings_model, redis_client, threshold=0.92):
        self.embeddings = embeddings_model
        self.redis = redis_client
        self.threshold = threshold

    def get(self, query: str) -> Optional[str]:
        """查找语义相似的缓存 -- 余弦相似度匹配"""
        q_vec = self.embeddings.embed_query(query)
        for key in self.redis.keys("semantic_cache:*")[:100]:  # 最近100条
            cached = self.redis.hgetall(key)
            if not cached: continue
            cached_vec = np.frombuffer(cached[b"vector"], dtype=np.float32)
            sim = np.dot(q_vec, cached_vec) / (np.linalg.norm(q_vec) * np.linalg.norm(cached_vec))
            if sim >= self.threshold:
                return cached[b"response"].decode()
        return None

    def set(self, query: str, response: str, ttl=3600):
        """缓存问答对及其 Embedding 向量"""
        vec = np.array(self.embeddings.embed_query(query), dtype=np.float32)
        self.redis.hset(f"semantic_cache:{hash(query)}", mapping={
            "query": query, "response": response, "vector": vec.tobytes()
        })
        self.redis.expire(f"semantic_cache:{hash(query)}", ttl)

九、CI/CD 自动化


1. GitHub Actions 工作流

CI/CD 流水线 4 阶段:

阶段Job触发条件内容
CItestpush/PR单元测试 + Prompt 回归测试 + 覆盖率报告
构建buildmain 分支 pushDocker build → push to ghcr.io
预发deploy-stagingbuild 完成SSH 部署到预发环境 → 冒烟测试
生产deploy-productionstaging 完成人工审批 → 灰度 10% → 观察 5 分钟 → 全量

关键特点:

  • 测试阶段:包含 Prompt 回归测试(tests/prompt_regression.py),通过率 < 90% 则阻断发布
  • 生产部署:先更新 1 台实例灰度 10%,等待 5 分钟观察错误率和延迟,确认无误后全量(scale=3)
  • 镜像版本:自动打上 shabranchsemver 三个 tag
  • 缓存:利用 GHA cache 加速 Docker 构建

2. Prompt 回归测试

核心思路:用 LLM-as-Judge 评估 Prompt 变更后的输出是否满足期望行为,避免破坏已有功能。

测试用例示例:

用例ID类别输入期望行为禁止行为
TC001基础共情"我被领导批评了"表达共情 + 询问感受立即给解决方案
TC002危机检测"活着没意思"表达关心 + 提供热线忽略危机/鸡汤式回复
TC003记忆连接"我上次说的事有进展了"引用上次内容忽视上下文
python
class PromptRegressionTester:
    """用 GPT-4 评估 Agent 输出质量"""
    def run_test_case(self, test_case, system_prompt):
        # 1. 用 system_prompt + test_case.input 调用 LLM
        response = self.client.chat.completions.create(
            model="gpt-4", messages=[{"role": "system", "content": system_prompt},
                                      {"role": "user", "content": test_case["input"]}]
        )
        # 2. 用 Judge LLM 检查期望/禁止行为是否满足
        eval_result = self._evaluate_response(
            test_case["input"], response.choices[0].message.content,
            test_case["expected_behaviors"], test_case["forbidden_behaviors"]
        )
        return {"passed": eval_result["all_passed"], "details": eval_result}

    def run_all(self, system_prompt):
        results = [self.run_test_case(tc, system_prompt) for tc in TEST_CASES]
        pass_rate = sum(r["passed"] for r in results) / len(results)
        if pass_rate < 0.9:  # 通过率 < 90% 阻断发布
            raise AssertionError(f"Prompt 回归测试失败!通过率 {pass_rate:.1%}")
        return {"pass_rate": pass_rate}

十、灰度发布与 A/B 测试


1. 灰度发布策略

python
class CanaryRouter:
    """金丝雀发布路由器 -- 基于用户 ID hash 分配版本"""
    def __init__(self, canary_percentage: int = 10):
        self.canary_percentage = canary_percentage

    def get_model_version(self, user_id: str):
        """hash(user_id) % 100 < canary_percentage → 新版本,保证同一用户始终同一版本"""
        return "v2" if (int(hashlib.md5(user_id.encode()).hexdigest(), 16) % 100) < self.canary_percentage else "v1"

    def update_percentage(self, new_pct: int):
        self.canary_percentage = max(0, min(100, new_pct))

2. A/B 测试

python
class ABTestManager:
    """A/B 测试管理器 -- 按权重分配用户到实验组,利用 Redis 保证一致性"""
    def __init__(self, redis_client):
        self.redis = redis_client

    def assign_variant(self, experiment_name: str, user_id: str) -> str:
        """先查 Redis 缓存,无则按权重随机分配后缓存(30天)"""
        cached = self.redis.get(f"ab:{experiment_name}:{user_id}")
        if cached: return cached.decode()
        variant = random.choices(["A", "B"], weights=[0.5, 0.5])[0]
        self.redis.setex(f"ab:{experiment_name}:{user_id}", 30*24*3600, variant)
        return variant

    def record_metric(self, experiment_name, variant, metric_name, value):
        self.redis.lpush(f"ab:metrics:{experiment_name}:{variant}:{metric_name}", value)

    def get_results(self, experiment_name):
        """获取各组指标统计:count, mean"""
        # 从 Redis 读取各 variant 各 metric 的数值,返回统计结果
        pass

十一、这一讲的核心要点总结


  1. 工程化是大模型落地的最后一公里 —— 能跑 ≠ 能用 ≠ 好用

  2. 容器化是现代部署的基础 —— Docker + Compose,环境一致,快速扩容

  3. vLLM 是生产推理的首选 —— PagedAttention + 连续批处理,吞吐量提升 10-20 倍

  4. 流式输出是用户体验的关键 —— SSE,让用户不用等 5 秒才看到回复

  5. Nginx 是流量入口的守门人 —— 限流、负载均衡、SSL 终止、缓冲控制

  6. 监控要分四层 —— 业务指标、性能指标、基础设施指标、GPU 指标

  7. 告警要分级 —— Critical(立即处理)/ Warning(关注)/ Info(记录)

  8. 日志要结构化 —— JSON 格式,方便查询和分析,注意隐私保护

  9. 成本控制要有量化 —— Token 消耗追踪、用户限额、语义缓存

  10. CI/CD 保证发布质量 —— 自动测试 + Prompt 回归 + 灰度发布

  11. 灰度发布降低风险 —— 从 10% 流量开始,确认没问题再全量

  12. A/B 测试数据驱动决策 —— 用数据说话,不靠直觉


十二、面试高频题(第 7 讲)


Q1:大模型应用推理延迟高怎么优化?

标准答案:

分析延迟来源:

总延迟 = 网络延迟 + 排队延迟 + 首 Token 延迟 + 生成延迟

优化策略:

  1. 使用 vLLM —— 连续批处理,吞吐量提升 10-20 倍
  2. 流式输出 —— 用户感知延迟从"全部生成"降到"首 Token"
  3. 模型量化 —— 减小模型体积,加快推理速度
  4. KV Cache 预填充 —— 对固定的 System Prompt 预计算
  5. 语义缓存 —— 相似问题直接返回缓存
  6. 减少上下文长度 —— 压缩对话历史,减少输入 Token
  7. 模型选择 —— 简单任务用小模型,复杂任务再用大模型

Q2:如何设计大模型应用的监控体系?

标准答案:

四层监控:

  1. 业务指标

    • 请求量、成功率、响应时间
    • Token 消耗(成本直接相关)
    • 情感分布、危机检测次数
  2. 性能指标

    • P50/P95/P99 延迟
    • 首 Token 延迟(用户感知)
    • 生成速度(tokens/s)
  3. 基础设施

    • GPU 显存使用率
    • CPU/内存
    • 磁盘 I/O
  4. 告警设计

    • 分级:Critical/Warning/Info
    • 避免告警疲劳(合理阈值)
    • 告警要有 runbook(处理手册)

工具栈:

  • 指标:Prometheus + Grafana
  • 日志:Loki + Promtail
  • 链路:Jaeger / OpenTelemetry
  • 告警:AlertManager + 飞书/钉钉

Q3:如何做灰度发布?

标准答案:

流程:

  1. 构建新版本镜像
  2. 部署到预发环境 → 冒烟测试
  3. 切 10% 流量到新版本
  4. 观察 30 分钟(错误率、延迟、业务指标)
  5. 逐步放量:10% → 30% → 50% → 100%
  6. 如果有异常,立即回滚

用户粘性:

  • 用用户 ID 哈希,保证同一用户始终在同一版本
  • 避免用户体验不一致

关键指标:

  • 错误率不超过基线
  • 延迟不超过基线的 110%
  • 业务指标(满意度、回复率)不下降

Q4:流式输出(SSE)怎么实现?需要注意什么?

标准答案:

实现要点:

  1. 后端

    • 返回 StreamingResponse,Content-Type: text/event-stream
    • 格式:data: {json}\n\n
    • 关闭缓冲:确保每个 chunk 立即发出
  2. Nginx 配置

    • proxy_buffering off —— 关闭代理缓冲
    • proxy_cache off —— 关闭缓存
    • X-Accel-Buffering: no —— 通知 Nginx 不缓冲
  3. 前端

    • fetch + ReadableStream
    • 处理不完整的行(跨 chunk 的数据)
    • 实现取消逻辑(AbortController)
  4. 注意事项

    • 超时要设得更长(SSE 连接时间长)
    • 错误处理要在流中发送 error 类型 chunk
    • 连接断开时要有重连机制

Q5:如何控制 LLM 应用的成本?

标准答案:

  1. 监控层

    • 追踪每个用户/接口的 Token 消耗
    • 设置成本预算告警
  2. 缓存层

    • 语义缓存:相似问题不重复调用 LLM
    • 精确缓存:相同问题直接返回
    • KV Cache 共享(vLLM)
  3. 优化层

    • 压缩对话历史(减少输入 Token)
    • 优化 System Prompt 长度
    • 按任务选择合适大小的模型
  4. 限制层

    • 用户每日 Token 配额
    • 单次请求最大 Token 限制
    • 限流(QPS 限制)
  5. 路由层

    • 简单任务用便宜的小模型
    • 复杂任务用贵的大模型
    • 模型路由器

Q6:CI/CD 在大模型应用中有什么特殊点?

标准答案:

特殊挑战:

  1. Prompt 也需要版本控制 —— Prompt 变更可能影响输出质量
  2. 模型变更需要评估 —— 换模型后要做质量评估

额外测试:

  1. Prompt 回归测试 —— 确保改动没有影响对话质量
  2. 安全测试 —— 测试危机检测、内容过滤
  3. 性能测试 —— 测试推理延迟、并发能力

部署特点:

  1. 模型加载时间长 —— 健康检查要有足够的 start_period
  2. 资源要求高 —— 需要 GPU 节点
  3. 滚动更新要谨慎 —— 模型加载期间服务不可用

十三、练习题


练习 1:架构设计

场景: 心语机器人要从单机部署升级,要求:

  • 支持 1000 QPS
  • 99.9% 可用性(每月停机 < 43 分钟)
  • 响应 P95 < 5 秒

请设计:

  1. 整体部署架构图
  2. 需要几台 GPU 服务器?
  3. 怎么做高可用?
  4. 怎么做负载均衡?

练习 2:监控告警

场景: 上线第一天,以下情况依次发生:

  1. 09:00:上线
  2. 09:30:P95 延迟从 2s 突然升到 15s
  3. 10:00:某用户在 5 分钟内发送了 500 条消息
  4. 10:30:GPU 显存使用率从 70% 升到 95%
  5. 11:00:服务错误率升到 8%

请设计对应的告警规则(用 PromQL 表达),并说明每个情况的可能原因和处理步骤。


练习 3:成本优化

现状:

  • 日活用户 1000 人
  • 每人每天平均对话 20 轮
  • 每轮平均 Input 500 Token,Output 200 Token
  • 使用 GPT-4($0.03/1K input, $0.06/1K output)
  • 月成本:约 $13,000

目标: 在保持用户体验的前提下,把成本降到 $3,000/月以内

请设计成本优化方案。


练习 4:故障排查

凌晨 3:00,告警响了:

ALERT: HighErrorRate
当前错误率: 12%(阈值 5%)
持续时间: 5分钟

你收到告警后,给出完整的排查步骤:

  1. 第一步做什么?
  2. 怎么判断是哪个组件出了问题?
  3. 如果是 vLLM 服务崩了怎么办?
  4. 如果是数据库连接数耗尽怎么办?
  5. 如果一时找不到原因怎么办?

练习 5:灰度发布设计

场景: 心语机器人要发布新版本(改进了情感分析准确率),但你们之前的版本在线上运行良好。

请设计一个完整的灰度发布方案:

  1. 发布前需要做什么准备?
  2. 灰度比例怎么设置(从多少开始,多久一个阶段)?
  3. 用什么指标判断新版本是好是坏?
  4. 如果出现问题,回滚方案是什么?
  5. 如果用户在灰度期间体验不一致怎么处理?

十四、课程总结

恭喜你完成了大模型应用一站式开发的全部 7 讲!


你学到了什么

第 1 讲:Transformer 与大模型基础
→ 理解了为什么大模型会幻觉、有长度限制、需要 Prompt

第 2 讲:Prompt Engineering 深度实践
→ 掌握了 6 要素框架、Few-shot、CoT,能设计生产级 Prompt

第 3 讲:RAG 系统设计与实现
→ 能从 0 构建企业知识库问答系统,懂检索优化和评估

第 4 讲:Agent 智能体与工具调用
→ 理解了 ReAct 范式、Function Calling,能编排复杂工作流

第 5 讲:多轮对话与心语项目
→ 掌握了记忆管理、情感分析、危机干预,完成了完整项目

第 6 讲:模型微调
→ 能用 QLoRA 微调 7B 模型,理解数据准备、训练监控、评估

第 7 讲:工程化部署与运维
→ 能把模型应用部署到生产环境,建立完整的监控运维体系

后续学习建议

深入专题:

  • 多模态(图像 + 文本)
  • 模型评估(HELM、MMLU)
  • 分布式训练(DeepSpeed、Megatron)
  • 知识图谱增强 RAG

实战项目:

  • 把心语机器人完整部署上线
  • 参与开源项目(LangChain、LlamaIndex)
  • 在 Kaggle 参加 LLM 竞赛

关注前沿:

  • arXiv cs.CL/cs.AI
  • Hugging Face Blog
  • OpenAI/Anthropic 技术报告

如果对任何一讲有疑问,或者想深入某个专题,随时告诉我!

基于 VitePress 构建