news 2026/10/17 3:13:51

模型服务细粒度计量计费:OpenClaw如何精确统计Token用量?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
模型服务细粒度计量计费:OpenClaw如何精确统计Token用量?

模型服务接入一个月,财务甩给我一张总账单。数字挺吓人,但我根本没法解释:这笔钱到底花在哪个业务、哪个功能、哪个用户身上?如果要给老板画一张“成本地图”,这张账单给不了任何答案。

这不是某个公司特有的尴尬,几乎所有接了大模型 API 的团队都会遇到。要解决它,关键就一句话:模型服务层必须支持细粒度计量计费,并且能把每次调用的 token 用量拆得清清楚楚。这篇内容就是围绕 OpenClaw 的模型服务展开,聊聊它支不支持这种计量,以及具体怎么统计 token。文章偏实战,适合正在做网关接入、成本治理、平台基建的后端同学读。

1. 为什么“模型账单”和“成本分摊”之间永远隔着一层

1.1 厂商账单只能到账户级,到不了业务级

先说我踩过的真实场景。某公司用了上游模型 API,网关是我搭的,服务里接了四个业务:内部知识库、客服助手、内容总结、还有开发自测。到月底一看,上游平台只给了“你这账户这个月花了多少钱、按模型汇总的 token 数”。开发者为了测试调的 API、线上服务的真实用户调用,全部混在一个池子里。

问题在哪?模型供应商看到的“账户”是一个计费主体,它根本不知道你的“客服助手”和“内容总结”是两码事。你的业务标识、项目归属、用户归属,这些信息只存在于你自己的调用链路上。所以成本分摊必须在自己的网关层做,厂商账单只能当总账,当不了明细账。

1.2 细粒度计费的本质:请求级、归属级、模型级三维拆账

细粒度计量计费,拆开看就是三个维度的台账。

请求级:每个请求一套独立的 token 计数,输入多少、输出多少、有没有缓存命中。归属级:这笔费用是哪个业务线、哪个项目、哪个 API Key、哪个用户产生的。模型级:同一个请求最终由哪个模型处理,不同模型单价完全不同。

只做其中一两个维度,就会出问题。比如只有请求级没有归属级,你只能看到“一天 1000 次调用”,完全看不到哪个业务消耗最大。只有归属级没有请求级,那你无法发现某个异常请求烧掉了大量 token。OpenClaw 在模型服务层同时维护这三个维度的计量数据,这才叫真正意义上的细粒度。

1.3 OpenClaw 在计量链路中的位置:网关是唯一合适的“打点处”

OpenClaw 这类模型服务网关,通常位于客户端和上游模型之间。客户端先打给 OpenClaw,OpenClaw 再转发给上游模型,拿到响应后回传。计量打点放这一层,天然合理:因为所有请求都从这过,而且每个请求的业务标签、API Key、路由结果都能在这拿到。

放到整个链路里看,OpenClaw 并不替代上游模型的 token 统计,它更像一个“中转站兼账房先生”。上游只负责报数:每个请求生成了多少 token;OpenClaw 负责记明细:给这些 token 打上项目、用户、时间、模型标签。两者配合,才有细粒度账单。

2. OpenClaw 统计 token 用量的数据来源与计算口径

2.1 一次调用带回的 usage 对象:prompt、completion、cached 各代表什么

先看一个典型的模型响应 JSON,一般长下面这样:

{ "id": "chatcmpl-xxx", "model": "gpt-4o-mini", "choices": [...], "usage": { "prompt_tokens": 1200, "completion_tokens": 340, "total_tokens": 1540 } }

有缓存服务的模型,还会多一个 cached_tokens 字段:

"usage": { "prompt_tokens": 1200, "completion_tokens": 340, "total_tokens": 1540, "prompt_tokens_details": { "cached_tokens": 800 } }

几个字段的含义并不难:prompt_tokens 是输入侧 token 数,包括系统提示词、历史消息、用户输入;completion_tokens 是输出侧 token 数,也就是模型生成内容的长度;total_tokens 是两者求和;cached_tokens 是命中了上下文缓存、不需要重新计算的输入 token,这部分在计费上价格往往低很多。

OpenClaw 统计 token 的第一步,就是把响应里的 usage 原样捕获,然后换算成自己的计量事件。没拿到 usage,后续一切都是估算。

2.2 流式输出的统计难点:SSE 事件里到底有没有 usage

很多开发者第一次接入流式输出时,都会被同一个问题卡住:SSE 是按块返回的,每个 chunk 里只有一小段 delta,根本没有完整 usage。那 token 怎么统计?

不同上游的做法不同。一部分会在流结束前追加一个特殊事件,里面放完整 usage;也有一部分就是不给,你得自己想办法。OpenClaw 的兼容策略是:优先透传上游真实 usage,如果流式响应末尾能拿到,直接以它为准。

