第2183篇:AI系统的成本监控告警——当费用超预算时如何第一时间知道
2026/4/30大约 6 分钟
第2183篇:AI系统的成本监控告警——当费用超预算时如何第一时间知道
适读人群:负责AI系统成本管控的工程师和技术负责人 | 阅读时长:约15分钟 | 核心价值:建立实时AI成本监控体系,在成本失控前主动发现并干预
那是一个普通的周三早上。我打开邮箱,看到了一封来自OpenAI的账单提醒。
当月费用:$23,847。
正常月份是$4,000左右。
我以为是账单出错了,点开明细一看——不是出错,是前一天晚上有个定时任务bug,在循环调用GPT-4,每次循环都把上下文带完整,不断累积Token,跑了整整一夜。一个bug,一个晚上,烧了两万美金。
更让人崩溃的是:没有任何告警。直到账单来了,才知道出了问题。
这之后,我把AI成本监控当成了和系统监控同等重要的基础设施。
AI成本的特殊性
为什么AI成本难以监控:
1. 按量计费,非固定成本
传统服务器是月租,AI是按Token付费
一个bug可以在几小时内让成本翻10倍
2. 成本在代码执行时产生,不在账单来时
账单通常月结,问题发生后30天才知道
必须实时追踪,不能等月底
3. Token数量不直观
"调用了100次API"说不清成本
"消耗了500万Token"才是成本的真实反映
4. 多模型、多场景、多团队
不同模型单价差异极大(GPT-4 vs GPT-4o-mini差40倍)
不同业务功能的成本配比需要分别监控
不同团队的用量需要分别归因
关键监控指标:
├── 实时Token消耗速率(最重要)
├── 每功能/每用户的成本
├── 成本异常(突然飙升)
├── 每次对话的平均成本
└── 预算进度(月累计 vs 月预算)成本追踪的核心基础设施
/**
* AI成本追踪服务
*
* 每次LLM调用都记录Token消耗和成本
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AICostTrackingService {
private final CostEventRepository costRepo;
private final ModelPricingRegistry pricingRegistry;
private final MeterRegistry meterRegistry; // Micrometer指标
/**
* 记录LLM调用成本
*
* 这个方法应该在所有LLM调用的拦截器/切面中调用
*/
public void recordCost(LLMCostRecord record) {
// 计算费用
ModelPricing pricing = pricingRegistry.getPricing(
record.getModelId(), record.getProviderId());
double inputCostUSD = record.getInputTokens() * pricing.getInputPricePerToken();
double outputCostUSD = record.getOutputTokens() * pricing.getOutputPricePerToken();
double totalCostUSD = inputCostUSD + outputCostUSD;
record.setInputCostUSD(inputCostUSD);
record.setOutputCostUSD(outputCostUSD);
record.setTotalCostUSD(totalCostUSD);
// 持久化
costRepo.save(record);
// 实时推送到指标系统(用于告警和看板)
publishMetrics(record, totalCostUSD);
log.debug("成本记录: model={}, tokens={}/{}, cost=${:.6f}",
record.getModelId(),
record.getInputTokens(),
record.getOutputTokens(),
totalCostUSD);
}
/**
* 推送指标到Prometheus/Micrometer
*/
private void publishMetrics(LLMCostRecord record, double costUSD) {
// 累计成本计数器
meterRegistry.counter("ai.cost.usd",
"model", record.getModelId(),
"feature", record.getFeatureId(),
"team", record.getTeamId())
.increment(costUSD);
// Token消耗计数器
meterRegistry.counter("ai.tokens.input",
"model", record.getModelId(),
"feature", record.getFeatureId())
.increment(record.getInputTokens());
meterRegistry.counter("ai.tokens.output",
"model", record.getModelId(),
"feature", record.getFeatureId())
.increment(record.getOutputTokens());
// 每次调用成本分布(histogram,用于统计P99等)
meterRegistry.timer("ai.cost.per.call",
"model", record.getModelId())
.record(Duration.ofNanos((long)(costUSD * 1_000_000_000)));
}
/**
* 查询当前小时/当天/当月的成本
*/
public CostSummary getCurrentCostSummary(String teamId) {
Instant now = Instant.now();
return CostSummary.builder()
.lastHourCostUSD(costRepo.sumCost(teamId,
now.minus(1, ChronoUnit.HOURS), now))
.todayCostUSD(costRepo.sumCost(teamId,
LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant(), now))
.currentMonthCostUSD(costRepo.sumCost(teamId,
YearMonth.now().atDay(1).atStartOfDay(ZoneId.systemDefault()).toInstant(), now))
.build();
}
}成本异常告警
/**
* AI成本告警服务
*
* 多层次告警:速率告警 + 绝对值告警 + 异常告警
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AICostAlertService {
private final AICostTrackingService costTracking;
private final AlertNotificationService notification;
private final CostBudgetRepository budgetRepo;
/**
* 每分钟检查一次成本速率异常
*/
@Scheduled(fixedDelay = 60000)
public void checkCostRateAnomaly() {
List<String> activeTeams = budgetRepo.getActiveTeamIds();
for (String teamId : activeTeams) {
checkTeamCostRate(teamId);
}
}
private void checkTeamCostRate(String teamId) {
// 当前小时的成本
double currentHourCost = costTracking.getCurrentCostSummary(teamId)
.getLastHourCostUSD();
// 历史同期的平均成本(最近4周同一时段的平均)
double historicalAvg = costTracking.getHistoricalAvgCost(
teamId, DayOfWeek.from(LocalDate.now()), LocalTime.now().getHour());
// 异常检测:超过历史均值的5倍
if (historicalAvg > 0 && currentHourCost > historicalAvg * 5) {
double multiple = currentHourCost / historicalAvg;
log.error("成本速率异常!team={}, 当前小时=${:.2f}, 历史均值=${:.2f}, 倍数={:.1f}x",
teamId, currentHourCost, historicalAvg, multiple);
notification.sendAlert(AlertMessage.builder()
.level(AlertLevel.CRITICAL)
.title(String.format("AI成本异常告警 - %s", teamId))
.body(String.format(
"当前小时成本 $%.2f,是历史均值 $%.2f 的 %.1f 倍。\n" +
"可能存在异常调用,请立即检查!",
currentHourCost, historicalAvg, multiple))
.channels(List.of("dingding", "email", "sms")) // 严重告警用所有渠道
.build());
}
}
/**
* 每小时检查预算进度
*/
@Scheduled(cron = "0 0 * * * *")
public void checkBudgetProgress() {
List<TeamBudget> budgets = budgetRepo.getAllActiveBudgets();
for (TeamBudget budget : budgets) {
double currentSpend = costTracking.getCurrentCostSummary(
budget.getTeamId()).getCurrentMonthCostUSD();
double usageRate = currentSpend / budget.getMonthlyBudgetUSD();
// 计算预期进度(今天是几号/这个月有几天)
int dayOfMonth = LocalDate.now().getDayOfMonth();
int totalDays = LocalDate.now().lengthOfMonth();
double expectedUsageRate = (double) dayOfMonth / totalDays;
// 实际用量超出预期进度的30%
if (usageRate > expectedUsageRate + 0.3) {
AlertLevel level = usageRate > 0.9 ? AlertLevel.CRITICAL : AlertLevel.HIGH;
notification.sendAlert(AlertMessage.builder()
.level(level)
.title(String.format("AI预算进度告警 - %s", budget.getTeamId()))
.body(String.format(
"当月已用 $%.2f / 预算 $%.2f(%.1f%%)\n" +
"今天是%d号,预期进度应在 %.1f%%\n" +
"预计月末超出预算 $%.2f",
currentSpend, budget.getMonthlyBudgetUSD(), usageRate * 100,
dayOfMonth, expectedUsageRate * 100,
(currentSpend / usageRate * 1.0 - budget.getMonthlyBudgetUSD())))
.channels(level == AlertLevel.CRITICAL ?
List.of("dingding", "email") :
List.of("dingding"))
.build());
}
}
}
/**
* 自动熔断:当成本超过阈值时自动降级到便宜模型
*/
@Scheduled(fixedDelay = 5000) // 每5秒检查
public void autoCostCircuitBreaker() {
List<TeamBudget> budgets = budgetRepo.getAllActiveBudgets();
for (TeamBudget budget : budgets) {
double currentSpend = costTracking.getCurrentCostSummary(
budget.getTeamId()).getCurrentMonthCostUSD();
double usageRate = currentSpend / budget.getMonthlyBudgetUSD();
if (usageRate >= 0.95 && !budget.isDegradedMode()) {
// 成本接近预算上限,自动降级到便宜模型
log.warn("自动成本熔断:team={}, 已用={:.1f}%,强制切换到低成本模式",
budget.getTeamId(), usageRate * 100);
budget.setDegradedMode(true);
budgetRepo.save(budget);
notification.sendAlert(AlertMessage.builder()
.level(AlertLevel.HIGH)
.title("AI成本熔断触发")
.body(String.format(
"团队 %s 的AI成本已达预算 %.1f%%,已自动切换到低成本模式",
budget.getTeamId(), usageRate * 100))
.build());
}
if (usageRate < 0.85 && budget.isDegradedMode()) {
// 成本降下来了,解除降级
budget.setDegradedMode(false);
budgetRepo.save(budget);
log.info("成本熔断解除:team={}", budget.getTeamId());
}
}
}
}成本归因:谁用了多少
/**
* 成本归因分析
*
* 按功能、团队、用户维度拆解成本
*/
@Service
@RequiredArgsConstructor
public class CostAttributionService {
private final CostEventRepository costRepo;
/**
* 按功能模块的成本分解
*/
public Map<String, FeatureCostBreakdown> getFeatureCostBreakdown(
LocalDate from, LocalDate to) {
return costRepo.findByDateRange(from, to)
.stream()
.collect(Collectors.groupingBy(
CostEvent::getFeatureId,
Collectors.collectingAndThen(
Collectors.toList(),
events -> {
double totalCost = events.stream()
.mapToDouble(CostEvent::getTotalCostUSD).sum();
long totalCalls = events.size();
double avgCostPerCall = totalCost / totalCalls;
long totalTokens = events.stream()
.mapToLong(e -> e.getInputTokens() + e.getOutputTokens()).sum();
return new FeatureCostBreakdown(
totalCost, totalCalls, avgCostPerCall, totalTokens);
})));
}
/**
* 找出最贵的用户请求(用于发现异常使用)
*/
public List<CostEvent> findTopExpensiveRequests(LocalDate date, int topN) {
return costRepo.findByDate(date)
.stream()
.sorted(Comparator.comparingDouble(CostEvent::getTotalCostUSD).reversed())
.limit(topN)
.collect(Collectors.toList());
}
}实用的Grafana看板配置
关键的成本监控指标,用PromQL查询:
# 每分钟成本速率
rate(ai_cost_usd_total[5m]) * 60
# 今日累计成本(按功能分组)
sum by (feature) (
increase(ai_cost_usd_total[1d])
)
# 当前小时成本 vs 历史均值(异常检测)
(
increase(ai_cost_usd_total[1h])
) / (
avg_over_time(
increase(ai_cost_usd_total[1h])[4w:1h]
)
)
# Token效率(平均每次对话消耗Token数,越低越好)
(
increase(ai_tokens_input_total[5m]) +
increase(ai_tokens_output_total[5m])
) / increase(ai_calls_total[5m])核心洞察:成本监控是AI系统的安全气囊
开头那次$23,847的教训之后,我意识到:对AI系统来说,成本监控和服务可用性监控同等重要。
两者的失控后果:
- 服务不可用:用户体验受损,可能影响收入
- 成本失控:直接烧钱,可能威胁项目存活
几个关键建议:
设置多层预警,不要只有一条警戒线。50%(提前关注)、80%(积极应对)、95%(触发自动降级)三层,每层采取不同行动。
实时监控,不等账单。月底账单是事后结果,发现问题已经损失发生了。实时监控才是防御手段。
自动降级是最重要的安全网。告警之后还需要人来处理,但半夜三点没人盯监控。自动成本熔断能在无人值守的情况下阻止最坏的情况发生。
每个LLM调用都要有业务标签。如果你不知道钱花在哪个功能上,就不知道应该优化哪里。从第一天开始就给每次调用打上功能标签、团队标签。
