从 0 开始搭建 AI 应用的监控体系——给没有运维团队的小团队
从 0 开始搭建 AI 应用的监控体系——给没有运维团队的小团队
去年有个朋友找我聊,他们 5 个人的团队,做了一个基于 GPT-4 的内容生成 SaaS。产品刚起来,付费用户还不多,但已经开始出现各种问题:有时候 API 报错用户没有提示,有时候响应很慢但不知道是自己的问题还是 OpenAI 的问题,每个月 OpenAI 账单到了才发现超预算了。
他问我:我们用什么监控?
我说:你们用 Datadog 吗?他说没有,贵。CloudWatch?也没有。Dynatrace?更没有。
然后我问:你们有 Docker Compose 会用吗?他说会。
我说:那行,一个下午就能搭起来。
这篇文章就是写给这类团队的:5-10 人、没有专职运维、需要能在生产环境快速落地的最小可行监控体系。不是大厂那套完整的 SRE,是够用就好的轻量版。
技术选型:为什么是 Prometheus + Grafana + Loki
在讲配置之前,先说清楚为什么选这套工具,而不是其他方案。
为什么不用商业 SaaS(Datadog/New Relic/Dynatrace):费用是关键。按主机收费的模式,3 台服务器一年就要几万元,对于早期 SaaS 团队是不合理的成本。而且这些工具功能太多,用不到 90%,学习成本高。
为什么不用云厂商的监控(CloudWatch/云监控):绑定云平台,迁移成本高。另外,AI 应用的监控指标很定制化,云厂商的通用监控做不到 "统计每个功能的 Token 消耗",需要自定义指标,而云厂商的自定义指标方案通常比较复杂且收费。
为什么是这套组合:
- Prometheus:pull-based 的指标收集,部署简单,生态好,Java/Spring Boot 的集成非常成熟
- Grafana:最成熟的可视化工具,有海量现成的仪表盘模板,对接 Prometheus 开箱即用
- Loki:Grafana 家族的日志系统,轻量(不像 ELK 那样重),和 Grafana 的集成是原生的
- Alertmanager:Prometheus 配套的告警管理,支持告警去重、分组、发送到钉钉/Slack
这套组合的资源消耗:1 核 2GB 的机器能跑起来,生产环境建议 2 核 4GB。
Docker Compose 一键启动监控栈
先看完整的 docker-compose.yml,然后逐块解释:
version: '3.8'
networks:
monitoring:
driver: bridge
volumes:
prometheus_data: {}
grafana_data: {}
loki_data: {}
alertmanager_data: {}
services:
# ============ 指标收集 ============
prometheus:
image: prom/prometheus:v2.50.0
container_name: prometheus
restart: unless-stopped
volumes:
- ./config/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./config/prometheus/rules/:/etc/prometheus/rules/:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
# 保留 30 天的指标数据
- '--storage.tsdb.retention.time=30d'
# 限制存储大小,防止磁盘爆满
- '--storage.tsdb.retention.size=10GB'
- '--web.enable-lifecycle'
- '--web.enable-admin-api'
ports:
- "9090:9090"
networks:
- monitoring
# ============ 告警管理 ============
alertmanager:
image: prom/alertmanager:v0.27.0
container_name: alertmanager
restart: unless-stopped
volumes:
- ./config/alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
- alertmanager_data:/alertmanager
command:
- '--config.file=/etc/alertmanager/alertmanager.yml'
- '--storage.path=/alertmanager'
ports:
- "9093:9093"
networks:
- monitoring
# ============ 可视化 ============
grafana:
image: grafana/grafana:10.3.0
container_name: grafana
restart: unless-stopped
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin123}
- GF_USERS_ALLOW_SIGN_UP=false
# 启用匿名访问(内网环境可以开,公网不要开)
# - GF_AUTH_ANONYMOUS_ENABLED=true
volumes:
- grafana_data:/var/lib/grafana
# 预置数据源配置,启动时自动加载
- ./config/grafana/provisioning/:/etc/grafana/provisioning/:ro
# 预置仪表盘
- ./config/grafana/dashboards/:/var/lib/grafana/dashboards/:ro
ports:
- "3000:3000"
depends_on:
- prometheus
- loki
networks:
- monitoring
# ============ 日志系统 ============
loki:
image: grafana/loki:2.9.4
container_name: loki
restart: unless-stopped
volumes:
- ./config/loki/loki-config.yml:/etc/loki/local-config.yaml:ro
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
ports:
- "3100:3100"
networks:
- monitoring
# 日志收集 Agent,部署在每台应用服务器上
promtail:
image: grafana/promtail:2.9.4
container_name: promtail
restart: unless-stopped
volumes:
- ./config/promtail/promtail-config.yml:/etc/promtail/config.yml:ro
# 挂载应用日志目录
- /var/log/app:/var/log/app:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
command: -config.file=/etc/promtail/config.yml
depends_on:
- loki
networks:
- monitoring
# ============ 基础指标采集 ============
# 采集宿主机的 CPU、内存、磁盘、网络指标
node-exporter:
image: prom/node-exporter:v1.7.0
container_name: node-exporter
restart: unless-stopped
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
ports:
- "9100:9100"
networks:
- monitoring
# 采集 Docker 容器指标
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.47.2
container_name: cadvisor
restart: unless-stopped
privileged: true
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
ports:
- "8080:8080"
networks:
- monitoringPrometheus 配置:
# config/prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
cluster: 'prod'
env: 'production'
# 告警规则文件
rule_files:
- "/etc/prometheus/rules/*.yml"
# 告警接收器
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
scrape_configs:
# Prometheus 自己
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
# 宿主机指标
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
# 容器指标
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
# AI 应用指标(Spring Boot Actuator 暴露的 /actuator/prometheus)
- job_name: 'ai-app'
scrape_interval: 10s
metrics_path: '/actuator/prometheus'
static_configs:
- targets:
- 'ai-app:8080'
# 添加标签,方便在 Grafana 里过滤
relabel_configs:
- source_labels: [__address__]
target_label: instance
- target_label: app
replacement: 'ai-chat-service'AI 指标采集:Spring Boot 集成
Spring Boot 的 Micrometer 库让自定义指标非常简单:
@Component
@Slf4j
public class AIMetricsCollector {
private final MeterRegistry meterRegistry;
// 初始化所有需要的指标
@PostConstruct
public void initMetrics() {
// 当前活跃的 AI 请求数(Gauge,实时反映)
Gauge.builder("ai.requests.active", this, AIMetricsCollector::getActiveRequests)
.description("当前正在处理的 AI 请求数")
.register(meterRegistry);
}
private final AtomicInteger activeRequests = new AtomicInteger(0);
public int getActiveRequests() {
return activeRequests.get();
}
/**
* 记录一次 AI 调用的完整指标
*/
public void recordAICall(String feature, String model, String userTier,
int promptTokens, int completionTokens,
long latencyMs, boolean success) {
String[] commonTags = {
"feature", feature,
"model", model,
"user_tier", userTier,
"success", String.valueOf(success)
};
// 调用计数
meterRegistry.counter("ai.calls.total", commonTags).increment();
// 延迟分布(Timer,自动计算 P50/P95/P99)
meterRegistry.timer("ai.call.duration",
"feature", feature,
"model", model
).record(latencyMs, TimeUnit.MILLISECONDS);
if (success) {
// Token 消耗
meterRegistry.counter("ai.tokens.prompt",
"feature", feature, "model", model
).increment(promptTokens);
meterRegistry.counter("ai.tokens.completion",
"feature", feature, "model", model
).increment(completionTokens);
// 成本(美分)
long costCents = estimateCostCents(model, promptTokens, completionTokens);
meterRegistry.counter("ai.cost.cents",
"feature", feature, "model", model
).increment(costCents);
}
}
/**
* 请求开始时增加活跃计数
*/
public AutoCloseable trackActiveRequest() {
activeRequests.incrementAndGet();
return () -> activeRequests.decrementAndGet();
}
private long estimateCostCents(String model, int promptTokens, int completionTokens) {
return switch (model) {
case "gpt-4o" -> Math.round((promptTokens * 5.0 + completionTokens * 15.0) / 1_000_000 * 100);
case "gpt-4o-mini" -> Math.round((promptTokens * 0.15 + completionTokens * 0.6) / 1_000_000 * 100);
default -> 0L;
};
}
}在 AI 服务里使用:
@Service
public class AIService {
@Autowired
private AIMetricsCollector metricsCollector;
@Autowired
private ChatClient chatClient;
public String chat(String feature, String model, String userTier,
String systemPrompt, String userInput) {
long startTime = System.currentTimeMillis();
boolean success = false;
int promptTokens = 0, completionTokens = 0;
// 追踪活跃请求数
try (var ignored = metricsCollector.trackActiveRequest()) {
ChatResponse response = chatClient.prompt()
.system(systemPrompt)
.user(userInput)
.call()
.chatResponse();
promptTokens = response.getMetadata().getUsage().getPromptTokens().intValue();
completionTokens = response.getMetadata().getUsage().getGenerationTokens().intValue();
success = true;
return response.getResult().getOutput().getContent();
} catch (Exception e) {
log.error("AI call failed: feature={}, model={}", feature, model, e);
throw e;
} finally {
metricsCollector.recordAICall(
feature, model, userTier,
promptTokens, completionTokens,
System.currentTimeMillis() - startTime,
success
);
}
}
}application.yml 中开启 Prometheus 端点:
management:
endpoints:
web:
exposure:
include: prometheus,health,info
endpoint:
prometheus:
enabled: true
metrics:
export:
prometheus:
enabled: true
distribution:
percentiles-histogram:
ai.call.duration: true # 开启直方图,才能计算 P95/P99
percentiles:
ai.call.duration: 0.5, 0.95, 0.99告警规则配置
# config/prometheus/rules/ai-alerts.yml
groups:
- name: ai-service-alerts
rules:
# AI 服务错误率告警
- alert: AIServiceHighErrorRate
expr: |
sum(rate(ai_calls_total{success="false"}[5m]))
/
sum(rate(ai_calls_total[5m]))
> 0.05
for: 3m
labels:
severity: warning
annotations:
summary: "AI 服务错误率过高: {{ $value | humanizePercentage }}"
description: "过去 5 分钟错误率 {{ $value | humanizePercentage }},超过阈值 5%"
# P99 延迟告警
- alert: AIServiceHighLatency
expr: |
histogram_quantile(0.99,
sum(rate(ai_call_duration_seconds_bucket[5m])) by (le, feature)
) > 15
for: 5m
labels:
severity: warning
annotations:
summary: "AI 服务 P99 延迟过高: {{ $labels.feature }}"
description: "功能 {{ $labels.feature }} P99 延迟 {{ $value | humanizeDuration }},超过 15 秒"
# 成本突增告警
- alert: AIHighCostSurge
expr: |
sum(rate(ai_cost_cents_total[1h])) * 3600
>
sum(rate(ai_cost_cents_total[1h] offset 7d)) * 3600 * 2
for: 15m
labels:
severity: warning
annotations:
summary: "AI 成本突增,是上周同时段的 2 倍以上"
# 活跃请求数告警(防止连接池打满)
- alert: AIHighConcurrency
expr: ai_requests_active > 50
for: 2m
labels:
severity: warning
annotations:
summary: "AI 并发请求数过高: {{ $value }}"告警接收器配置(发送到钉钉):
# config/alertmanager/alertmanager.yml
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'feature']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receiver: 'dingtalk'
routes:
# critical 级别告警立即发送,不等分组
- match:
severity: critical
group_wait: 10s
receiver: 'dingtalk'
receivers:
- name: 'dingtalk'
webhook_configs:
- url: 'http://dingtalk-webhook:8060/dingtalk/default/send'
send_resolved: true
http_config:
follow_redirects: true
inhibit_rules:
# 如果 critical 告警触发,抑制同一 feature 的 warning
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['feature']整体轻量级监控架构
快速启动步骤
把以上配置文件准备好后,一键启动:
# 克隆配置仓库
git clone https://github.com/yourteam/monitoring-stack.git
cd monitoring-stack
# 设置 Grafana 密码
echo "GRAFANA_PASSWORD=your_password" > .env
# 启动所有服务
docker compose up -d
# 检查状态
docker compose ps
# 导入 AI 应用的 Grafana 仪表盘
# 访问 http://localhost:3000,用 admin/your_password 登录
# 在 Dashboards > Import 里导入 config/grafana/dashboards/ai-overview.json大约 2 分钟后,访问 http://your-server:3000 就能看到 Grafana 界面。
起步优先级
对于刚起步的团队,不需要把所有东西都一次性搞定。按优先级:
第一优先级(必须有):
- AI 服务错误率告警:用户出问题你要第一时间知道
- AI 服务 P95 延迟监控:体验问题的预警
- 成本每日报告:防止账单爆炸
第二优先级(重要但可以后做):
- 结构化日志接入 Loki:出问题时有日志可查
- Token 消耗分 feature 的统计:成本优化的基础
- 主机资源监控:防止磁盘/内存撑爆
第三优先级(有余力再做):
- Prompt 效果分析看板
- 用户行为分析
- 成本趋势预测
我见过太多团队在监控上追求完美,搞了三周没上线。先把第一优先级的做完部署,后面再慢慢完善。
对比大厂方案
这套轻量级方案和 SRE 体系的大厂方案有什么差距?坦白说:
- 没有全链路追踪:Jaeger/Tempo 没有,如果需要追踪 Agent 的完整调用链,需要额外搭建
- 没有自动异常检测:Prometheus 的告警是规则驱动的,需要人工写规则。大厂用 ML-based 异常检测,能发现不知道会发生的问题
- 告警规则维护成本:随着服务增多,规则越来越多,需要定期清理过时的规则
- 高可用没有做:单机部署,监控系统本身挂了就没有监控了
但对于 5-10 人的团队,这些差距是可以接受的。这套方案能解决 80% 的监控需求,成本几乎为零(只需要一台 2C4G 的服务器)。
等团队规模到了 20-30 人,再考虑引入 Jaeger 做分布式追踪,引入 Elastic APM 做更完整的 APM,那时候也有人力去运维更复杂的系统了。
成本和复杂度要和团队阶段匹配,这是我这几年最核心的工程判断。
