Java教程

Java与AI:AI 成本不记录,月底账单直接失控——Spring AI 的 Micrometer Observation 能零代码暴露 token 和成本指标,但多供应商、多模型、多租户场景下到底该怎么把成本归因到具体功能和用户?

👤 为我痴狂 👁 43 阅读 ❤ 0 点赞 ➦ 0 分享 📅 2026-10-23
首页› 理学› Java教程› 正文
DEEP DIVE / OBSERVABILITY & COST ATTRIBUTION
Java 与 AI:AI 成本不记录,月底账单直接失控

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 成本的三层语义

笔者建议把"成本"拆成三层,避免在工程实现中混为一谈:

层级 定义 数据来源
用量成本token 数 × 单价Observation 标签 + 价格表
账单成本供应商实际结算金额供应商账单/API 用量接口
分摊成本按业务规则分摊到功能/租户用量成本 × 分摊规则

三者之间存在系统性偏差:账单可能有最低消费、阶梯折扣、赠送额度;用量成本可能因价格表更新滞后而失真。工程上必须明确"以哪一层为准",否则会出现监控显示 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 零代码拿不到的四类信息

但默认埋点存在明确的信息缺口,这些缺口正是归因失败的技术根因:

  1. 业务维度:默认标签只有技术维度(模型、供应商、操作名),没有"哪个功能模块""哪个 API 端点""哪次用户请求"。
  2. 租户身份:多租户场景下,租户 ID 通常存在于请求头或安全上下文,默认不会进入 Observation。
  3. 计费细分:缓存命中 token、推理 token 等细分字段是否暴露,取决于框架版本与供应商实现,不能假设一定存在。
  4. 会话关联:一次用户会话可能触发多次模型调用(多轮对话、工具调用循环、RAG 多次检索),默认指标是调用粒度,不是会话粒度。
笔者认为:Observation 提供的是"可扩展的观测骨架",而不是"开箱即用的成本账本"。把它当作骨架,在其上挂业务维度,是成本最低、侵入最小的路径;指望框架替你做业务归因,是把领域建模责任错误地外包给了基础设施。

3.3 高低基数标签的分野

Micrometer 对标签基数(cardinality)高度敏感。租户 ID、用户 ID、会话 ID 属于高基数标签,直接写入指标会导致时间序列爆炸,Prometheus 侧表现为内存与查询压力剧增。业界通行做法是:低基数维度进指标,高基数维度进追踪与日志。租户数量在数百以内可视为中低基数,可进指标;用户与会话 ID 应进 span 属性或结构化日志,通过 traceId 关联。

这一分野直接决定了归因架构:指标负责"聚合视图",追踪负责"明细追溯",两者通过 traceId 与租户标签对齐。任何试图把用户 ID 塞进 Prometheus 标签的方案,在租户规模上千后都会遇到工程瓶颈。

04 / MODEL归因主线:以 Observation 上下文为骨架的三级标签模型

本节提出全文的核心模型。笔者认为,成本归因可以抽象为"在正确的上下文里,用正确的维度,记录正确的量"。三个"正确"分别对应标签模型、传播机制与计量口径。

4.1 三级标签:租户、功能、会话

把归因维度分为三级,按基数从低到高排列:

级别 维度 典型基数 载体
L1 租户tenant_id、plan_tier10²–10³指标标签 + span 属性
L2 功能feature、endpoint、prompt_template_id10¹–10²指标标签
L3 会话session_id、user_id、request_id10⁵–10⁷span 属性 + 结构化日志

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 后,成本计算有两种路径:查询时计算与写入时计算。

路径 做法 优点 缺点
查询时PromQL 中乘以价格表价格调整无需重算历史查询复杂,多供应商价格难表达
写入时应用侧计算成本后写指标查询简单,支持复杂计费规则价格变更需回填,历史口径不一致

笔者认为,生产环境宜采用双写:应用侧写 token 原始指标(口径稳定),聚合侧写成本指标(口径可演进),两者通过版本号关联。价格表变更时,用原始指标重算成本,保证可审计。

5.5 第五步:明细落库与对账

指标适合聚合,不适合审计。每次调用的明细(traceId、租户、功能、模型、token、时间戳)应异步写入分析型存储(如 ClickHouse、Doris 或云数仓),支撑:

  • 按会话下钻:某用户投诉"为什么这次回答这么贵"。
  • 与供应商账单对账:按天、按模型比对用量成本与账单成本,偏差超过阈值告警。
  • 异常检测:单次调用 token 超阈值、单租户日环比突增。

明细写入应采用批量 + 背压策略,避免观测链路拖慢主链路。业界常见做法是通过 OpenTelemetry Collector 或消息队列解耦,应用只负责产生事件。

06 / MULTI-PROVIDER多供应商与多模型:价格矩阵与路由成本

6.1 价格矩阵的建模

