Token用量看板:用Codex Skill实现本地统计与可视化
1. 从“看不见的消耗”到“一句话看板”这次升级解决了什么如果你用 Codex 干活大概率遇到过这种场景早上打开终端照着昨天的思路继续让 Codex 改代码结果没跑几轮就提示额度不足。翻遍设置页也看不到今天到底用了多少 token只知道“没了”。等到发账单的时候才惊觉原来一天能在 API 上烧掉几十万 token而大部分都浪费在一遍遍重复调用上。我之前的方案是写一个独立的 Python 脚本手动去翻本地日志算 token 消耗输出一个终端表格。能用但不够顺手——每次都要切窗口、敲命令、再打开另一个终端看结果。而且独立脚本和 Codex 的工作流是割裂的用完就忘很难形成“随时看、随手查”的习惯。这次我把它做成了 Codex 的 Skill不需要额外开工具也不需要记 Python 路径直接在 Codex 对话里说一句“打开每日用量看板”就能拿到当天的消耗汇总、上下文占用、分时段走势等一屏信息。整个升级过程中最值得聊的其实不是统计逻辑本身而是怎么把一个高频查数需求以最低摩擦的方式塞进日常使用流程里。这篇就完整拆一遍实现过程包括数据结构设计、Skill 指令解析、看板模板和几个我踩过的坑。先说清楚这套方案适合谁。如果你只是偶尔用 Codex 问几个问题、跑点小脚本那没必要统计得太细。但如果你像我一样拿 Codex 当日常编码助攻一天几十次对话那么“用量数据”就是刚需——它直接影响你什么时候该省着用、什么时候可以放心跑长任务、以及月底对账时心里有没有数。2. 为什么要把统计做进 Codex Skill 而不是独立脚本2.1 独立脚本的痛点最早我写统计脚本的时候需要先从 Codex 的本地会话存储里导出 JSON 日志再写一段 pandas 逻辑去聚合数据。脚本本身没多少代码真正麻烦的环节在调用链路上先要找到 Codex 的日志目录跨系统路径还不一样然后打开终端输入一长串 python /path/to/token_stats.py --day today输出是纯文本表格想在手机上看一眼根本不可能用完一次之后下次再想用又忘了参数名还得翻 README。这些问题单个看都不大但叠在一起就会让“查用量”变成一个需要刻意去做的动作。而我想要的效果是——在工作流中间想起“我今天还剩多少量”动嘴说一句就能看到结果不用打断当前思路。2.2 Skill 方案的优势Codex 的 Skill 机制相当于给你提供了一种“方言”Codex 识别到特定意图后会主动去调用脚本、工具或数据源再把结果以对话形式返回。把统计逻辑封装成 Skill相比独立脚本有几个很实际的好处统一入口。用户面对的是自然语言指令不需要记参数、路径、环境变量上下文一致。Codex 在对话过程中能直接理解你问的“今天还剩多少”结合它自己的会话上下文进行补充说明结果即所得。Skill 返回的可以是一段 Markdown 看板渲染出来比终端的灰色表格可读性强很多复用门槛低。换台机器只要把技能目录复制过去说同句话就能用不用解释“你先执行那个 py 文件”。当然Skill 也不是万能的。它本质上还是靠 Codex 来编排调用如果你要处理的是超大文本分析、长期后台监控那仍然应该用独立服务。但对于“每日用量看板”这种轻量查询场景Skill 是摩擦最小的载体。2.3 Skill 脚本的定位与边界我在设计时给这个 Skill 定了三条边界只负责“读”和“展示”不负责修改任何 Codex 配置。这样即使出了 bug也不会影响主流程所有统计都基于本地已有的日志不额外请求接口。考虑到 token 本身是敏感信息能本地算的就不要上传看板要做到“打开即懂”不需要额外解释字段含义。一个每天都会看的东西不该让用户每次都回忆“这个数字到底是什么”。边界设得清楚后面实现的时候就不会跑偏。哪怕 Codex 的 Skill 机制以后升级了这套脚本的核心逻辑也可以原样迁移。3. 统计看板的设计思路与核心数据结构3.1 需要统计哪些指标在看板设计上我不建议一上来就堆十几个指标。人的注意力有限真正每天要看的其实就这几个今日累计 Token 消耗最核心判断“还能不能放开用”的依据输入/输出/缓存 Token 拆分定位消耗大头看看是长上下文拖累还是输出内容过多按时间段聚合的消耗走势比如上午十点集中用了一轮下午零零散散用了些方便安排高消耗任务的时间当前会话上下文占用如果上下文窗口已经占掉大半后面回答质量会明显下降该考虑新开会话了默认存储路径与日志时间范围帮助自己在数据对不上时快速定位。其中“输入/输出/缓存拆分”很多人会忽略但 Codex 在调用大模型时缓存命中与否对 cost 影响极大。如果你的工作流经常在同一个会话里反复修改同一段代码缓存 token 会占相当比例拉高总消耗却不产生太多新内容。看板里把缓存单列出来能帮你判断是不是该把某些上下文拆到短会话里。3.2 数据来源与采集方式Codex 在本机会存储会话历史和相关元数据。不同版本记录的内容略有差异但核心字段通常都包括request_id一次请求的唯一 IDtimestamp请求发起时间建议用 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(descriptionCodex Token 统计看板) parser.add_argument(--day, defaultdatetime.now().strftime(%Y-%m-%d), help统计日期格式 YYYY-MM-DD) parser.add_argument(--days, typeint, default1, help统计最近 N 天默认 1) parser.add_argument(--detail, typeint, default0, help是否展示分时段明细默认 0) return parser.parse_args()这里把day和days分开是有讲究的。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_tokens加cache_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-mini、gpt-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(tzinfoLOCAL_TZ) return local.astimezone(ZoneInfo(UTC)).strftime(%Y-%m-%dT%H:%M:%S)如果你不处理时区数据交叉时的偏差会让人特别崩溃。尤其是每日看板这种按自然日聚合的场景UTC 和本地时间的切割点不一致统计结果就会“漂”。这里宁可多写几行代码也要保证口径统一。5.4 与登录态和凭证相关的问题用 Codex 过程中很多人会遇到类似 “token 刷新失败”“凭证过期” 的报错。这些报错通常和 Codex 自身的登录凭证、网络代理设置有关不是统计 Skill 的问题。但统计看板在设计时要把这类情况考虑进去——当日志缺失或者凭证失效期间没有任何请求记录时看板应该展示“无数据”而不是报一堆异常。我在脚本里做了两层保护数据库没有记录某天数据时显示当前日期无使用记录数据库文件不存在时自动创建并提示首次使用需要先跑一次 Codex 对话以产生日志。这两个保护看似简单但能避免用户在你排查问题时直接被错误堆栈吓到。毕竟一个看板工具的职责是给人看数据不是给人看 crash traceback。5.5 大文件日志导致 SQLite 膨胀本地 SQLite 文件如果无限追加也会越来越大。我设置了保留最近 30 天数据的策略在每次写入时顺带执行一次清理def clean_old_records(conn, days30): cutoff (datetime.now(ZoneInfo(UTC)) - timedelta(daysdays)).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 目前只做了每日看板但底层的数据结构已经预留了扩展空间。我计划在后续迭代里加上三个功能会话级对比列出今天 top 5 消耗会话可以定位到具体哪次任务烧掉了最多的 token模型消耗排行榜把不同模型的使用量和成本折线并排方便决定要不要切换到更经济的模型配置定期汇总推送通过系统通知或即时通讯机器人每天固定时间把前一天的使用报告推过来。如果你也想做类似的统计我的建议是先从小范围开始把“日看板”这一个场景做顺比一上来就搭一套复杂的报表系统更实用。因为使用习惯和数据口径没有稳定之前复杂功能只会增加维护成本。最后分享一个我踩了几次坑才养成的习惯Skill 脚本里所有路径都用Path.home()拼接不要写死绝对路径。换机器、切换用户时很多“找不到数据”的问题都是因为路径写死了导致的。保持脚本对机器无关才敢放心复制到别的环境里用。这套东西现在已经成为我日常使用 Codex 的一部分希望这次的拆解也能给你一些参考。