Token用量看板:用Codex Skill实现本地统计与可视化
2026/9/13 8:34:57 网站建设 项目流程

1. 从“看不见的消耗”到“一句话看板”:这次升级解决了什么

如果你用 Codex 干活,大概率遇到过这种场景:早上打开终端,照着昨天的思路继续让 Codex 改代码,结果没跑几轮就提示额度不足。翻遍设置页也看不到今天到底用了多少 token,只知道“没了”。等到发账单的时候才惊觉,原来一天能在 API 上烧掉几十万 token,而大部分都浪费在一遍遍重复调用上。

我之前的方案是写一个独立的 Python 脚本,手动去翻本地日志算 token 消耗,输出一个终端表格。能用,但不够顺手——每次都要切窗口、敲命令、再打开另一个终端看结果。而且独立脚本和 Codex 的工作流是割裂的,用完就忘,很难形成“随时看、随手查”的习惯。

这次我把它做成了 Codex 的 Skill:不需要额外开工具,也不需要记 Python 路径,直接在 Codex 对话里说一句“打开每日用量看板”,就能拿到当天的消耗汇总、上下文占用、分时段走势等一屏信息。整个升级过程中最值得聊的其实不是统计逻辑本身,而是怎么把一个高频查数需求,以最低摩擦的方式塞进日常使用流程里。这篇就完整拆一遍实现过程,包括数据结构设计、Skill 指令解析、看板模板和几个我踩过的坑。

先说清楚这套方案适合谁。如果你只是偶尔用 Codex 问几个问题、跑点小脚本,那没必要统计得太细。但如果你像我一样,拿 Codex 当日常编码助攻,一天几十次对话,那么“用量数据”就是刚需——它直接影响你什么时候该省着用、什么时候可以放心跑长任务、以及月底对账时心里有没有数。

2. 为什么要把统计做进 Codex Skill 而不是独立脚本

2.1 独立脚本的痛点

最早我写统计脚本的时候,需要先从 Codex 的本地会话存储里导出 JSON 日志,再写一段 pandas 逻辑去聚合数据。脚本本身没多少代码,真正麻烦的环节在调用链路上:

  1. 先要找到 Codex 的日志目录,跨系统路径还不一样;
  2. 然后打开终端,输入一长串 python /path/to/token_stats.py --day today;
  3. 输出是纯文本表格,想在手机上看一眼根本不可能;
  4. 用完一次之后,下次再想用又忘了参数名,还得翻 README。

这些问题单个看都不大,但叠在一起,就会让“查用量”变成一个需要刻意去做的动作。而我想要的效果是——在工作流中间想起“我今天还剩多少量”,动嘴说一句就能看到结果,不用打断当前思路。

2.2 Skill 方案的优势

Codex 的 Skill 机制相当于给你提供了一种“方言”,Codex 识别到特定意图后,会主动去调用脚本、工具或数据源,再把结果以对话形式返回。把统计逻辑封装成 Skill,相比独立脚本有几个很实际的好处:

  • 统一入口。用户面对的是自然语言指令,不需要记参数、路径、环境变量;
  • 上下文一致。Codex 在对话过程中能直接理解你问的“今天还剩多少”,结合它自己的会话上下文进行补充说明;
  • 结果即所得。Skill 返回的可以是一段 Markdown 看板,渲染出来比终端的灰色表格可读性强很多;
  • 复用门槛低。换台机器,只要把技能目录复制过去,说同句话就能用,不用解释“你先执行那个 py 文件”。

当然,Skill 也不是万能的。它本质上还是靠 Codex 来编排调用,如果你要处理的是超大文本分析、长期后台监控,那仍然应该用独立服务。但对于“每日用量看板”这种轻量查询场景,Skill 是摩擦最小的载体。

2.3 Skill 脚本的定位与边界

我在设计时给这个 Skill 定了三条边界:

  1. 只负责“读”和“展示”,不负责修改任何 Codex 配置。这样即使出了 bug,也不会影响主流程;
  2. 所有统计都基于本地已有的日志,不额外请求接口。考虑到 token 本身是敏感信息,能本地算的就不要上传;
  3. 看板要做到“打开即懂”,不需要额外解释字段含义。一个每天都会看的东西,不该让用户每次都回忆“这个数字到底是什么”。

