用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
) {
}
tenantId和userId从认证上下文读取,不信任前端直接传值。
五、从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/
智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。