Spring AI 的 Micrometer Observation 能零代码暴露 token 与成本指标,但在多供应商、多模型、多租户的真实生产环境里,指标"有"和"能归因到具体功能与用户"之间,隔着一条完整的工程鸿沟。本文试图把这条鸿沟拆成可施工的桥墩。
00 / ABSTRACT摘要
大模型应用的账单失控,往往不是因为单价高,而是因为成本没有被记录到正确的维度上。Spring AI 通过 Micrometer Observation 在 ChatClient 调用链路上自动埋点,理论上可以零代码拿到 token 用量与调用耗时;但 token 只是成本的原料,真正的成本归因需要回答三个问题:这笔钱花在哪个功能上、由哪个租户承担、对应哪一次会话。本文以 Observation 的上下文传播机制为主线,构建"采集—标签—聚合—核算—告警"五段式成本归因模型,讨论多供应商价格差异、缓存命中、流式响应、异步与批处理等边界条件,并给出可复用的代码骨架与验证方法。笔者认为,成本可观测性的本质不是指标采集问题,而是维度建模问题——指标是免费的,维度是昂贵的。
00 / TOC目录
01 / PROBLEM问题的真实形状:账单失控的四种典型模式
在讨论技术方案之前,先把"账单失控"这个模糊感受拆成可观测的现象。根据业界公开的成本治理实践(FinOps Foundation 的云成本管理框架、多家云厂商的 LLM 计费文档),失控通常表现为四类模式,它们的根因各不相同,对应的技术手段也不同。
这四类模式对应成本可观测性的四个成熟度层级:看得见总量 → 看得见结构 → 看得见归属 → 看得见异常。Spring AI 的 Observation 默认解决的是第一层,部分解决第二层,而第三、第四层需要额外的维度建模。这是本文的核心判断,也是后续所有章节的组织依据。
本文评述:很多团队把"接入 Micrometer"当作成本治理的终点,实际上它只是起点。指标系统的价值取决于标签基数与语义一致性,而不是埋点数量。一个没有租户标签的 token 计数器,在财务视角下几乎等于零信息。
02 / CONCEPTS背景与概念界定:Observation、Token 与成本的三层语义
2.1 Micrometer Observation 是什么
Micrometer Observation API 是 Micrometer 1.10 引入的统一观测抽象,其设计目标是用一套 API 同时产出 Metrics(指标)、Tracing(链路追踪)与 Logging(日志)三类信号。一个 Observation 表示一次被观测的操作,具有名称、上下文(Context)与一组 Key-Value 低基数标签;ObservationRegistry 负责管理 Handler 链,Observation.Context 在操作生命周期内传递状态。官方文档将其定位为"可观测性的单一入口",避免为指标和追踪分别埋点导致的语义漂移。
关键机制是上下文传播:Observation 打开时创建上下文,操作过程中可以写入高低基数标签,关闭时由各 Handler 统一消费。这意味着一次 LLM 调用从进入到返回,中间产生的 token 数、模型名、供应商、耗时都可以挂到同一个上下文上,最终由不同 Handler 分别输出为指标、span 或日志。
2.2 Token 的三种口径
成本计算的第一道陷阱是 token 口径不一致。业界普遍区分三类:
- 输入 token(prompt tokens):发送给模型的全部内容,包括系统提示、历史消息、检索增强注入的文档片段、工具定义。
- 输出 token(completion tokens):模型生成的内容,流式场景下需要累计到流结束才能确定。
- 计费 token(billed tokens):供应商实际计费的 token,可能包含缓存命中折扣、推理 token(reasoning tokens)、多模态折算等差异。
三者并不总是相等。以 OpenAI 的定价文档为例,缓存命中的输入 token 价格显著低于未命中部分;Anthropic 的 prompt caching 同样对缓存读取与缓存写入采用不同费率。这意味着仅记录总 token 数不足以还原真实成本,必须记录计费口径下的细分字段。
2.3 成本的三层语义
笔者建议把"成本"拆成三层,避免在工程实现中混为一谈:
三者之间存在系统性偏差:账单可能有最低消费、阶梯折扣、赠送额度;用量成本可能因价格表更新滞后而失真。工程上必须明确"以哪一层为准",否则会出现监控显示 800 元、账单 1200 元的信任危机。笔者认为,监控层以用量成本为准,财务层以账单成本为准,两者定期对账,是较为稳妥的分工。
03 / MECHANISMSpring AI 的埋点机制:零代码能拿到什么、拿不到什么
3.1 自动埋点的位置
Spring AI 在 ChatClient 与底层 ChatModel 的调用路径上集成了 Observation。当应用引入 Micrometer 与相应 registry(如 Prometheus)后,一次 chat 调用会生成名为 spring.ai.chat.client 或模型层对应的 Observation,其上下文在调用结束时被 Handler 消费。Spring AI 文档说明,这些观测包含模型、供应商等低基数标签,以及 token 用量等关键信息(具体字段随版本演进,需以所用版本文档为准)。
零代码的含义是:只要 classpath 上有 Micrometer 与 registry,且配置了 ObservationRegistry,指标就会自动出现在 /actuator/metrics 与 Prometheus 端点。这对"看得见总量"已经足够。
3.2 零代码拿不到的四类信息
但默认埋点存在明确的信息缺口,这些缺口正是归因失败的技术根因:
- 业务维度:默认标签只有技术维度(模型、供应商、操作名),没有"哪个功能模块""哪个 API 端点""哪次用户请求"。
- 租户身份:多租户场景下,租户 ID 通常存在于请求头或安全上下文,默认不会进入 Observation。
- 计费细分:缓存命中 token、推理 token 等细分字段是否暴露,取决于框架版本与供应商实现,不能假设一定存在。
- 会话关联:一次用户会话可能触发多次模型调用(多轮对话、工具调用循环、RAG 多次检索),默认指标是调用粒度,不是会话粒度。
笔者认为:Observation 提供的是"可扩展的观测骨架",而不是"开箱即用的成本账本"。把它当作骨架,在其上挂业务维度,是成本最低、侵入最小的路径;指望框架替你做业务归因,是把领域建模责任错误地外包给了基础设施。
3.3 高低基数标签的分野
Micrometer 对标签基数(cardinality)高度敏感。租户 ID、用户 ID、会话 ID 属于高基数标签,直接写入指标会导致时间序列爆炸,Prometheus 侧表现为内存与查询压力剧增。业界通行做法是:低基数维度进指标,高基数维度进追踪与日志。租户数量在数百以内可视为中低基数,可进指标;用户与会话 ID 应进 span 属性或结构化日志,通过 traceId 关联。
这一分野直接决定了归因架构:指标负责"聚合视图",追踪负责"明细追溯",两者通过 traceId 与租户标签对齐。任何试图把用户 ID 塞进 Prometheus 标签的方案,在租户规模上千后都会遇到工程瓶颈。
04 / MODEL归因主线:以 Observation 上下文为骨架的三级标签模型
本节提出全文的核心模型。笔者认为,成本归因可以抽象为"在正确的上下文里,用正确的维度,记录正确的量"。三个"正确"分别对应标签模型、传播机制与计量口径。
4.1 三级标签:租户、功能、会话
把归因维度分为三级,按基数从低到高排列:
L1 与 L2 进指标,支撑"按租户看成本""按功能看成本"的聚合查询;L3 进追踪与日志,支撑"某次异常调用为什么贵"的明细追溯。三级之间通过 traceId 与租户标签对齐,形成"聚合—下钻"的完整链路。
4.2 传播机制:上下文如何进入 Observation
三级标签的注入点不同,需要分别设计:
- L1 租户:来自请求头(如
X-Tenant-Id)或安全上下文(Spring Security 的Authentication)。在 Web 层通过 Filter 写入 MDC 与 Observation 上下文,或使用Observation.Context的父上下文传递。 - L2 功能:来自业务代码的显式声明,例如在调用 ChatClient 前通过装饰器或 AOP 传入 feature 名。这是唯一需要业务代码配合的维度,也是归因准确性的关键。
- L3 会话:来自会话管理组件,写入 span 属性与日志,不进入指标标签。
Spring AI 的 ChatClient 支持通过 Advisor 机制在调用前后插入逻辑,这为标签注入提供了天然切面。Advisor 可以在请求前读取上下文、写入 Observation 标签,在响应后累计 token 并记录明细。
4.3 计量口径:一次调用记什么
建议每次模型调用记录以下字段(括号内为去向):
model (tag) // 模型标识 provider (tag) // 供应商 feature (tag) // L2 功能 tenant_id (tag) // L1 租户 prompt_tokens (metric) // 输入 token completion_tokens (metric) cached_tokens (metric) // 缓存命中,若供应商支持 reasoning_tokens (metric) latency_ms (metric) session_id (span attr) user_id (span attr) trace_id (span attr)
注意 cached_tokens 与 reasoning_tokens 并非所有供应商都提供,需要在适配层做能力探测与降级:缺失时记为 0 并在标签中标记 billing_detail=unavailable,避免把"未知"误算为"零成本"。
05 / ENGINEERING工程实现:从 ChatClient 装饰器到成本聚合管道
5.1 第一步:定义成本上下文
先定义一个不可变的成本上下文对象,承载三级标签与计量字段。它既是 Observation 的输入,也是日志与消息队列的输出格式。
public record CostContext(
String tenantId,
String feature,
String sessionId,
String userId,
String provider,
String model
) {
public static CostContext fromRequest(HttpServletRequest req) {
return new CostContext(
req.getHeader("X-Tenant-Id"),
req.getHeader("X-Feature"),
req.getHeader("X-Session-Id"),
req.getHeader("X-User-Id"),
null, null
);
}
}
把租户与功能放在请求头,是最低侵入的做法:网关统一注入,业务代码无需感知。但功能维度若完全依赖请求头,容易被前端伪造或遗漏,因此建议在服务端做白名单校验,未识别的 feature 归入 unknown 并单独告警——未知功能的成本恰恰是最需要关注的部分。
5.2 第二步:用 Advisor 注入标签
Spring AI 的 Advisor 机制允许在 ChatClient 调用链中插入横切逻辑。下面是一个概念性骨架(API 细节以所用版本为准):
public class CostAttributionAdvisor implements CallAroundAdvisor {
private final ObservationRegistry registry;
private final CostContextHolder holder; // ThreadLocal / Reactor Context
@Override
public AdvisedResponse aroundCall(AdvisedRequest request, CallAroundAdvisorChain chain) {
CostContext ctx = holder.current();
Observation obs = Observation.createNotStarted("llm.call", registry)
.lowCardinalityKeyValue("tenant_id", ctx.tenantId())
.lowCardinalityKeyValue("feature", ctx.feature())
.lowCardinalityKeyValue("model", ctx.model())
.highCardinalityKeyValue("session_id", ctx.sessionId())
.start();
try (Observation.Scope scope = obs.openScope()) {
AdvisedResponse resp = chain.nextAroundCall(request);
Usage usage = resp.response().getMetadata().getUsage();
obs.lowCardinalityKeyValue("prompt_tokens", String.valueOf(usage.getPromptTokens()));
obs.lowCardinalityKeyValue("completion_tokens", String.valueOf(usage.getCompletionTokens()));
return resp;
} catch (RuntimeException e) {
obs.error(e);
throw e;
} finally {
obs.stop();
}
}
}
这里有一个容易踩的坑:token 数在调用返回前是未知的,而 Micrometer 的 lowCardinalityKeyValue 在 Observation 启动后仍可追加,但部分 registry 的指标在 stop 时才最终确定标签集。工程上应确保在 stop() 之前写入全部标签,否则可能出现标签缺失或序列分裂。更稳妥的做法是把 token 数作为独立的 Counter/Gauge 记录,而不是塞进标签——标签是维度,不是数值。
本文评述:把 token 数写成标签是一个常见反模式。标签的语义是"分类维度",数值的语义是"度量",混用会导致时间序列数量随 token 值变化而膨胀。正确做法是 Counter 按维度累加,或 Summary/Histogram 记录分布。
5.3 第三步:流式响应的累计
流式(SSE)场景下,token 用量通常在流的最后一个 chunk 或 usage 事件中给出。Advisor 的 aroundCall 在流式模式下返回的是 Flux,需要在流终止时回调记录。推荐用 Reactor 的 doOnComplete / doOnError 钩子,并注意以下边界:
- 客户端提前断开:流未完成,供应商可能仍按已生成 token 计费,需在取消信号中记录部分用量。
- usage 事件缺失:部分供应商的流式响应不返回 usage,需在适配层用 tokenizer 估算并标记
estimated=true。 - 背压与缓冲:流式累计状态应放在请求作用域,避免并发请求串扰。
5.4 第四步:成本聚合管道
指标进入 Prometheus 后,成本计算有两种路径:查询时计算与写入时计算。
笔者认为,生产环境宜采用双写:应用侧写 token 原始指标(口径稳定),聚合侧写成本指标(口径可演进),两者通过版本号关联。价格表变更时,用原始指标重算成本,保证可审计。
5.5 第五步:明细落库与对账
指标适合聚合,不适合审计。每次调用的明细(traceId、租户、功能、模型、token、时间戳)应异步写入分析型存储(如 ClickHouse、Doris 或云数仓),支撑:
- 按会话下钻:某用户投诉"为什么这次回答这么贵"。
- 与供应商账单对账:按天、按模型比对用量成本与账单成本,偏差超过阈值告警。
- 异常检测:单次调用 token 超阈值、单租户日环比突增。
明细写入应采用批量 + 背压策略,避免观测链路拖慢主链路。业界常见做法是通过 OpenTelemetry Collector 或消息队列解耦,应用只负责产生事件。
06 / MULTI-PROVIDER多供应商与多模型:价格矩阵与路由成本
6.1 价格矩阵的建模
多供应商意味着价格不是单一数值,而是一个多维矩阵:供应商 × 模型 × 输入/输出 × 缓存状态 × 批次类型。以公开定价文档为例(价格随供应商调整,以下为示意结构,具体数值请以官方文档为准):
价格矩阵应作为配置而非硬编码,并带版本号与生效时间。工程上建议维护一张 price_book 表,字段包括 provider、model、unit_type、unit_price、currency、effective_from、version。成本计算时按调用时间匹配生效版本,保证历史可重算。
6.2 路由成本:被忽视的隐性开销
多模型路由(如按复杂度选择模型、按成本选择供应商)本身会引入额外成本:路由决策可能调用一次轻量模型做分类,失败重试会重复计费,降级切换可能丢失上下文导致重发。这些开销在指标上表现为"额外的调用",若不标注 call_purpose=router 或 retry,就会被错误地归因到业务功能上。
笔者建议在标签模型中增加一个正交维度 call_purpose,取值如 business、router、retry、eval。这样既能看业务成本,也能看"为了省钱而花的钱"是否划算——如果路由开销超过节省的成本,路由策略本身就需要重新评估。
07 / MULTI-TENANT多租户:隔离、配额与"谁买单"的边界条件
7.1 三种隔离模型
多租户的成本归因,前提是租户身份在链路上可靠传递。常见三种隔离模型:
- 共享实例 + 标签隔离:所有租户共用一套服务,靠 tenant_id 标签区分。成本最低,但标签基数与数据隔离风险最高。
- 按租户分实例:每个租户独立部署,天然隔离,但资源利用率低,成本归因反而简单(按实例计费)。
- 混合:大客户独立实例,小客户共享池。需要在归因模型里同时支持"实例级"与"标签级"两种口径。
本文评述:隔离模型的选择不应只由安全需求驱动,成本归因的精度也是决策变量。共享池方案若没有可靠的租户标签传播,后期对账成本会远超节省的基础设施成本。
7.2 配额与预算护栏
归因的目的是治理。有了租户维度的成本指标后,可以实施三级护栏:
- 软告警:租户日成本达到预算 80% 时通知客户成功团队。
- 硬限流:达到 100% 时降级到轻量模型或拒绝非关键请求。
- 异常熔断:单会话 token 超过阈值(如正常分布的 P99 数倍)时中断并人工复核,防止提示注入导致的 token 耗尽攻击。
第三级尤其重要。公开研究已多次指出,长上下文与提示注入可被用于放大 token 消耗(如"重复以下内容直到上限"类攻击)。这类攻击在指标上表现为单次调用 token 异常,若无会话级明细,很难与正常的长文档处理区分。
7.3 异步与批处理的归因难点
异步任务(如后台文档摘要、批量 embedding)脱离了 HTTP 请求上下文,租户与功能标签无法从请求头获取。解决方案是把 CostContext 序列化后随任务消息传递,消费者恢复上下文后再创建 Observation。这要求消息格式向前兼容:老消息缺少新字段时,归因到 legacy 并记录比例,作为技术债指标持续跟踪。
08 / DATA关键数据与案例:公开价格、缓存与流式的成本影响
8.1 公开价格的结构性事实
根据 OpenAI、Anthropic、Google 等厂商公开的定价页面(截至本文写作时的公开信息),可以观察到几个稳定的结构性事实:
- 输出 token 单价普遍高于输入 token,比例因模型而异,常见区间在 2–5 倍。
- 缓存命中的输入 token 通常有显著折扣,缓存写入可能加价。
- 同一供应商内,旗舰模型与轻量模型的单价差可达一个数量级以上。
- 批处理接口通常提供折扣,但牺牲延迟。
这些事实对归因模型的直接含义是:只记录总 token 会导致成本估算系统性偏低或偏高。例如一个大量使用系统提示缓存的客服场景,若忽略缓存折扣,成本会被高估;反之,若缓存写入被忽略,成本会被低估。工程上必须把 cached_tokens 作为一等字段。
8.2 一个模拟的成本拆解
为说明维度建模的价值,构造一个模拟场景(以下为模拟数据,仅用于演示计算逻辑,不代表任何真实系统):某 SaaS 产品有 200 个租户,三个 AI 功能:智能客服、文档摘要、代码补全。月度调用分布与 token 用量如下表。
这个模拟揭示了一个反直觉的结论:调用量最大的功能未必是最贵的功能。代码补全调用量是文档摘要的 37 倍,但成本占比不到三分之一,因为单次 token 量小。若只看调用次数做容量规划,会严重误判成本结构。这正是 L2 功能维度必须进指标的理由。
8.3 缓存与流式的量化影响
以智能客服为例,若系统提示与知识库片段高度重复,缓存命中率可达较高水平。假设输入 token 中 70% 可缓存,缓存读取价格为标准输入价的 10%(示意比例,实际以供应商文档为准),则输入侧成本可降至原来的 0.3 + 0.7×0.1 = 0.37,即约 63% 的降幅。这个量级足以改变产品定价策略,而它完全依赖于 cached_tokens 是否被记录。
流式响应的成本影响则体现在"提前断开":用户关闭页面时,模型可能已生成部分 token 并计费。若指标只在流正常完成时记录,这部分成本会成为"看不见的支出"。工程上应在取消信号中读取已累计的 usage,或按已生成字符数估算并标记。
09 / LIMITS对比与局限:Observation 方案的适用边界
9.1 与其他方案的对比
9.2 方案的固有局限
必须诚实指出 Observation 路线的边界:
- 依赖框架版本:Spring AI 的观测字段随版本演进,升级可能改变标签名或缺失字段,需要回归测试与版本锁定。
- 无法覆盖非框架调用:直接使用 HTTP 客户端调用供应商 API 的代码路径不会被自动埋点,需要统一收口。
- 估算误差:tokenizer 估算与供应商实际计费存在偏差,尤其在多模态与工具调用场景,误差可能累积。
- 标签基数上限:租户数超过数千后,指标侧需要分片或降维,否则 Prometheus 压力显著。
笔者认为,这些局限不构成否定方案的理由,而是提示"成本可观测性"应作为独立子系统建设,而非依附于某个框架的默认行为。框架提供骨架,子系统提供治理。
10 / OUTLOOK结论与展望:从成本可观测到成本可治理
回到最初的问题:为什么 AI 成本不记录,月底账单就会失控?因为 LLM 的计费粒度是 token,而业务的决策粒度是功能、租户与用户。两者之间缺少一层翻译,这层翻译就是维度建模。Spring AI 的 Micrometer Observation 提供了高质量的观测骨架,但骨架本身不会自动长出业务语义。
本文提出的三级标签模型(租户—功能—会话)与五段式管道(采集—标签—聚合—核算—告警),本质上是在回答"在哪一层记录什么"。笔者认为,未来一到两年,这一领域会朝三个方向演进:
- 标准化:OpenTelemetry 的 GenAI 语义约定正在推进,token 用量、模型名、操作类型等字段有望跨语言统一,减少供应商锁定。
- 实时化:从"月底看账单"转向"调用时预算校验",在网关层做成本准入控制。
- 治理闭环:成本指标与路由策略、缓存策略、提示词优化联动,形成"观测—分析—优化—验证"的自动闭环。
最后给出一条可执行的起步路径:第一周,接入 Micrometer 与 Prometheus,确认默认指标可见;第二周,用 Advisor 注入 tenant_id 与 feature 两个标签;第三周,建立价格表与成本聚合查询;第四周,接入明细落库与租户预算告警。四步之后,账单失控的概率会显著下降——不是因为模型变便宜了,而是因为每一分钱都有了名字。
拓展资源
- Micrometer Observation 官方文档:micrometer.io/docs/observation
- Spring AI 参考文档(Observability 章节):docs.spring.io/spring-ai/reference
微信扫一扫分享
打开微信「扫一扫」,扫描二维码后在微信中分享给好友或朋友圈。
💬 评论 (0)
评论功能已关闭