边界设得清楚,后面实现的时候就不会跑偏。哪怕 Codex 的 Skill 机制以后升级了,这套脚本的核心逻辑也可以原样迁移。

3. 统计看板的设计思路与核心数据结构

3.1 需要统计哪些指标

在看板设计上,我不建议一上来就堆十几个指标。人的注意力有限,真正每天要看的其实就这几个:

  • 今日累计 Token 消耗:最核心,判断“还能不能放开用”的依据;
  • 输入/输出/缓存 Token 拆分:定位消耗大头,看看是长上下文拖累,还是输出内容过多;
  • 按时间段聚合的消耗走势:比如上午十点集中用了一轮,下午零零散散用了些,方便安排高消耗任务的时间;
  • 当前会话上下文占用:如果上下文窗口已经占掉大半,后面回答质量会明显下降,该考虑新开会话了;
  • 默认存储路径与日志时间范围:帮助自己在数据对不上时快速定位。

其中“输入/输出/缓存拆分”很多人会忽略,但 Codex 在调用大模型时,缓存命中与否对 cost 影响极大。如果你的工作流经常在同一个会话里反复修改同一段代码,缓存 token 会占相当比例,拉高总消耗却不产生太多新内容。看板里把缓存单列出来,能帮你判断是不是该把某些上下文拆到短会话里。

3.2 数据来源与采集方式

Codex 在本机会存储会话历史和相关元数据。不同版本记录的内容略有差异,但核心字段通常都包括:

  • request_id:一次请求的唯一 ID;
  • timestamp:请求发起时间,建议用 UTC ISO 8601 格式;
  • model:实际使用的模型标识;
  • input_tokens:请求输入 token 数;
  • output_tokens:响应输出 token 数;
  • cache_creation_tokens:本次写入缓存的 token 数;
  • cache_read_tokens:本次从缓存读取的 token 数。

实际采集时,我会优先找最近一次会话产生的日志文件,因为 Codex 通常按会话分目录,最新的日期和修改时间能直接定位。然后针对当天的请求逐条读取。要特别注意:不是所有日志字段都是必存的,早期版本可能没有缓存相关字段,读取时需要用 0 或 None 兜底。

3.3 本地存储结构

为了不让每次查询都扫描全部日志,我设计了一个轻量的本地冗余存储:一个 SQLite 文件,每次会话结束后主动追加一行统计摘要。表结构设计如下:

CREATE TABLE IF NOT EXISTS token_usage_daily ( id INTEGER PRIMARY KEY AUTOINCREMENT, request_id TEXT UNIQUE, ts TEXT NOT NULL, model TEXT NOT NULL, input_tokens INTEGER DEFAULT 0, output_tokens INTEGER DEFAULT 0, cache_creation_tokens INTEGER DEFAULT 0, cache_read_tokens INTEGER DEFAULT 0, total_tokens INTEGER DEFAULT 0, session_id TEXT ); CREATE INDEX IF NOT EXISTS idx_ts ON token_usage_daily(ts); CREATE INDEX IF NOT EXISTS idx_model ON token_usage_daily(model);

为什么不用原始日志直接算?原因很实际:日志文件会定期归档清理,而且解析成本随着会话增长不断升高。SQLite 文件小、查询快、数据明确定义,还能在 Skill 里直接用 SQL 做聚合。缺点是会多一次写入动作,但相对于每次查询都翻几百 MB 日志,这笔开销非常划算。

在记录时,我还会把total_tokens直接算好存进去,避免每次统计重复做加法。虽然这看起来像冗余,但能让查询语句简化不少,也方便在 SQLite 客户端里直接预览。

4. 升级实现:从命令触发到一句话调起

4.1 Skill 入口与参数解析

