用Spring Boot搭建AI成本与稳定性监控看板:Token、预算、429、熔断与降级

文章摘要

Spring AI 2.0已经可以通过Micrometer输出模型调用耗时与Token指标,但企业要真正控制成本,还需要把模型价格、业务场景、租户、预算、重试、Tool Calling和降级事件统一起来。本文实现一个轻量级AI成本与稳定性监控服务:从ChatResponse读取Usage,计算单次费用,写入业务成本表,同时用Micrometer记录低基数指标,并提供租户月预算、场景成本、429、熔断和降级看板所需的数据模型与PromQL。

一、最终要看哪些数据

系统健康看板:

QPS
P95/P99延迟
当前并发
错误率
429
超时
熔断
降级

成本看板:

输入Token
输出Token
缓存读写Token
按模型成本
按租户成本
按业务场景成本
单任务平均成本
预算使用率

质量辅助:

完成率
重试次数
工具调用次数
人工接管率

二、为什么Prometheus不能单独做财务结算

Prometheus适合趋势和告警,但不适合最终账单:

  • 指标可能有采样;
  • 数据有保留期限;
  • 实例重启和标签变化会影响序列;
  • 不适合保存每个用户高基数记录;
  • 难以做审计和追溯。

推荐:

Micrometer
→ 实时监控

成本明细表
→ 结算与审计

三、价格配置模型

public record ModelPrice(
        String provider,
        String model,
        BigDecimal inputPerMillion,
        BigDecimal outputPerMillion,
        BigDecimal cacheWritePerMillion,
        BigDecimal cacheReadPerMillion,
        LocalDate effectiveFrom
) {
}

价格不能写死在业务代码中。

配置表:

CREATE TABLE ai_model_price (
    id BIGINT PRIMARY KEY,
    provider VARCHAR(64) NOT NULL,
    model VARCHAR(128) NOT NULL,
    input_per_million DECIMAL(18,8) NOT NULL,
    output_per_million DECIMAL(18,8) NOT NULL,
    cache_write_per_million DECIMAL(18,8),
    cache_read_per_million DECIMAL(18,8),
    effective_from DATE NOT NULL,
    effective_to DATE
);

模型价格变化后保留历史版本。

四、请求上下文

public record AiCostContext(
        String requestId,
        String tenantId,
        String userId,
        String businessScene,
        String promptVersion,
        String modelTier
) {
}

tenantIduserId从认证上下文读取,不信任前端直接传值。

五、从ChatResponse读取Usage

ChatResponse response = chatClient.prompt()
        .user(message)
        .call()
        .chatResponse();

Usage usage = response
        .getMetadata()
        .getUsage();

读取:

long input = usage.getPromptTokens();
long output = usage.getCompletionTokens();
long total = usage.getTotalTokens();

不同Provider的详细缓存Token可以通过统一Usage字段或getNativeUsage()读取。

必须允许:

Usage为空

这时标记为:

COST_UNKNOWN

不要默认为0,否则财务数据会被低估。

六、成本计算器

@Component
public class AiCostCalculator {

    private static final BigDecimal MILLION =
            new BigDecimal("1000000");

    public BigDecimal calculate(
            UsageSnapshot usage,
            ModelPrice price
    ) {
        BigDecimal inputCost = cost(
                usage.inputTokens(),
                price.inputPerMillion()
        );

        BigDecimal outputCost = cost(
                usage.outputTokens(),
                price.outputPerMillion()
        );

        BigDecimal cacheWriteCost = cost(
                usage.cacheWriteTokens(),
                price.cacheWritePerMillion()
        );

        BigDecimal cacheReadCost = cost(
                usage.cacheReadTokens(),
                price.cacheReadPerMillion()
        );

        return inputCost
                .add(outputCost)
                .add(cacheWriteCost)
                .add(cacheReadCost);
    }

    private BigDecimal cost(
            long tokens,
            BigDecimal perMillion
    ) {
        if (perMillion == null) {
            return BigDecimal.ZERO;
        }

        return BigDecimal.valueOf(tokens)
                .multiply(perMillion)
                .divide(
                        MILLION,
                        10,
                        RoundingMode.HALF_UP
                );
    }
}

七、成本明细表

CREATE TABLE ai_usage_record (
    id BIGINT PRIMARY KEY,
    request_id VARCHAR(64) NOT NULL,
    tenant_id VARCHAR(64) NOT NULL,
    user_id VARCHAR(64),
    business_scene VARCHAR(64) NOT NULL,
    provider VARCHAR(64) NOT NULL,
    model VARCHAR(128) NOT NULL,
    prompt_version VARCHAR(64),
    input_tokens BIGINT,
    output_tokens BIGINT,
    cache_write_tokens BIGINT,
    cache_read_tokens BIGINT,
    model_call_count INT NOT NULL,
    tool_call_count INT NOT NULL,
    retry_count INT NOT NULL,
    estimated_cost DECIMAL(18,10),
    cost_status VARCHAR(32) NOT NULL,
    duration_ms BIGINT,
    result_status VARCHAR(32) NOT NULL,
    created_at TIMESTAMP NOT NULL
);

索引:

CREATE INDEX idx_ai_usage_tenant_month
ON ai_usage_record(tenant_id, created_at);

CREATE INDEX idx_ai_usage_scene_month
ON ai_usage_record(business_scene, created_at);

八、为什么要记录model_call_count

一次ChatClient请求可能包含:

第一次模型选择工具
第二次模型读取工具结果
第三次模型修复结构化输出

最终Usage可能是累计值,但为了分析效率,还要记录模型轮次。