拿不到的时候,就只能自己在代理层累计。大致思路是解析每个 SSE 事件,如果事件是 usage 类型就把数据存下来;否则累计 content delta 的字符数,流结束后换算成估算 token。伪代码逻辑长这样:

async def handle_stream(resp): prompt_tokens = None completion_chars = 0 async for line in resp.aiter_lines(): if line.startswith("data:"): data = json.loads(line[5:]) if data.get("usage"): prompt_tokens = data["usage"]["prompt_tokens"] completion_tokens = data["usage"]["completion_tokens"] # 记录真实 usage,后面的估算就不用管了 record_usage(data["usage"]) else: delta = data["choices"][0]["delta"]["content"] or "" completion_chars += len(delta) if prompt_tokens is None: # 用字符长度估算 completion tokens estimated = int(completion_chars / 3.5) record_usage({"prompt_tokens": 0, "completion_tokens": estimated, "estimated": True})

这里 prompt_tokens 拿不到时,更靠谱的做法是在请求发出前先用本地 tokenizer 数一遍输入,或者让上游在非流式模式额外返回一次用量。字符数除以 3.5 只是一个粗糙系数,别当精确值用。

2.3 拿不到真实 usage 时的兜底方案:本地 tokenizer 估算的口径误差

本地估算,常见方案是用 tiktoken 或 transformers 里的 tokenizer 对文本切分。拿输入文本直接计数,拿流式累计的输出文本再计数。优势是不依赖上游返回,响应一到就能算出数;劣势是口径可能与上游不一致。

同一个句子,闭源模型官方 tokenizer 分出来的结果,和开源模型自己的 tokenizer 分出来的结果,经常能差 5% 到 10%。如果你的网关同时路由多个模型,本地估算几乎不可能跟每个上游完全一致。所以 OpenClaw 这类网关在计量事件里一般会打一个 estimated 标记。只要标记存在,你就知道这笔数据是对账时“待确认”的,而不是精确账。

我的经验是:实时看板可以用估算值,最终计费一定要以真实 usage 为准。两者差异如果超过 10%,优先去查是不是漏了流式 usage 事件,而不是怀疑价格表。

3. 从请求到账单:OpenClaw 细粒度计费的完整落地步骤

3.1 开启计量并配置归属维度:把 API Key、项目、标签串起来

OpenClaw 的计量能力默认是否开启,取决于部署版本,但通常都能在配置文件里显式开启。一般配置类似这样:

usage-metering: enabled: true dimensions: - api_key - project - model - custom_tags sink: type: clickhouse table: usage_events price_table: price_config.json

这里的关键是 dimensions。建议最少包含 api_key 和 model。api_key 对应归属,model 对应价格。如果业务上有多个产品线共用一个网关,project 也建议加上,可以放进请求元数据里,由调用方附带。

还有一个小建议:在配置里顺便开启请求 ID 透传。每次调用生成一个全局 request_id,同时往里带你的业务 trace_id。后面所有计量事件都用这个 request_id 关联,对账时能少掉很多头发。

3.2 请求打点与事件流:为什么日志文件撑不住计量需求

配置只是第一步。真正跑起来之后,每次请求完成都要产生一条计量事件。字段大致包括:时间戳、request_id、api_key、project、model、上游模型 ID、prompt_tokens、completion_tokens、cached_tokens、费用、estimated 标记。

为什么不直接写进日志文件?因为日志不能按项目、按时间范围做高效聚合。真要算“客服项目昨天花了多少钱”,对着一堆日志 grep 会痛不欲生。常见做法是事件发到 ClickHouse、Doris 或 Elasticsearch,再按维度建索引。

下面是我用过的表结构示意:

CREATE TABLE usage_events ( ts DateTime, request_id String, api_key String, project String, model String, upstream_model String, prompt_tokens Int64, completion_tokens Int64, cached_tokens Int64, cost Float64, estimated Bool ) ENGINE = MergeTree PARTITION BY toDate(ts) ORDER BY (ts, project, api_key);

这张表的核心价值在于:所有计量问题都能变成 SQL 查询。比如“按项目按月汇总”“按 key 找 top 10 用户”“查某天异常突增的 request”,都是顺手的事。

3.3 费用计算与账单拆分:价格表、单价和聚合 SQL

记录 token 是第一步,折算成钱才是真正意义上的计费。OpenClaw 一般会维护一份价格表。模型输入输出单价往往不同,比如某模型输入 0.15 元/千 token,输出 0.6 元/千 token。价格表用配置仓库管理也行,直接放 JSON 也行。下面是一段基础的费用计算函数:

