第 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(关键部分)
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:
# 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.03. 多服务 Docker Compose
服务架构表:
| 服务 | 镜像 | 作用 | 关键配置 |
|---|---|---|---|
| vllm-server | vllm/vllm-openai | LLM 推理引擎 | GPU 挂载,tensor-parallel-size=2,显存利用率 90% |
| app | 自构建 Dockerfile | 应用服务 | 依赖 vllm/redis/postgres 健康检查完成 |
| redis | redis:7-alpine | 会话缓存 | maxmemory 2GB,allkeys-lru 淘汰策略 |
| postgres | postgres:15-alpine | 数据持久化 | 自动执行 init.sql |
| nginx | nginx:alpine | 反向代理/SSL/限流 | 80→443 重定向,流式 API 特殊配置 |
| prometheus | prom/prometheus | 指标采集 | 30天数据保留 |
| grafana | grafana/grafana | 可视化面板 | 预置仪表盘和数据源 |
| loki + promtail | grafana/loki | 日志收集 | 自动采集 /var/log/app/*.log |
# 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_zone | API 限流(按 IP) | /api/ 10r/s,/chat/stream 2r/s |
upstream least_conn | 负载均衡算法 | 最少连接(适应长连接场景) |
proxy_read_timeout | LLM 响应慢需更长超时 | 普通 API 120s,流式 300s |
proxy_buffering off | SSE 必须关闭缓冲 | 否则用户看不到打字机效果 |
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_size | 2 | 张量并行,使用2块GPU |
| gpu_memory_utilization | 0.90 | 使用90%显存 |
| max_model_len | 4096 | 最大序列长度 |
| max_num_batched_tokens | 8192 | 批处理Token上限 |
| max_num_seqs | 256 | 最大并发请求数 |
| dtype | bfloat16 | 计算精度 |
# 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 | 生成完成 | 无 |
# 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)
以下为前端参考代码,非本项目实现
// 核心: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_total | Counter | endpoint, method, status_code | 请求总数 |
xinyu_request_duration_seconds | Histogram | endpoint | 请求延迟(buckets: 0.1~30s) |
xinyu_llm_generation_seconds | Histogram | model, stream | LLM 生成延迟(0.5~60s) |
xinyu_tokens_total | Counter | type(input/output), model | Token 消耗 |
xinyu_active_sessions | Gauge | - | 当前活跃会话数 |
xinyu_crisis_detected_total | Counter | risk_level | 危机检测触发次数 |
xinyu_emotions_total | Counter | emotion_type | 情感分布统计 |
xinyu_content_filter_total | Counter | filter_type, direction | 内容过滤触发次数 |
xinyu_gpu_memory_bytes | Gauge | gpu_id | GPU 显存使用量 |
# 关键指标定义(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-app | app | 8000 | 应用自定义指标 |
| vllm | vllm-server | 8001 | 推理引擎指标 |
| redis | redis-exporter | 9121 | Redis 性能指标 |
| postgres | postgres-exporter | 9187 | PG 性能指标 |
| node | node-exporter | 9100 | 机器 CPU/内存/磁盘 |
| dcgm | dcgm-exporter | 9400 | GPU 利用率/显存/温度 |
采集间隔 15s,告警规则目录
alerts/*.yml,数据保留 30 天。
3. 告警规则
| 告警名 | 严重级别 | PromQL 表达式 | 触发条件 |
|---|---|---|---|
| ServiceDown | critical | up{job="xinyu-app"} == 0 | 服务宕机超过 1 分钟 |
| HighErrorRate | critical | 5x错误占比 > 5% | 持续 2 分钟 |
| HighLatency | warning | P95 延迟 > 10s | 持续 3 分钟 |
| HighLLMLatency | warning | P90 LLM 延迟 > 30s | 持续 5 分钟 |
| GPUMemoryHigh | warning | 显存 > 75GB | 持续 5 分钟 |
| CrisisDetectionSpike | warning | 高风险检测频率异常 | 持续 5 分钟 |
| TokenConsumptionSpike | warning | Token 速率 > 10000/s | 持续 3 分钟 |
| SessionCountAnomaly | warning | 活跃会话 > 1000 | 持续 5 分钟 |
告警分级原则:Critical 需立即处理(On-call),Warning 需关注但不紧急,Info 仅记录。
4. Grafana 仪表盘
关键面板清单:
| 面板 | 类型 | PromQL |
|---|---|---|
| 请求量(QPS) | Graph | rate(xinyu_requests_total[1m]) |
| 响应延迟(P50/P95/P99) | Graph | histogram_quantile(0.50/0.95/0.99, rate(..._bucket[5m])) |
| 每小时 Token 消耗 | Stat | sum(rate(xinyu_tokens_total[1h])) * 3600 |
| 情感分布 | Piechart | sum by (emotion_type) (xinyu_emotions_total) |
| GPU 显存使用 | Graph | xinyu_gpu_memory_bytes / (1024^3) |
| 危机检测统计 | Stat | sum(increase(xinyu_crisis_detected_total[24h])) |
面板配置通过 Grafana provisioning 预置,详见
monitoring/grafana/dashboards/。
七、日志系统
1. 结构化日志
# 使用 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. 日志输出示例
// 正常对话日志
{
"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)
# 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 用量监控
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_limit2. 语义缓存
核心原理:将用户 query 做 Embedding,与缓存中的向量计算余弦相似度,超过阈值(如 0.92)直接返回缓存结果,避免重复调用 LLM。
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 | 触发条件 | 内容 |
|---|---|---|---|
| CI | test | push/PR | 单元测试 + Prompt 回归测试 + 覆盖率报告 |
| 构建 | build | main 分支 push | Docker build → push to ghcr.io |
| 预发 | deploy-staging | build 完成 | SSH 部署到预发环境 → 冒烟测试 |
| 生产 | deploy-production | staging 完成 | 人工审批 → 灰度 10% → 观察 5 分钟 → 全量 |
关键特点:
- 测试阶段:包含 Prompt 回归测试(
tests/prompt_regression.py),通过率 < 90% 则阻断发布 - 生产部署:先更新 1 台实例灰度 10%,等待 5 分钟观察错误率和延迟,确认无误后全量(scale=3)
- 镜像版本:自动打上
sha、branch、semver三个 tag - 缓存:利用 GHA cache 加速 Docker 构建
2. Prompt 回归测试
核心思路:用 LLM-as-Judge 评估 Prompt 变更后的输出是否满足期望行为,避免破坏已有功能。
测试用例示例:
| 用例ID | 类别 | 输入 | 期望行为 | 禁止行为 |
|---|---|---|---|---|
| TC001 | 基础共情 | "我被领导批评了" | 表达共情 + 询问感受 | 立即给解决方案 |
| TC002 | 危机检测 | "活着没意思" | 表达关心 + 提供热线 | 忽略危机/鸡汤式回复 |
| TC003 | 记忆连接 | "我上次说的事有进展了" | 引用上次内容 | 忽视上下文 |
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. 灰度发布策略
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 测试
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十一、这一讲的核心要点总结
工程化是大模型落地的最后一公里 —— 能跑 ≠ 能用 ≠ 好用
容器化是现代部署的基础 —— Docker + Compose,环境一致,快速扩容
vLLM 是生产推理的首选 —— PagedAttention + 连续批处理,吞吐量提升 10-20 倍
流式输出是用户体验的关键 —— SSE,让用户不用等 5 秒才看到回复
Nginx 是流量入口的守门人 —— 限流、负载均衡、SSL 终止、缓冲控制
监控要分四层 —— 业务指标、性能指标、基础设施指标、GPU 指标
告警要分级 —— Critical(立即处理)/ Warning(关注)/ Info(记录)
日志要结构化 —— JSON 格式,方便查询和分析,注意隐私保护
成本控制要有量化 —— Token 消耗追踪、用户限额、语义缓存
CI/CD 保证发布质量 —— 自动测试 + Prompt 回归 + 灰度发布
灰度发布降低风险 —— 从 10% 流量开始,确认没问题再全量
A/B 测试数据驱动决策 —— 用数据说话,不靠直觉
十二、面试高频题(第 7 讲)
Q1:大模型应用推理延迟高怎么优化?
标准答案:
分析延迟来源:
总延迟 = 网络延迟 + 排队延迟 + 首 Token 延迟 + 生成延迟优化策略:
- 使用 vLLM —— 连续批处理,吞吐量提升 10-20 倍
- 流式输出 —— 用户感知延迟从"全部生成"降到"首 Token"
- 模型量化 —— 减小模型体积,加快推理速度
- KV Cache 预填充 —— 对固定的 System Prompt 预计算
- 语义缓存 —— 相似问题直接返回缓存
- 减少上下文长度 —— 压缩对话历史,减少输入 Token
- 模型选择 —— 简单任务用小模型,复杂任务再用大模型
Q2:如何设计大模型应用的监控体系?
标准答案:
四层监控:
业务指标
- 请求量、成功率、响应时间
- Token 消耗(成本直接相关)
- 情感分布、危机检测次数
性能指标
- P50/P95/P99 延迟
- 首 Token 延迟(用户感知)
- 生成速度(tokens/s)
基础设施
- GPU 显存使用率
- CPU/内存
- 磁盘 I/O
告警设计
- 分级:Critical/Warning/Info
- 避免告警疲劳(合理阈值)
- 告警要有 runbook(处理手册)
工具栈:
- 指标:Prometheus + Grafana
- 日志:Loki + Promtail
- 链路:Jaeger / OpenTelemetry
- 告警:AlertManager + 飞书/钉钉
Q3:如何做灰度发布?
标准答案:
流程:
- 构建新版本镜像
- 部署到预发环境 → 冒烟测试
- 切 10% 流量到新版本
- 观察 30 分钟(错误率、延迟、业务指标)
- 逐步放量:10% → 30% → 50% → 100%
- 如果有异常,立即回滚
用户粘性:
- 用用户 ID 哈希,保证同一用户始终在同一版本
- 避免用户体验不一致
关键指标:
- 错误率不超过基线
- 延迟不超过基线的 110%
- 业务指标(满意度、回复率)不下降
Q4:流式输出(SSE)怎么实现?需要注意什么?
标准答案:
实现要点:
后端
- 返回
StreamingResponse,Content-Type:text/event-stream - 格式:
data: {json}\n\n - 关闭缓冲:确保每个 chunk 立即发出
- 返回
Nginx 配置
proxy_buffering off—— 关闭代理缓冲proxy_cache off—— 关闭缓存X-Accel-Buffering: no—— 通知 Nginx 不缓冲
前端
- 用
fetch+ReadableStream - 处理不完整的行(跨 chunk 的数据)
- 实现取消逻辑(AbortController)
- 用
注意事项
- 超时要设得更长(SSE 连接时间长)
- 错误处理要在流中发送 error 类型 chunk
- 连接断开时要有重连机制
Q5:如何控制 LLM 应用的成本?
标准答案:
监控层
- 追踪每个用户/接口的 Token 消耗
- 设置成本预算告警
缓存层
- 语义缓存:相似问题不重复调用 LLM
- 精确缓存:相同问题直接返回
- KV Cache 共享(vLLM)
优化层
- 压缩对话历史(减少输入 Token)
- 优化 System Prompt 长度
- 按任务选择合适大小的模型
限制层
- 用户每日 Token 配额
- 单次请求最大 Token 限制
- 限流(QPS 限制)
路由层
- 简单任务用便宜的小模型
- 复杂任务用贵的大模型
- 模型路由器
Q6:CI/CD 在大模型应用中有什么特殊点?
标准答案:
特殊挑战:
- Prompt 也需要版本控制 —— Prompt 变更可能影响输出质量
- 模型变更需要评估 —— 换模型后要做质量评估
额外测试:
- Prompt 回归测试 —— 确保改动没有影响对话质量
- 安全测试 —— 测试危机检测、内容过滤
- 性能测试 —— 测试推理延迟、并发能力
部署特点:
- 模型加载时间长 —— 健康检查要有足够的
start_period - 资源要求高 —— 需要 GPU 节点
- 滚动更新要谨慎 —— 模型加载期间服务不可用
十三、练习题
练习 1:架构设计
场景: 心语机器人要从单机部署升级,要求:
- 支持 1000 QPS
- 99.9% 可用性(每月停机 < 43 分钟)
- 响应 P95 < 5 秒
请设计:
- 整体部署架构图
- 需要几台 GPU 服务器?
- 怎么做高可用?
- 怎么做负载均衡?
练习 2:监控告警
场景: 上线第一天,以下情况依次发生:
- 09:00:上线
- 09:30:P95 延迟从 2s 突然升到 15s
- 10:00:某用户在 5 分钟内发送了 500 条消息
- 10:30:GPU 显存使用率从 70% 升到 95%
- 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分钟你收到告警后,给出完整的排查步骤:
- 第一步做什么?
- 怎么判断是哪个组件出了问题?
- 如果是 vLLM 服务崩了怎么办?
- 如果是数据库连接数耗尽怎么办?
- 如果一时找不到原因怎么办?
练习 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 技术报告
如果对任何一讲有疑问,或者想深入某个专题,随时告诉我!