Gemini CLI 用起来是真顺手,但每个月看到账单那一刻也是真的肉疼。我见过不少团队一开始都用得挺欢,等到月底财务把 API 费用统计甩过来,才发现 Token 消耗完全是个黑盒。这篇博文想聊的不是怎么省 Token,而是怎么把“到底烧了多少”这件事,做成一套不需要人工干预的全局自动化统计系统。无论你是个人开发者想摸清自己的使用习惯,还是小团队里负责成本管控的人,这篇内容都能直接拿走用。
1. 为什么 Token 统计会成为刚需
1.1 从一次账单异常说起
我最早意识到 Token 统计这个问题,是因为一次账单异常。月初查用量的时候发现某一天的消耗量是平时的三倍,但那天我明明没怎么高强度使用。翻遍 Gemini CLI 的终端输出,只能看到零散的单次请求数据,完全拼不出那天的完整用量图景。单次调用有 token 数,但“今天一共用了多少”“哪个项目消耗最大”“哪类任务最费 token”,这些全都答不上来。
时间一长就发现,手动记录 Token 这件事根本不可行。命令行工具的使用是碎片化的,可能在终端 A 跑了一个长任务,在终端 B 试了几个 prompt,又在脚本里批量跑了几十次调用。每一次单独看都有数,但没有任何地方能给出一个汇总视角。没有全局统计,就意味着你对成本的感知永远是滞后的——等账单出来才发现超了,再回头优化,已经被动。
1.2 全局统计到底在统计什么
要设计这套统计系统,先得把“全局”两个字拆清楚。我最开始以为只要把每次调用的 token 数累加就行,实际落地后发现远没那么简单。真正有价值的统计至少要覆盖三个维度:
- 跨会话:终端关了就没了,但统计不能丢。每次
gemini run是一次独立会话,需要按时间连续记录,才能回答“这个月累计消耗多少”。 - 跨目录:Gemini CLI 支持在任意项目目录下运行,但默认日志全局只有一份。如果不主动记录当前工作目录,就没法知道哪个项目在烧钱。
- 跨类型:Token 有输入和输出之分,输入是上下文和 prompt 的费用,输出是生成结果的费用,两者计费单价不一样。只看总 token 数会掩盖很多信息。
想清楚这三点之后,“全局自动化统计”的定义就清晰了:自动捕获每一次 Gemini CLI 请求的 Token 明细,带上时间、模型、目录等上下文信息,持久化存储,并提供可查询的汇总能力。整个过程不能依赖人工记忆,也不能漏掉任何一次调用。
2. 方案选型:三条路线怎么挑
2.1 路线一:直接解析 CLI 终端输出
最直观的思路是从 CLI 的 stdout 里捞数据。Gemini CLI 在正常输出之后会打印 token 使用情况,看起来像这样:
Model: gemini-2.0-flash (google) Total tokens: 2,365 Input tokens: 1,523 Output tokens: 842理论上写个脚本包一层,每次执行命令后捕获输出,用正则把这几行提取出来就行。我一开始也确实这么试了,但很快就发现这个方案有几个难以逾越的问题。
首先是输出格式不稳定。不同子命令、不同版本、甚至不同模型返回的 token 信息格式都有差异。有时候是Total tokens: 2,365,有时候是Tokens: 2,365 (input: 1,523, output: 842),正则需要不断打补丁。其次是脚本包装只对交互式命令有效——如果你在终端里正常聊天式地使用,那没问题;但如果你在别的程序里调用 CLI,或者通过 IDE 插件用,包装层根本拦截不到。
这个方案不是不能跑,但它太脆了,任何一个小变化都会让整个统计链路断裂。只适合临时用一下,不适合做成长期运行的全局统计系统。
2.2 路线二:从日志文件里挖数据
Gemini CLI 会在本地写入详细的日志,里面包含了每次请求的结构化信息。这是所有路线里最稳的一条,因为日志是 CLI 自己写的,天然覆盖所有使用方式——无论你是交互式聊天、单次命令执行,还是第三方脚本调用,只要底层是同一个 CLI,日志就一定会记录。
日志的格式整体是 JSON 结构,每条记录包含时间戳、请求 ID、模型名、操作类型和 token 使用明细。这就意味着不需要做脆弱的文本解析,可以直接按 JSON 字段提取,准确率高得多。而且日志是追加写入的,用一个文件追踪脚本就能实时捕获新记录,天然适合做持续统计。
这条路线唯一的门槛是日志文件的位置和格式需要摸清楚。但一旦摸清,后面就是一条笔直的大路,稳定性和可维护性都远超终端输出解析方案。
2.3 路线三:网络代理层接管全部流量
还有人会想到更“底层”的方案:把 Gemini API 的请求全部指向一个本地代理服务,由代理记录每个请求和响应的 body,从里面提取 token 数据。这样可以拿到最原始、最完整的 API 交互记录,甚至能统计到 CLI 之外的应用发起的请求。
但我个人非常不建议为这个需求专门做代理层。首先是开发成本高,你需要起一个本地服务,配置 TLS 证书,处理各种协议细节,这比写一个日志解析脚本要重得多。其次是维护风险大,代理层一旦出问题,直接影响所有请求的正常使用,本来只是想统计一下用量,结果把主力工具搞挂了,得不偿失。除非你有审计全部流量的合规需求,否则纯为 Token 统计上代理,属于杀鸡用牛刀。
2.4 我最终的选择和理由
三条路比下来,我最终选了路线二——日志提取。理由有三个:
- 覆盖最全:底层 CLI 的所有调用都会写日志,不需要依赖调用方式的约束。
- 侵入性最小:只是被动读文件,完全不影响 Gemini CLI 本身的运行。
- 实现成本最低:一个 Python 脚本加一个定时任务就能搞定,不需要常驻复杂服务。
当然,光读日志还不够,日志只是原料,还需要一套解析、存储、汇总的加工流程。这就是后面要详细展开的核心内容。
3. 日志结构拆解:Token 数据藏在哪
3.1 日志目录与文件组织
Gemini CLI 的日志通常集中在用户主目录下的一个隐藏目录里(Windows 上是用户目录下对应的 AppData 路径),我用一个环境变量GEMINI_LOG_DIR来统一指定,方便测试时灵活切换。目录下会有多个日志文件,按天或按大小滚动切割,但核心的使用记录集中在名为usage.log或application.log的这类文件中,格式是每行一条 JSON。
这个组织方式意味着两个关键点。第一,日志会被自动轮转,旧文件可能会被清理或改名,所以统计脚本不能只盯一个固定文件名,需要能处理轮转场景。第二,不同版本的 CLI 可能调整日志目录的位置,脚本里最好把路径做成可配置的,不要硬编码。
3.2 一条关键日志的逐字段解读
用常见的默认配置为例,一条典型的请求日志是这样的 JSON 结构:
{ "timestamp": "2025-06-15T10:23:41.123Z", "level": "INFO", "requestId": "req_8f3a2b1c7d9e", "model": "gemini-2.0-flash", "action": "generate_content", "tokenUsage": { "inputTokenCount": 1523, "outputTokenCount": 842, "totalTokenCount": 2365 }, "context": { "cwd": "/home/dev/projects/blog", "sessionId": "session_01h3x..." } }逐个字段拆开看,最关键的永远是requestId,它是每条请求的唯一标识,后面做去重就靠它。timestamp是请求完成的时间,UTC 格式,用来做时间维度的聚合——注意是 UTC,如果你要看本地时间的日维度报表,需要做时区转换。model决定计费单价,不同模型的 token 单价差距可以到几十倍,这个字段必须保留。
tokenUsage里面三个数是核心中的核心:输入 token 数、输出 token 数和总数。输入和输出一定不能混在一起统计,有些模型输入和输出的单价完全不同。context.cwd是我最看重的附加信息,它记录了运行命令时的工作目录,有了它才能按项目聚合成本。这个字段在不同版本里可能叫cwd也可能叫workingDirectory,解析时最好两个都兼容。
3.3 为什么有的请求会漏记
日志提取方案虽然稳,但刚开始用的时候我也发现有些请求没有出现在统计里。排查之后发现原因不外乎三种。
第一种是日志缓冲未刷新。有些日志框架是异步写入的,请求结束后数据还在内存缓冲区里,脚本读取时正好赶上写入延迟。解决方法是读文件时稍微等一下,或者用带重试的读取逻辑。
第二种是模型预加载产生的消耗。某些模型在正式回答问题之前会先做一些内部处理,这部分在请求日志里的 tokenUsage 字段能看到,但个别版本的输出日志把这类记录写在了别的 action 下。如果你的业务对这一类消耗敏感,需要把 action 过滤条件放宽。
第三种是直接复用了本地缓存。多次问同一个 prompt,命中了上下文缓存,模型没有真正重新计算输出,对应的 token 消耗极低或者为 0。这种记录日志里有,但数值很小,如果不做特殊情况标记,汇总时会把真实成本拉低。
完整理解日志结构之后,实现一个可靠的统计脚本就只剩工作量问题了。
4. 落地实现:从零搭一套 Token 全局统计系统
4.1 准备环境
动手之前先列一下需要的工具和依赖。系统跑在主流 Linux 发行版或 macOS 上,Python 3.9+ 是最低要求,因为需要用到标准库里的sqlite3和pathlib,这两个在 3.9 以上都内置了,不需要额外安装第三方包。运行时我会用systemd服务来托管脚本,macOS 上则对应改成launchd的 plist 配置。
代码目录结构建议按下面这样组织,方便后续维护:
~/toolbox/token_stat/ ├── token_spy.py # 主监控脚本 ├── query.py # 查询与报表 ├── requirements.txt # 依赖(其实为空,注释说明用标准库) └── data/ └── usage.db # SQLite 数据库文件,自动创建这个结构不复杂,但把监控和查询分离是很重要的一个设计。监控脚本只负责把数据写进数据库,查询脚本只负责从数据库读数据生成报表,两者不耦合。这样你随时可以换监控实现方式,或者换报表生成工具,互不影响。
4.2 核心脚本:日志监控与解析
主监控脚本的核心逻辑是:持续追踪日志文件新增的行,从每一行 JSON 里提取 token 信息,写入数据库。完整的脚本框架如下:
import json import sqlite3 import time from pathlib import Path from datetime import datetime, timezone LOG_PATH = Path.home() / ".gemini" / "logs" / "usage.log" DB_PATH = Path.home() / "toolbox" / "token_stat" / "data" / "usage.db" SCHEMA = """ CREATE TABLE IF NOT EXISTS token_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, request_id TEXT UNIQUE, ts TEXT NOT NULL, model TEXT, action TEXT, input_tokens INTEGER, output_tokens INTEGER, total_tokens INTEGER, cwd TEXT, session_id TEXT ); """ def init_db(): db_path = DB_PATH.parent db_path.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(DB_PATH) conn.execute(SCHEMA) conn.commit() return conn def parse_line(line: str): try: data = json.loads(line) except json.JSONDecodeError: return None if "tokenUsage" not in data: return None tu = data.get("tokenUsage", {}) context = data.get("context", {}) return { "request_id": data.get("requestId", ""), "ts": data.get("timestamp", ""), "model": data.get("model", ""), "action": data.get("action", ""), "input_tokens": int(tu.get("inputTokenCount", 0)), "output_tokens": int(tu.get("outputTokenCount", 0)), "total_tokens": int(tu.get("totalTokenCount", 0)), "cwd": context.get("cwd", "") or context.get("workingDirectory", ""), "session_id": context.get("sessionId", ""), } def follow_file(path: Path): # 首次启动时先定位到文件末尾,只处理新增日志 f = open(path, "r", encoding="utf-8") f.seek(0, 2) while True: line = f.readline() if line.strip(): yield line else: f.flush() time.sleep(0.5) def main(): conn = init_db() # 用 INSERT OR IGNORE 利用 request_id 唯一约束天然去重 insert_sql = """ INSERT OR IGNORE INTO token_usage (request_id, ts, model, action, input_tokens, output_tokens, total_tokens, cwd, session_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """ for line in follow_file(LOG_PATH): item = parse_line(line) if item is None: continue try: conn.execute(insert_sql, ( item["request_id"], item["ts"], item["model"], item["action"], item["input_tokens"], item["output_tokens"], item["total_tokens"], item["cwd"], item["session_id"], )) conn.commit() except sqlite3.Error as e: print(f"[error] insert failed: {e}") if __name__ == "__main__": main()这段脚本有几个设计细节值得说明。
第一,INSERT OR IGNORE配合request_id的唯一约束,是从源头防重复的关键。日志可能会因为轮转、手动重放等原因被重复读取,但有了唯一约束,重跑脚本也不会产生重复数据。所以我前面再三强调requestId字段,它的价值在这里体现得淋漓尽致。
第二,follow_file首次定位到文件末尾,意味着启动脚本之前的旧日志不会被处理。这个设计是有意为之——监控脚本是常驻服务,历史数据可以通过一次性的回填脚本处理,平时运行只关心新产生的数据,避免每次重启都重复扫全量文件。
第三,没有用第三方库做实时追踪,而是老老实实用readline + sleep。很多人第一反应是tail -f,但 Python 里直接做文件尾随,这个朴素写法在处理一个持续追加的日志文件时是最稳定可靠的,不存在线程同步问题,也不依赖系统命令差异。
4.3 数据落库:从 CSV 到 SQLite
日志解析出来了,存到哪里?我建议直接用 SQLite,而不是 CSV。
CSV 的好处是简单、人类可读性强,但作为持续积累的统计数据,它有两个硬伤:一是并发写容易损坏,二是查询聚合能力太弱。SQLite 是一个单文件数据库,不需要额外部署服务,Python 内置支持,几百 MB 的数据量性能完全够用,而且支持完整的 SQL 查询,后续做日周月报表非常方便。
建表时需要注意几件事。request_id字段必须加 UNIQUE 约束,这既是去重机制也是数据完整性的保障。所有 token 字段都用 INTEGER,不用 TEXT,否则 SUM 聚合时会有一堆隐式类型转换的坑。ts字段建议直接存原始 ISO 字符串,查询时再用 SQLite 的datetime函数处理,不要存 Unix 时间戳——肉眼排查问题时 ISO 格式可读性更好,而且 SQLite 字符串比较在 ISO 格式下就是时间顺序比较。
如果你想要更精细的表结构设计,可以考虑把会话维度和请求维度分开,但就 Token 统计这个场景来说,单表完全够用,不需要过度建模。
4.4 查询与报表:让数据说话
数据库里有了数据,下一步就是把它变成能看的报表。我写了一个query.py,提供几个最核心的查询能力。
按天按模型汇总,回答“每天每个模型烧了多少”:
import sqlite3 from datetime import datetime, timedelta conn = sqlite3.connect("data/usage.db") # 最近 7 天按天按模型汇总 sql = """ SELECT substr(ts, 1, 10) AS day, model, SUM(input_tokens) AS input_tokens, SUM(output_tokens) AS output_tokens, SUM(total_tokens) AS total_tokens FROM token_usage WHERE ts >= ? GROUP BY day, model ORDER BY day DESC, total_tokens DESC; """按项目目录汇总,回答“哪个项目在烧钱”:
sql = """ SELECT cwd, COUNT(*) AS request_count, SUM(total_tokens) AS total_tokens FROM token_usage GROUP BY cwd ORDER BY total_tokens DESC LIMIT 20; """输出方式我一般用两种:终端直接打印表格,或者导出成 CSV 再丢进数据分析工具里。打印表格时用标准库的tabulate逻辑简单写一下就行,不必引第三方依赖。查询脚本和数据存储分离之后,你完全可以后续再写一个 Web 页面或者对接通知系统,比如每天定时发一条用量摘要到群里,这些都是锦上添花的事。
每日摘要推送是这套系统里我觉得实用价值最高的扩展。用一个定时作业在每天 9 点跑一个查询,把昨天的总消耗和环比数据打印出来,比月底看账单体感好太多。成本异常增长当天就能发现,不用等到月底肉疼。
4.5 自动化调度配置
脚本写好之后,需要在系统层面保证它 7x24 小时运行。Linux 上我用 systemd 服务来托管,配置如下:
[Unit] Description=Gemini Token Usage Monitor After=network.target [Service] ExecStart=/usr/bin/python3 /home/dev/toolbox/token_stat/token_spy.py WorkingDirectory=/home/dev/toolbox/token_stat Restart=always RestartSec=5 User=dev [Install] WantedBy=multi-user.target安装方式:
cp token-spy.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable token-spy systemctl --user start token-spy关键配置是Restart=always,保证脚本挂掉后 5 秒内自动拉起。服务以用户态运行,避免污染系统级服务,权限也更可控。
macOS 上对应的是 launchd,思路一样,只是配置格式从 ini 换成 plist。核心两处是KeepAlive设为 true 和RunAtLoad设为 true,保证开机自启、崩溃重启。
除了常驻监控,别忘了给历史数据做一个回填处理。首次部署时,日志目录里已经有大量未入库的历史记录,可以写一个一次性脚本,遍历日志目录所有文件逐行解析入库。注意回填时不要用“定位到文件末尾”的逻辑,要从头开始读,同时依靠 request_id 唯一约束自动跳过已入库的数据。回填之后,历史成本和当前成本就能无缝衔接了。
5. 实测数据与效果对比
5.1 一周实测下来能看到什么
我把这套系统跑了一周,数据积累之后,视角完全变了。之前我只有“今天用了不少”这种模糊感觉,现在我可以直接看到这周的总消耗是 2.8M token,其中 63% 是输入 token,37% 是输出 token——也就是说,成本大头在喂给模型的上下文上,而不是在模型生成的回复上。
按模型拆分更说明问题。我平时主要用两个模型:一个轻量模型跑日常问答,一个重量模型跑复杂推理。统计下来,重量模型的调用次数只占 14%,但消耗的 token 占总量的 51%。这就是我一直凭直觉想验证的事情:贵模型即使是少量调用,成本占比也远高于次数占比。有了这个数据,我就可以更理性地决定哪些任务必须用重模型,哪些可以降级用轻模型。
按项目维度看更是惊喜。我一直以为博客写作项目消耗最大,结果数据告诉我,排第一的其实是某个数据处理脚本——它在循环里批量调用了大量请求,每次的 token 不多,但累积起来非常可观。这种“长尾消耗”在缺乏统计时极难察觉,一旦暴露出来,优化方法立竿见影。
5.2 统计准确性验证
我对这套统计的准确性做过专门验证。方法是手动跑十次gemini run,每次记录终端显示的 token 数,然后对比数据库里的记录。十次全部匹配,总数分毫不差。这说明日志里的 tokenUsage 字段与 CLI 展示给用户的数据是同一来源,取日志数据就是取官方数据,不存在二次估算误差。
还有一次更严格的校验:把一个测试请求日志里的 tokenUsage 手动改大后重新导入,再用INSERT OR IGNORE重跑一遍,发现改动没有生效,因为 request_id 冲突被忽略了。这验证了去重逻辑的真实有效性,数据不会被重复写入污染。
这套验证体系也给了我信心:用日志做统计,数据源本身就是 CLI 的权威输出,不是估算值,这在成本核算场景里非常重要。报销、对账这种事,必须拿得出手。
6. 常见问题与排查心得
6.1 日志轮转导致漏统计
日志文件被轮转后,监控脚本的follow_file可能还在盯旧文件,新日志写到新文件里就漏掉了。这个问题我在实测第二周遇到过,表现为某天下午的统计突然消失了一截。
排查思路是看系统里的日志轮转配置。有的默认配置是按大小轮转,文件超过阈值就改名,新建一个同名文件。我的解决办法是升级follow_file逻辑,周期性地检查当前打开的文件和实际路径是否同一个 inode,如果变了就重新打开新文件。也可以用 inotify 这类文件系统事件机制去监听目录变化,但纯 Python 实现时会引入第三方依赖,我倾向于先用 inode 轮询这种朴素的方案。
6.2 Token 重复计数
重复计数的根源通常是监控脚本重启了多次,而每次启动都从头读取了一遍日志文件。由于首次启动定位到文件末尾,理论上不会读到旧数据;但如果你改了逻辑或者手动了文件位置,重复就会发生。
我的防线是双层的。第一层是前面反复提的request_id唯一约束,这是兜底;第二层是在脚本启动日志里记录启动时间和文件读取位置,出问题时可追溯。实际排查中,遇到重复计数最有效的 SQL 是:
SELECT request_id, COUNT(*) FROM token_usage GROUP BY request_id HAVING COUNT(*) > 1;如果查询结果为空,说明数据层面没有重复。
6.3 多终端不同步
Gemini CLI 如果同时在多台机器上使用,每台机器的日志是独立的,统计数据库也是独立的。这时候“全局”就不是单机的全局了。遇到这种情况,最朴素的方案是把每台机器当作独立数据源,各跑各的监控脚本,然后定期把 SQLite 文件同步到一台汇总机上合并。合并时依然靠 request_id 唯一约束去重,但要注意不同机器的 request_id 理论上不会冲突。更高阶的方案是统一把数据上报到一个汇总服务,但那就超出了本文“轻量自动化”的范畴了。
6.4 隐私与数据安全
日志里除了 token 数据,还包含请求的上下文信息,可能有敏感的业务内容。所以这个统计系统要刻意控制数据的留存范围。我的做法是在解析阶段就只提取统计需要的字段,丢弃完整请求体。SQLite 数据库文件的权限必须调整为仅当前用户可读写:
chmod 600 data/usage.db同样重要的是,日志原文件本身包含了请求内容,如果存放时间过长,泄露风险会累积。可以搭配一个定期清理任务,把超过 N 天的日志自动压缩归档或直接删除。成本统计只需要 token 数字,不需要原始请求文本,没必要把敏感数据一直留在磁盘上。
最后再分享一个小技巧
这套系统跑顺之后,我给自己加了一个“每日 9 点用量播报”的定时任务,把前一天的总消耗和本周趋势打印在终端顶部。做法很简单,就是在.bashrc里加一个启动时运行的命令,调用query.py的日汇总接口,输出格式控制在三行以内。
这个动作看起来不起眼,但它把成本从“月末惊吓”变成了“日常感知”。我现在基本不看账单,也不太担心突然出现超支。因为每天一睁眼,昨天的消耗就摆在面前。如果你想做得更精细,还可以给某个项目设定周预算,脚本检测到超支后直接在终端提醒。这些扩展都不需要改监控核心,只需要在查询层加逻辑就行。
用下来整体的感受是:Gemini CLI 本身把大模型的入口做得很轻,但“轻”也意味着很多东西不会帮你主动记账。Token 统计这件事,本质上是给 AI 工具补上一个成本侧的后视镜。日志解析这条路不华丽,但胜在稳定、准确、可维护,适合长期跑下去。希望这套方案能帮你把 Token 消耗这摊糊涂账,变成一笔明明白白的明白账。