Codex Skill 的编排方式大同小异:一个 skill 目录里放着 SKILL.md 描述文件和一个可执行脚本。SKILL.md 写清楚这个 Skill 的触发条件、参数说明和输出规范,相当于给 Codex 一份“使用说明书”。

我的SKILL.md核心内容大概长这样:

# Token Usage Dashboard ## 触发方式 当用户想查看 Codex 的 token 使用量、每日消耗、用量看板时,自动触发此 Skill。 ## 支持的指令示例 - 打开每日用量看板 - 看看今天 token 用了多少 - 最近三天的消耗情况 - 当前会话上下文占用如何 ## 参数 - day: 可选参数,指定日期,格式 YYYY-MM-DD,默认今天 - days: 可选参数,生成最近 N 天趋势图,默认 1 - detail: 可选参数,值设为 1 时展示分时段明细 ## 输出 返回 Markdown 表格,并附上异步生成的趋势图链接。

Codex 读到这份文件后会根据对话内容自动填充参数,然后调用底层脚本,再把脚本输出整理成回答。所以脚本侧不需要做太多意图理解,做好参数解析就行。

下面是脚本的入口部分,用 Python 标准库即可实现,不依赖外部工具:

import argparse import sqlite3 from pathlib import Path from datetime import datetime, timedelta DB_PATH = Path.home() / ".codex_token_stats" / "usage.sqlite" def parse_args(): parser = argparse.ArgumentParser(description="Codex Token 统计看板") parser.add_argument("--day", default=datetime.now().strftime("%Y-%m-%d"), help="统计日期,格式 YYYY-MM-DD") parser.add_argument("--days", type=int, default=1, help="统计最近 N 天,默认 1") parser.add_argument("--detail", type=int, default=0, help="是否展示分时段明细,默认 0") return parser.parse_args()

这里把daydays分开是有讲究的。day表示一个指定日期,适合想回看历史某一天时使用;days表示最近 N 天,用于生成趋势视图。很多统计工具只给一个参数,导致“今天”和“最近七天”的语义混在一起,不易处理。

4.2 核心统计逻辑

看板的主体是几个聚合查询,不需要复杂算法。但有几个细节要注意:时间字段统一存储为 UTC,展示时再转本地时间。这样即使你换一台时区不同的机器,数据也不会乱。

当日汇总查询如下:

def query_daily_summary(conn, day): start = f"{day}T00:00:00" end = f"{day}T23:59:59" sql = """ SELECT COUNT(DISTINCT session_id) AS session_count, SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens, SUM(cache_creation_tokens) AS cache_creation_tokens, SUM(cache_read_tokens) AS cache_read_tokens, SUM(total_tokens) AS total_tokens FROM token_usage_daily WHERE ts >= ? AND ts <= ? """ cur = conn.execute(sql, (start, end)) row = cur.fetchone() return { "session_count": row[0] or 0, "input_tokens": row[1] or 0, "output_tokens": row[2] or 0, "cache_creation_tokens": row[3] or 0, "cache_read_tokens": row[4] or 0, "total_tokens": row[5] or 0, }

分时段走势我按照小时粒度切分,这样一眼能看出“上午那波大消耗”和“下午的小开销”。如果你需要更细的粒度,改成 minute 也行,但对看板意义不大,反而会把表格拉得很长。

def query_hourly_trend(conn, day): sql = """ SELECT strftime('%H:00', ts) AS hour, SUM(total_tokens) AS total_tokens, COUNT(*) AS request_count FROM token_usage_daily WHERE ts >= ? AND ts <= ? GROUP BY hour ORDER BY hour """ day_start = f"{day}T00:00:00" day_end = f"{day}T23:59:59" return [ {"hour": r[0], "total_tokens": r[1], "request_count": r[2]} for r in conn.execute(sql, (day_start, day_end)).fetchall() ]

“当前会话上下文占用”查询会稍微特殊一点。Codex 的会话里可能包含多条请求,上下文的真实占用往往要看最后一条请求的input_tokenscache_read_tokens。其中cache_read_tokens表示已经进入长上下文缓存的部分,算作基础占用的一部分;新的输入增量则会被计到cache_creation_tokens里。所以粗略计算公式是:

context_used ≈ last_input_tokens + last_cache_read_tokens

注意这只是估算。不同版本 Codex 对上下文的统计口径略有差异,但这个近似值已经足够帮你判断“该不该新开会话”了。

4.3 看板生成与展示

看板最终的呈现方式是 Markdown 表格加一段汇总文本。Skill 触发后,Codex 会把脚本输出原样拼接到回答里,因此脚本的 print 内容就是看板本身。

以下是我实际使用的输出模板:

## 今日 Token 用量看板(2025-06-14) | 指标 | 数值 | | --- | --- | | 会话数 | 23 | | 输入 Tokens | 1,230,456 | | 输出 Tokens | 456,789 | | 缓存写入 Tokens | 320,100 | | 缓存读取 Tokens | 780,200 | | 总 Tokens | 2,787,545 | ### 分时段消耗(按小时) | 时段 | 请求数 | Tokens | | --- | --- | --- | | 08:00 | 4 | 120,300 | | 09:00 | 11 | 870,200 | | 10:00 | 5 | 1,100,300 | | ... | ... | ... |

这段输出在 Codex 对话窗口里会渲染成美观的表格。如果你用的客户端不支持 GFM 表格,也至少是纯文本对齐,不影响阅读。

趋势图我用了另一个小技巧:脚本生成一个基于 HTML 的本地文件,路径显示在看板下方,点击即可在浏览器打开。这样既能保持对话内轻量,又能看更直观的柱状图。生成 HTML 用的是模板字符串,不依赖任何图表库,只输出简单的 div 高度来模拟柱形,效果够用。

5. 关键细节与踩坑记录

5.1 Token 统计偏差是必然的

首先要接受一个现实:Token 计数在不同环节有可能不一致。模型 API 返回的 token 数、本地日志记录的 token 数、以及你自己用分词器估算的值,三者往往存在细微差异。原因很复杂,包括多轮对话时系统提示词的重算、负载均衡导致的日志写入延迟、以及 Codex 自身对上下文压缩的处理。

我采用的原则是:以本地日志为准,在使用说明里直接注明“统计结果用于趋势观察,不是计费依据”。这样用户看到数字和账单有出入时,不会觉得是 bug。如果你确实需要精确计费审计,应该去官方后台的使用页面核对,而不是依赖本地日志。

5.2 模型标识差异导致分组混乱

Codex 历史版本里模型标识有过多种写法,比如gpt-4o-minigpt-5-sol、以及一些带日期后缀的内部代号。同一个任务在不同时段可能使用不同模型,统计数据如果不归并,看板会出现很多分散的小行。

我在聚合前会先做一层模型映射,将同类的模型标识归到一个展示名下面:

MODEL_ALIAS_MAP = { "gpt-4o-mini": "gpt-4o-mini", "gpt-5-sol": "gpt-5-sol", "gpt-5.6-sol": "gpt-5-sol", "gpt-5-codex": "codex", } def normalize_model(raw): return MODEL_ALIAS_MAP.get(raw, raw)

这个映射表非常重要,否则你会在看板里看到三个长得像但实际同类的模型名,白白增加理解成本。自己使用时,建议先跑一个去重查询,确认本地日志里到底出现过多少种 model 字段。

5.3 时间范围和时区

我在第一次上线时遇到过一个诡异现象:明明昨晚十一点跑了一大轮任务,今天早上的看板却显示为 0。排查后发现问题出在时间存储上——日志里的 UTC 时间被直接按本地时间处理,导致昨晚十一点其实已经是 UTC 第二天凌晨,被划到了“明天”的数据里。

解决办法就是前面提到的:统一存储 UTC,展示时再做转换。脚本里加一个tz_local参数,默认使用系统时区,但查询范围始终用 UTC 计算:

from zoneinfo import ZoneInfo LOCAL_TZ = ZoneInfo("Asia/Shanghai") def local_to_utc_start(local_date_str): local = datetime.strptime(local_date_str, "%Y-%m-%d").replace(tzinfo=LOCAL_TZ) return local.astimezone(ZoneInfo("UTC")).strftime("%Y-%m-%dT%H:%M:%S")