多供应商意味着价格不是单一数值,而是一个多维矩阵:供应商 × 模型 × 输入/输出 × 缓存状态 × 批次类型。以公开定价文档为例(价格随供应商调整,以下为示意结构,具体数值请以官方文档为准):

维度 示例取值 对成本的影响
供应商OpenAI / Anthropic / 国内厂商单价差异可达数倍
模型档位旗舰 / 轻量 / 推理型输出 token 单价通常高于输入
缓存状态命中 / 未命中 / 写入缓存读取通常有显著折扣
批次类型实时 / 批处理批处理通常有折扣,但延迟高

价格矩阵应作为配置而非硬编码,并带版本号与生效时间。工程上建议维护一张 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 配额与预算护栏

归因的目的是治理。有了租户维度的成本指标后,可以实施三级护栏:

  1. 软告警:租户日成本达到预算 80% 时通知客户成功团队。
  2. 硬限流:达到 100% 时降级到轻量模型或拒绝非关键请求。
  3. 异常熔断:单会话 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 用量如下表。

功能 月调用量 平均输入 token 平均输出 token 成本占比
智能客服120 万1800220约 62%
文档摘要8 万12000600约 30%
代码补全300 万30060约 8%

这个模拟揭示了一个反直觉的结论:调用量最大的功能未必是最贵的功能。代码补全调用量是文档摘要的 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 与其他方案的对比

方案 侵入性 归因能力 适用场景
默认 Observation零总量 + 技术维度单租户、快速验证
Advisor 注入标签低功能 + 租户多数 SaaS 场景
网关统一埋点中依赖网关解析响应体多语言、多框架
供应商账单对账无仅总量财务核算兜底

9.2 方案的固有局限

必须诚实指出 Observation 路线的边界:

  • 依赖框架版本:Spring AI 的观测字段随版本演进,升级可能改变标签名或缺失字段,需要回归测试与版本锁定。
  • 无法覆盖非框架调用:直接使用 HTTP 客户端调用供应商 API 的代码路径不会被自动埋点,需要统一收口。
  • 估算误差:tokenizer 估算与供应商实际计费存在偏差,尤其在多模态与工具调用场景,误差可能累积。
  • 标签基数上限:租户数超过数千后,指标侧需要分片或降维,否则 Prometheus 压力显著。

笔者认为,这些局限不构成否定方案的理由,而是提示"成本可观测性"应作为独立子系统建设,而非依附于某个框架的默认行为。框架提供骨架,子系统提供治理。

10 / OUTLOOK结论与展望:从成本可观测到成本可治理

回到最初的问题:为什么 AI 成本不记录,月底账单就会失控?因为 LLM 的计费粒度是 token,而业务的决策粒度是功能、租户与用户。两者之间缺少一层翻译,这层翻译就是维度建模。Spring AI 的 Micrometer Observation 提供了高质量的观测骨架,但骨架本身不会自动长出业务语义。

本文提出的三级标签模型(租户—功能—会话)与五段式管道(采集—标签—聚合—核算—告警),本质上是在回答"在哪一层记录什么"。笔者认为,未来一到两年,这一领域会朝三个方向演进:

  1. 标准化:OpenTelemetry 的 GenAI 语义约定正在推进,token 用量、模型名、操作类型等字段有望跨语言统一,减少供应商锁定。
  2. 实时化:从"月底看账单"转向"调用时预算校验",在网关层做成本准入控制。
  3. 治理闭环:成本指标与路由策略、缓存策略、提示词优化联动,形成"观测—分析—优化—验证"的自动闭环。

最后给出一条可执行的起步路径:第一周,接入 Micrometer 与 Prometheus,确认默认指标可见;第二周,用 Advisor 注入 tenant_id 与 feature 两个标签;第三周,建立价格表与成本聚合查询;第四周,接入明细落库与租户预算告警。四步之后,账单失控的概率会显著下降——不是因为模型变便宜了,而是因为每一分钱都有了名字。

拓展资源

🔒 复制本站文章内容需登录并达到 L3。当前:未登录

分享到

💬
微信
📷
朋友圈
🐧
QQ好友
🌐
QQ空间
👁
微博
📌
钉钉
🔗
复制链接
📑
复制图文

微信扫一扫分享

打开微信「扫一扫」,扫描二维码后在微信中分享给好友或朋友圈。

💬 评论 (0)

评论功能已关闭

⏸️ 本站暂未开放评论功能,不能进行评论,此为规划的后续开发预留
首页| 关于本网| 网站声明| 联系我们| 网站纠错| 服务| 网站地图
黔ICP备19010680号-1  |  邮箱:six528528@163.com
贵公网安备 52010302001819号
Copyright 2019-2026 http://www.databrush.com/ All rights reserved.
QQ
QQ扫一扫
Logo
DBN数据刷