def calc_cost(model, prompt_tokens, completion_tokens, cached_tokens): p = PRICE_TABLE[model] input_cost = prompt_tokens * p["input_per_1k"] / 1000 cached_cost = cached_tokens * p["cached_per_1k"] / 1000 output_cost = completion_tokens * p["output_per_1k"] / 1000 return input_cost + cached_cost + output_cost

注意缓存计费不能漏。如果上游返回了 cached_tokens,却没有对应单价,可以按输入单价的某个倍率处理,常见是 0.1 到 0.5 倍。统一在价格表里配置,别在代码里写死。

落到查询侧,账单拆分就是按分组聚合:

SELECT project, model, sum(prompt_tokens + completion_tokens) AS total_tokens, sum(cost) AS total_cost FROM usage_events WHERE ts >= toDate('2025-01-01') GROUP BY project, model;

到这一步,“客服项目一个月花了多少钱、哪个模型最烧钱”这类问题才终于有了明确答案。

3.4 配额反哺限流:计量不只是记账,还能拦超支

计量体系如果只有统计功能,顶多算一个后置报告。真正让它有价值的,是把计量结果反馈到网关执行层。具体说:给每个项目、每个 API Key 设置月配额或余额,每次请求完成之后实时累加。当累计费用超过阈值,OpenClaw 在转发前直接返回 429 或者自定义错误。

这一步需要精确的预算口径。是按 token 数限,还是按费用限?我建议按费用限,因为 token 多不代表贵,输出型模型的 token 单价可能比输入型贵好几倍。配置示例:

quota: - scope: project name: customer-service limit_cost: 5000 period: month action: block

配额控制最大的价值不是省钱,而是避免“某个功能出了问题后疯狂调模型,一夜烧掉几万块”这种事发生。机器不会替你心疼钱,但配额会。

4. 我跑计量体系时踩过的几个坑

4.1 缓存命中 token 的计费单价问题

第一次对账时,我发现账单和上游账单差了一大截,最后定位到是缓存字段没算好。一个输入 1200 token 的请求,其中 800 token 命中了缓存,计费单价是普通输入的 0.1 倍。如果我按照完整 1200 token 按原价算,每笔都会多算不少钱。聚合起来,误差就是天文数字。

建议在价格表里给读取缓存单独一个单价,并且在上游未返回 cached_tokens 时按 0 处理,不要默认它不存在。做费用换算时,先把 cached_tokens 从 prompt_tokens 里拆出来单独乘系数,避免重复计费。

4.2 重试和并发下的重复计费

另一个坑来自客户端重试。调用 OpenClaw 的时候,如果响应超时,客户端一般会换一个请求重发。个别客户端没带幂等键,同样的请求到达网关,被当成两笔独立调用,算了两遍费用。

解决思路有两个。比较实用的:OpenClaw 网关层为同一客户端相同请求内容生成 request hash,短时间窗口内自动去重;或者在计量事件落库前,按 request_id + 输入内容 hash 做唯一约束。分布式环境下注意原子性,建议用数据库唯一索引兜底。我后来选了“唯一索引 + 幂等键透传”双保险,从源头堵住重复。

4.3 动态路由导致的“记错模型名”

OpenClaw 这类网关支持动态路由。同一个 API 名称下,可能根据上下文长度把请求路由到小模型,或者根据失败自动 fallback 到另一个模型。如果计量事件里只记录“客户端请求的模型名”,而不是“实际完成计算的上游模型名”,计费一定错。

正确做法是:从上游响应头或者 OpenClaw 的路由日志里取实际模型 ID,写进 upstream_model 字段。费用计算用 upstream_model 查价格表,而不是用请求时的 model 字段。如果上游响应没给出明确模型 ID,至少在路由时把 routing_result 一并写入事件。

4.4 聚合窗口与时区:跨天账单为什么对不平

本地时区中午 12 点,换成 UTC 是凌晨 4 点。如果你的计量事件按本地时间存储,聚合口径稍微一变,跨天数据就会错位,和上游账单对不上。我踩过这个坑后,把所有事件时间统一存 UTC 时间戳,展示时再由前端转成业务时区。这样无论怎么按天、按小时聚合,底层口径都不会变。

另外建议,日结报表不用“今天 00:00 到 24:00”这种本地区间,直接用 UTC 的 toDate,或者明确指定时区转换函数。别让数据库时区设置背锅,很多时候只是你忘了事件时间不是业务所在地时间。

5. 如果不用 OpenClaw,自己搭最小可用 token 统计方案

5.1 中间件打点:在 FastAPI 网关里埋一个计费器

如果你不想引入完整网关,也可以在自己的服务里搭一个最小版。核心思路是在路由层包一层中间件:请求进来时复制 body,响应返回时解析 body,从里面取 usage,然后异步发计量事件。