如果你不处理时区,数据交叉时的偏差会让人特别崩溃。尤其是每日看板这种按自然日聚合的场景,UTC 和本地时间的切割点不一致,统计结果就会“漂”。这里宁可多写几行代码,也要保证口径统一。

5.4 与登录态和凭证相关的问题

用 Codex 过程中,很多人会遇到类似 “token 刷新失败”“凭证过期” 的报错。这些报错通常和 Codex 自身的登录凭证、网络代理设置有关,不是统计 Skill 的问题。但统计看板在设计时,要把这类情况考虑进去——当日志缺失,或者凭证失效期间没有任何请求记录时,看板应该展示“无数据”,而不是报一堆异常。

我在脚本里做了两层保护:

  1. 数据库没有记录某天数据时,显示当前日期无使用记录
  2. 数据库文件不存在时,自动创建并提示首次使用需要先跑一次 Codex 对话以产生日志。

这两个保护看似简单,但能避免用户在你排查问题时直接被错误堆栈吓到。毕竟一个看板工具的职责是给人看数据,不是给人看 crash traceback。

5.5 大文件日志导致 SQLite 膨胀

本地 SQLite 文件如果无限追加,也会越来越大。我设置了保留最近 30 天数据的策略,在每次写入时顺带执行一次清理:

def clean_old_records(conn, days=30): cutoff = (datetime.now(ZoneInfo("UTC")) - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%S") conn.execute("DELETE FROM token_usage_daily WHERE ts < ?", (cutoff,))

由于源日志文件本身有归档机制,本地统计库保留 30 天足够覆盖绝大多数的对账和趋势分析需求。再长的历史数据,直接去官方后台导出更靠谱。

6. 使用效果与后续扩展

6.1 实际使用效果

升级成 Skill 之后,我实际使用了两周,最大的变化不是“能看数字了”,而是“愿意每天都看了”。过去查一次用量至少要半分钟,现在说句话就出来,就愿意坚持。两周下来我发现几个有意思的趋势:

  • 我的 token 消耗并不是均匀分布的,而是集中在每天上午十点到十一点。因为那段时间我会让 Codex 做多文件重构,经常一次性读满上下文;
  • 缓存读取 token 占了总消耗的大头,说明我在同一个会话里反复修改同一批文件很频繁。于是我开始有意在改动面变大时新开一个会话,缓存读取占比降了一些;
  • 输出 token 比想象中高。原因是 Codex 有时会把不需要修改的文件也完整输出一遍,这受模型行为影响比较大,但至少看板能让你意识到这个现象。

看板的价值就在这里——它不直接帮你省 token,但能通过肉眼可见的数据分布,逼你反思自己的工作流。“知道自己在哪浪费”往往比“用更贵的模型”更能立竿见影地省钱。

6.2 可扩展方向

这个 Skill 目前只做了每日看板,但底层的数据结构已经预留了扩展空间。我计划在后续迭代里加上三个功能:

  1. 会话级对比:列出今天 top 5 消耗会话,可以定位到具体哪次任务烧掉了最多的 token;
  2. 模型消耗排行榜:把不同模型的使用量和成本折线并排,方便决定要不要切换到更经济的模型配置;
  3. 定期汇总推送:通过系统通知或即时通讯机器人,每天固定时间把前一天的使用报告推过来。

如果你也想做类似的统计,我的建议是先从小范围开始,把“日看板”这一个场景做顺,比一上来就搭一套复杂的报表系统更实用。因为使用习惯和数据口径没有稳定之前,复杂功能只会增加维护成本。

最后分享一个我踩了几次坑才养成的习惯:Skill 脚本里所有路径都用Path.home()拼接,不要写死绝对路径。换机器、切换用户时,很多“找不到数据”的问题都是因为路径写死了导致的。保持脚本对机器无关,才敢放心复制到别的环境里用。这套东西现在已经成为我日常使用 Codex 的一部分,希望这次的拆解也能给你一些参考。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询