Appearance
成本治理与降级:把账单管在预算内
1. 本节产出
一套成本治理:按租户/用户的 Token 配额、实时成本统计、预算告警、超预算自动降级。并且能回答「一次问答到底多少钱」这个老板必问的问题。
2. 前置依赖
3. 为什么成本治理必须在上线前做
一个真实场景:系统上线两周,财务拿着云服务账单来问「这个月 AI 花了 4 万,谁在用?」。此时你:
| 问题 | 能不能回答 |
|---|---|
| 哪个租户花得最多? | 答不上(没按租户统计) |
| 哪类请求最贵? | 答不上(没分类统计) |
| 有没有异常调用? | 答不上(没基线) |
| 明天会不会超预算? | 答不上(没配额) |
四个都答不上,这就是「成本事故」。而且它比技术故障更难处理——技术故障会报警,成本超标是月底才知道。
核心原则:成本必须可归因、可预测、可限制。 三件事缺一件,迟早出事。
4. 核心原理
4.1 一次 RAG 问答的成本构成
总成本 = 查询向量化 + 检索(可忽略) + 精排 + 生成 + 查询改写(可选)
典型数值(单次,中档模型):
查询向量化 0.00002 元 (可忽略)
Rerank 0.003 元 (20 个候选)
生成 0.005 元 (输入 2000 + 输出 400 Token)
─────────────────────────
合计 约 0.008 元生成是大头,精排次之。所以降本的第一优先级是:
- 缓存(02-15):直接省掉全流程,命中率 40% 就是省 40%
- 压缩上下文:少塞几个片段、片段切小一点
- 模型分级:简单问题用便宜模型
- 精排按需:只对低置信度查询做 Rerank
4.2 三级配额模型
| 级别 | 限制对象 | 作用 |
|---|---|---|
| 全局 | 整个系统每日/每月上限 | 兜底,防止失控 |
| 租户 | 每个租户的月度预算 | 商用必需,也是计费依据 |
| 用户 | 单个用户的日请求数 | 防滥用、防脚本刷 |
配额超限后的行为(按严重程度递进):
80% → 告警通知
95% → 自动降级(切便宜模型 / 关闭 Rerank / 减少 topK)
100% → 限流拒绝,返回「本月额度已用完」95% 的自动降级是关键设计:它让系统在预算耗尽前先「变省」,而不是突然不可用。
4.3 统计口径
必须能按以下维度出报表:
├─ 按租户
├─ 按用户
├─ 按功能(问答/灌库/改写/精排)
├─ 按模型
└─ 按时间(日/月)灌库成本要单独统计。它是一次性大额支出,混在问答成本里会让日常曲线看不懂。
4.4 降级策略的设计
| 降级档 | 措施 | 成本降幅 | 质量损失 |
|---|---|---|---|
| 轻微 | 关闭查询改写 | ~5% | 小 |
| 中等 | 关闭 Rerank,topK 5→3 | ~35% | 中 |
| 重度 | 切轻量模型 | ~70% | 明显 |
| 极限 | 只返回检索结果,不生成 | ~85% | 大(但仍有信息) |
最后一档很实用:不生成答案,直接列出相关文档片段。用户仍然能得到信息,系统成本降到接近零。这比「额度用完请下月再来」好得多。
5. 代码走查
5.1 用量采集
java
// ch02-rag/src/main/java/com/aitech/rag/metrics/TokenMeter.java
@Component
public class TokenMeter {
private final MeterRegistry meters;
private final UsageRepository repo;
public void record(UsageEvent e) {
// 实时指标(用于告警面板)
meters.counter("ai.tokens",
"tenant", e.tenantId(),
"type", e.type(), // QUERY / INGEST / RERANK / REWRITE
"model", e.model()).increment(e.totalTokens());
// 明细落库(用于对账与报表)
repo.save(e);
}
public record UsageEvent(String tenantId, String userId, String type,
String model, int promptTokens, int completionTokens,
Instant at) {
public int totalTokens() { return promptTokens + completionTokens; }
}
}两条路都要:实时指标用于告警,明细落库用于对账。只做前者,月底对不上账;只做后者,无法实时告警。
5.2 配额检查
java
// ch02-rag/src/main/java/com/aitech/rag/quota/QuotaManager.java
@Service
public class QuotaManager {
/** 返回当前应采用的运行档位 */
public Tier check(String tenantId) {
long used = repo.monthlyTokens(tenantId);
long limit = props.limitFor(tenantId);
double ratio = used / (double) limit;
if (ratio >= 1.0) {
alert(tenantId, "额度已用尽");
return Tier.BLOCKED;
}
if (ratio >= 0.95) {
alert(tenantId, "额度即将用尽,已自动降级");
return Tier.DEGRADED_HEAVY;
}
if (ratio >= 0.80) {
alert(tenantId, "额度使用 80%");
return Tier.DEGRADED_LIGHT;
}
return Tier.NORMAL;
}
public enum Tier { NORMAL, DEGRADED_LIGHT, DEGRADED_HEAVY, BLOCKED }
}5.3 按档位调整链路
java
// ch02-rag/src/main/java/com/aitech/rag/chain/AdaptiveRagChain.java
@Service
public class AdaptiveRagChain {
public Answer answer(String question, String tenantId, List<String> acl) {
QuotaManager.Tier tier = quota.check(tenantId);
if (tier == QuotaManager.Tier.BLOCKED) {
return Answer.quotaExceeded();
}
RagSettings s = switch (tier) {
case NORMAL -> RagSettings.full(); // 改写 + 混合 + 精排
case DEGRADED_LIGHT -> RagSettings.noRewrite(); // 关改写
case DEGRADED_HEAVY -> RagSettings.economy(); // 关改写+精排,topK=3,轻量模型
case BLOCKED -> throw new IllegalStateException();
};
return ragChain.answerWith(question, tenantId, acl, s);
}
}5.4 成本报表
java
// ch02-rag/src/main/java/com/aitech/rag/metrics/CostReporter.java
@Service
public class CostReporter {
/** 月度成本报表:按租户 × 类型 */
public List<CostRow> monthly(YearMonth month) {
return jdbc.query("""
SELECT tenant_id, type, model,
SUM(prompt_tokens) AS pt,
SUM(completion_tokens) AS ct
FROM ai_usage
WHERE at >= ? AND at < ?
GROUP BY tenant_id, type, model
ORDER BY pt + ct DESC
""", rowMapper, month.atDay(1), month.plusMonths(1).atDay(1));
}
}6. 跑起来
bash
git checkout ch02-18-cost
mvn spring-boot:runbash
# 1. 单次成本查询
curl "http://localhost:8080/api/ask" -d '{"q":"年假怎么算","tenantId":"acme"}' -v
# 期望响应头:X-Cost-Tokens: 2431 X-Cost-Yuan: 0.0081
# 2. 配额状态
curl http://localhost:8080/api/quota/acme
# 期望:{"used":1245000,"limit":5000000,"ratio":0.249,"tier":"NORMAL"}
# 3. 模拟超配额(把测试租户额度调到很低)
curl -X POST http://localhost:8080/api/admin/quota \
-d '{"tenantId":"demo","limit":10000}'
curl "http://localhost:8080/api/ask" -d '{"q":"年假怎么算","tenantId":"demo"}'
# 期望:降级档生效,日志显示 DEGRADED_HEAVY,仍返回答案
# 4. 成本报表
curl "http://localhost:8080/api/cost/monthly?month=2026-10"| 检查项 | 通过标准 |
|---|---|
| 单次成本可见 | 响应头或日志带 Token 与估算金额 |
| 配额可查 | 能看到已用/上限/档位 |
| 降级可用 | 超配额后仍返回结果(质量下降但不报错) |
| 报表可用 | 能按租户和类型出数据 |
| 告警 | 80%/95% 触发通知 |
7. 生产避坑
- 不要用估算值计费,只用厂商返回的 usage。估算用于预算告警和前端提示,对账必须用真实返回值。两者混用会导致月底和账单对不上,而排查这类问题极其耗时。
- 灌库成本要单独统计并单独设额度。一次全量重灌可能花掉几千元,如果和问答共用额度,会在某天突然触发全局限流,影响所有租户的正常使用。
- 降级必须是「变省」而不是「不可用」。直接拒绝服务会让用户在月底集体无法使用,体验极差。正确设计是逐级降级,最后一档「只返回检索片段」仍然有价值。
8. 延伸与锚点
- 思考题:线上发现某个租户的查询 P95 突然变高,你怎么定位是哪一环慢?(答案在下一课时)
- 代码锚点:
git checkout ch02-18-cost - 下一课时:02-19 可观测与链路追踪
- 对应课件:L02-18 成本治理