指标:

平均每个业务请求模型调用次数

如果从1.3突然上升到4.8,可能发生:

  • Tool循环;
  • 输出修复;
  • 重试;
  • Agent规划失效。

九、Micrometer低基数指标

@Component
public class AiMetrics {

    private final MeterRegistry registry;

    public AiMetrics(MeterRegistry registry) {
        this.registry = registry;
    }

    public void recordCost(
            String scene,
            String modelTier,
            BigDecimal cost
    ) {
        registry.counter(
                "enterprise.ai.estimated.cost",
                "scene", scene,
                "model_tier", modelTier
        ).increment(cost.doubleValue());
    }

    public void recordFallback(
            String scene,
            String reason
    ) {
        registry.counter(
                "enterprise.ai.fallback",
                "scene", scene,
                "reason", reason
        ).increment();
    }
}

不要把:

userId
requestId
conversationId

作为Meter标签。

十、预算表

CREATE TABLE ai_budget (
    id BIGINT PRIMARY KEY,
    scope_type VARCHAR(32) NOT NULL,
    scope_id VARCHAR(128) NOT NULL,
    budget_month CHAR(7) NOT NULL,
    warning_amount DECIMAL(18,2) NOT NULL,
    hard_limit_amount DECIMAL(18,2) NOT NULL,
    currency VARCHAR(8) NOT NULL,
    UNIQUE(scope_type, scope_id, budget_month)
);

范围:

ORGANIZATION
PROJECT
TENANT
BUSINESS_SCENE

十一、预算检查

public BudgetDecision check(
        String tenantId,
        String scene,
        BigDecimal estimatedRequestCost
) {
    BigDecimal monthCost =
            usageRepository.sumMonthCost(
                    tenantId,
                    YearMonth.now()
            );

    Budget budget = budgetRepository
            .findTenantBudget(
                    tenantId,
                    YearMonth.now()
            );

    BigDecimal projected =
            monthCost.add(estimatedRequestCost);

    if (projected.compareTo(
            budget.hardLimitAmount()
    ) >= 0) {
        return BudgetDecision.BLOCK;
    }

    if (projected.compareTo(
            budget.warningAmount()
    ) >= 0) {
        return BudgetDecision.WARN;
    }

    return BudgetDecision.ALLOW;
}

请求前只能估算,调用后再用实际Usage结算。

十二、预算接近上限如何降级

80%
→ 告警

90%
→ 默认切换低成本模型

95%
→ 关闭非核心AI功能

100%
→ 阻止请求或转规则系统

模型路由:

public ModelTier route(
        BudgetDecision decision,
        ModelTier requested
) {
    return switch (decision) {
        case ALLOW -> requested;
        case WARN -> requested.downgrade();
        case BLOCK -> ModelTier.NONE;
    };
}

十三、429看板

区分:

rate_limit_exceeded
insufficient_quota
project_spend_limit

指标:

enterprise_ai_429_total{
  reason="rate_limit"
}

不要把所有429合并,否则无法判断应该:

等待重试
还是
立即停止并调整预算

十四、熔断与降级指标

resilience4j_circuitbreaker_state
resilience4j_circuitbreaker_calls_seconds
enterprise_ai_fallback_total
enterprise_ai_degraded_request_total

看板应显示:

  • 当前熔断状态;
  • 最近打开次数;
  • 降级模型比例;
  • 转人工数量;
  • 被预算阻止数量。

十五、核心PromQL

输入Token速率

sum by (gen_ai_request_model) (
  rate(gen_ai_client_token_usage_total{
    gen_ai_token_type="input"
  }[5m])
)

输出Token速率

sum by (gen_ai_request_model) (
  rate(gen_ai_client_token_usage_total{
    gen_ai_token_type="output"
  }[5m])
)

模型平均耗时

sum(rate(gen_ai_client_operation_seconds_sum[5m]))
/
sum(rate(gen_ai_client_operation_seconds_count[5m]))

业务降级速率

sum by (scene, reason) (
  rate(enterprise_ai_fallback_total[5m])
)

十六、日报接口

@GetMapping("/internal/ai-cost/daily")
public DailyCostReport daily(
        @RequestParam LocalDate date
) {
    return reportService.build(date);
}

返回:

{
  "date": "2026-07-31",
  "totalCost": 128.43,
  "totalRequests": 45210,
  "costPerSuccess": 0.0031,
  "topScenes": [],
  "topTenants": [],
  "fallbackCount": 318,
  "rateLimitCount": 26
}

十七、看板布局建议

第一行:

今日费用
月度费用
预算使用率
成功请求
单次成功成本

第二行:

请求趋势
Token趋势
P95延迟
错误率

第三行:

模型成本占比
业务场景成本
租户成本Top10

第四行:

429
熔断
降级
重试
Tool失败

十八、需要防止的统计错误

1. Tool Calling只算最后一次模型

会低估成本。

2. Provider未返回Usage时记0

应该记未知。

3. 价格更新后重算历史

历史记录应使用调用当时价格版本。

4. Prometheus值作为账单

不适合最终审计。

5. 把userId作为指标标签

造成高基数爆炸。

总结

一个可用的AI成本看板必须把:

Spring AI Usage
+模型价格
+业务上下文
+预算
+429
+熔断
+降级

组织在一起。

Micrometer负责实时健康和趋势,数据库明细负责按租户、场景和请求进行成本结算与审计。

延伸阅读

如果你正在关注企业级 AI 应用、Spring AI、RAG、Agent 与 MCP 工程化落地,欢迎访问 智元界

https://www.zyentor.com/

智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。