一个简化版 FastAPI 中间件长这样:

@app.middleware("http") async def metering_middleware(request: Request, call_next): body = await request.body() response = await call_next(request) if response.status_code == 200: resp_body = b"".join([chunk async for chunk in response.body_iterator]) payload = json.loads(resp_body) if payload.get("usage"): send_usage_event(payload["usage"], request.headers.get("X-Project", "unknown")) return response

注意,中间件方案对流式响应支持很差,因为你拿不到完整响应体。真要支持流式,就得在响应迭代器里自行聚合,或者干脆改走事件回调。这也是我最终更偏向用现成网关的原因:自己写流式计量非常容易漏数。

5.2 本地 token 计数与真实账单的误差控制

实在拿不到真实 usage 时,本地计数要选对 tokenizer。不同模型对应不同的词表,用错 tokenizer 统计出来的数基本没有参考价值。tiktoken 适合某类闭源模型;transformers 的 AutoTokenizer 适合大部分开源模型;如果是自己微调的模型,直接用模型训练时的 tokenizer。

误差控制上,我的建议是“用真实数据校准系数”。比如拿一千条真实请求,对比本地估算值与上游 usage,算出一个平均偏差比例,再在估算公式里乘上校准系数。即使这样,也只能缩小误差,不能完全消除。所以,任何涉及真金白银的计费,都必须以真实 usage 为准。

5.3 离线对账:让糊涂账变明白账的最后一道防线

自建方案的最后一步,是离线对账。每天凌晨跑一个任务,从上游平台导出离线账单,拉取本地计量明细,按 request_id 或按天汇总 token 总数,两边做比对。差异超过 5% 就告警。

对账要有粒度。如果只是“今天总计差了多少”,很难定位问题。建议按模型分组对比:哪个模型差异最大,就看哪条链路的 usage 数据最不可靠。历史经验里,差异来源不是价格表配错,就是流式计量漏数,再就是缓存 token 没拆开。把这几个方向逐个排查,大部分都能一天内查清。

整套 OpenClaw 计量跑下来,我最深的体会是:细粒度计量计费真正难的不是代码,而是口径。token 怎么数、缓存怎么算、重试怎么去重、时区怎么统一,每一处口径不一致,最终都会反映在对不上的账单里。把我这次的实现过程写出来,就是希望你在搭的时候能少试几遍错。

最后再分享一个实操小技巧——计量事件一定要留原始响应快照。我之前为了减存储成本,只存了 token 汇总字段,后来对账发现有差异却没有任何原始证据,只能干瞪眼。现在我会把每次调用的请求体摘要和响应的 usage 原始 JSON 一起存,保留 90 天。存储确实多花了点钱,但对账时省下的时间,绝对值回票价。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 3:13:36

基于STM32H743的Modbus TCP工业网关设计与实现

简介:stm32h743平台下的FreeRTOSLWIPFreeMODBUS TCP/IP移植资源包,面向需要在H7系列MCU上搭建实时网络通信能力的嵌入式开发者。以正点原子阿波罗开发板为硬件基础,整合了LWIP Socket TCP客户端例程与FreeMODBUS TCP移植代码,适合…

作者头像 李华
网站建设 2026/10/17 3:13:27

Android Input事件分发全链路解析:从硬件中断到View onTouchEvent

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 3:13:10

UE/C++调试信息打印与增强输入绑定完整教程

1. 先搞清楚这个项目要解决什么问题把"打印"和"输入"放在同一个标题里,乍一看像两个不相关的知识点,但实际做项目时它们经常一起出现:你想确认按键有没有触发、数值有没有传对、Mapping Context 有没有加载,都…

作者头像 李华
网站建设 2026/10/17 3:12:50

基于LLM智能体的城市遥感图像变化分析系统设计与实现

城市遥感变化检测这几年几乎是所有测绘院、规划院和卫星应用公司的刚需。但凡是做过实际项目的人都清楚,传统流程是把两期影像丢进模型里,出一张差分图,再人工看一遍,写报告,过程又慢又费人。我这两年一直在琢磨一件事…

作者头像 李华
网站建设 2026/10/17 3:12:49

Hexo部署与Sakura主题调教:从零搭建个人博客全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/17 3:12:34

Spring Boot少儿教育网站毕业设计实战:从数据库到权限控制完整解析

1. 项目概述1.1 核心需求解析看到这个标题,很多刚入行的开发者第一反应可能是:又是一个简单的CRUD管理系统。但真正上手做过少儿教育类项目的朋友都清楚,这类系统的坑远比其他管理系统多。表面上看是普通的注册登录加课程展示,实际…

作者头像 李华