上个月底我想算一笔账:WorkBuddy 这一个月到底吃了我多少 token。打开界面,能看到的只有两样东西——当前会话的上下文占用,写着「476.7k / 1.00M,47.7%」,以及右上角一个孤零零的积分数字。

我想知道的不是这个——上周三为什么烧得特别快,想知道思考在输出里占了多少,想知道那个 98% 的缓存命中是真的还是我自我安慰。它一个都答不上来,因为它压根没打算记这些——界面上翻不到昨天,更别说上个月。

也不能说它做得差。界面回答的是「现在够不够用」,我想问的是「过去怎么花的」,这两个问题长得像,其实要的东西完全不同。前一个问题它答得挺好,后一个它连历史都没存。

那就自己动手。中间试了三个现成方案,全没跑通,最后写了个单文件 exe,双击就能看。这篇记录整个过程:数据藏在哪、现成的为什么不能用、自己写要注意什么、以及怎么证明它没在骗我,最后那部分是我花时间比较多的地方,因为看板这东西特别容易自己骗自己。

先说结论

  • 数据一直都在本地硬盘上,每次请求的完整响应都落了盘,带着 usage 字段,只是没有界面去读它。
  • 现成的三个方向各有各的墙:官方的要企业管理员权限,开源的要你对得上它的数据格式,同门产品只认它自己那个目录,我是逐个装上试过才放弃的。
  • 真正麻烦的不是画图,画柱状图半天就够,难的是让它在 WorkBuddy 下次改版本的时候别整片散架。
  • 思考过程看不到,只能看到它花了多少,一千四百多条响应我全扫了,一条 reasoning_content 都没有。
  • 做完之后我改了自己的用法,以前觉得「上下文快满了」是个模糊感觉,现在能一眼看出哪几次缓存命中率掉得厉害,回头看都是我把话题扯太远的那几次。

数据其实一直在硬盘上

动手之前我先干了一件事——把 ~/.workbuddy 整个翻了一遍,不是翻文档,是翻文件,因为文档写的是功能,文件里才是真相。

结果比我想的干净。每一次模型调用,完整响应都写在 traces/<pid>/trace_*.jsonl 里,一行一个 span。工具调用、流式输出、最后的 API 响应全在里面,不是摘要,是原文。

顺手记一下结构,万一你也要读:每个 span_end 行里有个 generation 类型的 span。它的 toolOutput 是一段被转义过的 JSON 字符串,解析出来是个数组,数组第一项就是那次请求的响应体。

{
  "id": "chatcmpl-...",
  "model": "deepseek-v4.1-flash",
  "usage": {
    "prompt_tokens": 78234,
    "completion_tokens": 912,
    "total_tokens": 79146,
    "prompt_tokens_details": { "cached_tokens": 76700 },
    "completion_tokens_details": { "reasoning_tokens": 486 }
  }
}

就这些——cached_tokens 告诉你多少输入复用了前缀缓存,reasoning_tokens 告诉你模型在思考上花了多少输出,有这两个数,一个看板要的东西基本就齐了,剩下的都是排列组合。

另一处是 workbuddy.db,里面的 session_usage 表存了每个会话的上下文占用、轮数和一个 credit_json。界面右上角那个积分就是从这儿来的,它按会话记账,没有按天的明细。

跑到今天,这台机器上已经是一千九百二十九次请求、三亿三千万输入、一百五十六万输出。其中思考占了五成左右。

命令行执行 WorkBuddy 用量看板 --once 后的输出,显示 1929 次请求、三亿三千多万输入 token、一百五十六万输出 token
不带界面跑一次,直接把这些数字打到终端

找到这儿的时候我有点想笑。我一开始的判断是「它不给你历史」——实际上历史一条不少,只是没人把它读出来。

缺的是一个读取器,不是一个记录器,这两件事的工作量差得很远,前者我周末就能弄完。

现成的三个方案,一个都没成

在动手写之前,我先确认了市面上真没有能直接用的。不是没搜,是搜完之后逐个装上试,三个方向,三种墙。

方向能看什么卡在哪
WorkBuddy 企业版可观测中心实时观测、会话追踪、积分按部门和模型拆解要企业管理员权限,个人版进不去
ccusage(开源)支持十几种 Agent CLI,有热力图、按会话、按模型的统计读不进来,格式校验比文档里写的严
codebuddy-stats(npm cbs)TUI 界面、成本热力图、缓存命中率写死读 ~/.codebuddy/projects/,这台机器上那个目录是空的

企业版那个是唯一真正意义上的官方看板——2026 年 10 月才开始公测。如果你们单位开通了,用它就行,别自己造,一套东西维护起来是要花时间的。我这边是个人版,所以这条路直接断了。

ccusage 是离得比较近的一个,我甚至写了个转换器。它能用 CLAUDE_CONFIG_DIR 指定数据目录,读的是 Claude Code 那种 JSONL,那我就把 trace 里的 usage 映射成它的字段:prompt_tokens 减去缓存的部分算新增输入,缓存那部分单独一个字段。推理 token 塞进 thinking。

两种放法我都试了——自定义目录,和直接铺到它默认的 ~/.claude/projects/ 下面,都返回 No usage data found。它的格式校验比 README 里写的严格,而且内置定价表全是 Claude 和 GPT 的单价,deepseek 的价格对不上,硬塞进去算出来的钱是错的。

到这一步我就没往下磨了。因为就算格式骗过去了,价格还是错的,一个价格错位的成本看板对我来说没什么意义,还不如自己算 token 数。

顺便说,测试时建的 ~/.claude 我删掉了,那会儿该目录本来不存在,是本次测试新建的,不清掉的话以后自己都分不清它是哪个工具留下的。

codebuddy-stats 更可惜,它长得挺像我要的——终端里跑,有热力图,能按项目拆。

但它把路径写死了,同门产品不等于同门数据目录,它要的那个目录在这台机器上是空的,界面进去一片零,连报错都没有。

那就自己写一个

写的时候我给自己定了一条规矩,后来发现这是整套东西里唯一真正重要的决定:只有一个目录允许知道 WorkBuddy 的存在。

原因很实际。这类工具最大的风险不是写不出来,是上游改一版就全废,而 trace 那个格式属于内部实现,说变就变,没有任何承诺。

如果解析逻辑散在采集、统计、界面三个地方,一次改版我要改三处,还特别容易漏掉其中一处。

层它知道什么上游改了要动它吗
adapters/trace 长什么样、数据库有哪些表要,而且只动这里
contracts只有一个「一条用量记录」的定义不动
core/去重、聚合、健康状态不动
web/怎么画不动

于是核心层从头到尾不知道「WorkBuddy」这个词,它收到的就是一批标准化记录——什么时候、哪个模型、输入多少、缓存多少、输出多少、思考多少,谁产的它不关心。

这条规矩真正派上用场,是在我写验证脚本的时候。我造了一个纯假的数据源塞进去,界面一行代码没改,照样出图。如果换个数据源还要动前端,那说明分层只是画在文档上的。

采集是增量读的——trace 是追加写,所以只记一个字节偏移量,每次从上次读完的位置接着读,不用整文件重扫,文件长到几十兆也不影响速度,因为它只碰新长出来的那一截。

wb-usage-monitor/
├── config.py            所有路径、端口、字段映射
├── contracts.py         数据契约:一条记录长什么样
├── adapters/
│   ├── trace_adapter.py     ← 唯一认识 trace 格式的地方
│   └── sqlite_adapter.py    ← 唯一认识数据库表结构的地方
├── core/
│   ├── store.py         落库、水位、聚合
│   └── engine.py        调度、健康状态、广播
└── web/index.html       看板(手写 SVG,不依赖 CDN)

刷新走 SSE,服务端一有增量就推,页面不用轮。这条链路实测是追加记录之后零点八到一秒,页面上就能看到,不用手动刷新。后来还补了一道兜底:超过十二秒没收到任何消息,页面自己去拉一次快照。

WorkBuddy 用量看板整页截图,顶部是 token 总量与缓存占比,中间是近七天消耗趋势,下面是模型分布与会话消耗排行
整页长这样,图里的数据是演示模式生成的,不含真实会话记录

打包成单文件 exe,八点八兆,双击跑起来会自己开浏览器。

做成 exe 而不是让你装 Python,是因为我先发的是源码版,收到的第一句反馈是「执行 Python 代码后没有任何输出」。查下来是三件事叠在一起——输出被缓冲区吞掉、启动后不自动开浏览器、双击 .py 报错时窗口一闪而过。

这三件事单看都不算 bug,凑在一起的效果就是「程序没反应」。后来所有给用户看的输出都强制刷一次缓冲,启动自己开浏览器,出错时用 input 把窗口挂住,至少让人能看见报错。

几个把我耗到半夜的坑

写代码本身没花多少时间,卡住的都是环境。留个记录,省得下次再撞一遍。

第一个:8787 端口根本不清净。我一开始判断这个端口空闲,理由是「没人监听」。后来加了端口冲突处理才发现,占着它的是 WeChatAppEx.exe,微信的小程序组件,而我之前那句「空闲」是错的,错得还没有任何提示,只表现为偶尔启动失败。

第二个:Windows 上的端口探测不能用 bind。我写的探测逻辑是「试着 bind 一下,成功就是空闲」——这行代码在 Linux 上是对的,在 Windows 上是错的。问题出在 SO_REUSEADDR,它在那里允许重复绑定同一个端口,于是明明被占用,bind 也返回成功。

两个进程会真的去抢同一个端口,然后其中一个莫名其妙收不到请求。

改成用 connect 探测之后,又炸出第三个问题:为了让重启更快,我把端口复用关了,结果强杀进程后端口进入 TIME_WAIT,连续重启时端口一路往后跳,8877、8878、8879,每次重启地址都不一样,之前存的书签全废。

最后的解法是分两种情况:先 connect 一下看有没有实例在跑,有就直接复用那个地址,顺便把单实例也实现了,双击两次不会开两个服务。没有的情况才允许复用绑定,用来覆盖 TIME_WAIT 的残留。

现在连续重启三次,端口稳稳不动。

第四个:PyInstaller 的 __file__ 指向解压目录。源码跑的时候网页文件在相对路径上找得到,打成 exe 之后 __file__ 指向临时解压目录 _MEIPASS,静态资源全 404,页面一片白,而且控制台不报错,你只会看到浏览器里一个空页面。

第五个:控制台中文乱码。Windows 控制台默认 GBK,我那些带中文的启动横幅全变成问号,得在启动时把代码页切到 65001,再把标准输出重新包一层 UTF-8。这问题不影响功能,但用户第一眼看到的就是乱码,印象基本就没了。

第六个特别隐蔽:截图验证不了「页面会自己刷新」。我想截个图证明 SSE 生效,结果 headless Chrome 一直等不到「网络空闲」。因为 SSE 是长连接,它永远不会空闲,--screenshot 直接不产出文件,也不报错。

一个不报错的失败,是最容易骗过自己的东西,我一开始还以为是自己路径写错了,反复改了三四遍才发现方向根本不对。后来改用 DevTools 协议直接读页面里的 DOM,同一个页面不刷新,追加一条记录,看数字变不变。

这个办法还有个额外好处:能顺手验证兜底逻辑。把 SSE 请求整个拦掉之后,页面在十一秒左右靠轮询把新数据补上了,跟设计时估的时间差不多,算是把「断线了怎么办」也验了一遍。

怎么证明它没在骗我

看板这类工具麻烦的地方在于:数字错了你也不知道。一根柱子画歪了,你会以为是那天的数据真的低;一个字段取错,曲线会变得很平滑很好看,看不出异常。所以我一开始就把验证当成功能的一部分写,不是写完再补。

每个用例都写死判定标准,跑完直接输出 PASS 或 FAIL 和实际数值,不靠人眼看。现在一共十一项,前九项是核心性质,后两项是后来加功能时补的。

#验什么判定
V1数据完整性和独立重算的总量逐项比对,差 0
V2幂等重复采三次、重开五次,条数不变
V3实时延迟P50 397ms,P95 602ms
V4断点续采中途强杀,重启后不漏不重
V5格式漂移改字段名、写半行、删表、删目录,都不崩
V6资源占用CPU 3.7%,内存 33MB
V8只读安全采五十个文件,指纹零变动
V9可插拔换假数据源,核心与界面零改动
V10时间分桶四种粒度合计与独立重算一致
V11会话检索翻页不重不漏,通配符已转义

V9 是整套东西的验收点,不是凑数的。如果换数据源还要改界面,这套分层就是假的,代码写得再整洁也没用。

同理还有 V5:我故意把 trace 里的 prompt_tokens 改名成 prompt_tokens_v2,程序不能崩,只能把这一项降级,其余面板继续显示最后一次有效值。

V8 那条我多说一句。我读的是 WorkBuddy 自己的数据文件,如果读的时候把它锁住了,或者顺手往里写了什么,那就成了「看板把工具搞坏」的经典事故。所以采集全程只读打开,前后对五十一个文件做指纹比对,零变动才算过。

也有不体面的地方。第七项「长稳」我只跑了六十秒,内存增长 0%,但这说明不了八小时之后不涨。表格里写的是实测值,没测的就是没测,我不想拿六十秒的结论冒充八小时,那属于自己给自己发合格证。

还有一件更尴尬的事:验证脚本自己出过两次错。V3 最初测出 P50 五千多毫秒,我差点以为是采集太慢,查下来是去重键写错,延迟被累积了;V5 一开始 FAIL,是几个子用例共用同一个目录互相污染。

修完才是上面那些数字,第一版是错的,我没拿它交差。这件事让我后来养成一个习惯——验证失败的时候,先怀疑验证本身,尤其是当失败结论「特别符合预期」的时候。

后补的三样东西

第一版只有一张每日消耗柱状图和一张会话表,用了几天发现不够用,又补了三样。

一是时间粒度。只能看最近几天,没法回答「这个月比上个月多花了多少」,所以加了四个档位:近七天按天、近四周按周、近十二个月按月、近三年按年。切档的时候趋势图、上面几个数字、模型分布一起变,不会出现图是七天、模型却是全部历史这种自相矛盾。

这里有个细节我纠结了一会儿:没有数据的那几天要不要留着。留着会出现一根零高的柱子,不好看,但删掉更糟——如果停用了一周,图上看不出断层,会让人以为你天天都在用。

二是单位切换。M、K、原始值三档,看着像小事,实际是我自己点得挺多的按钮:看总量用 M,对比两个小模型时要切原始值,不然它们都显示成 0.00M,看着像没数据。

三是会话检索。会话表原来只显示积分最高的二十个,这意味着消耗小的那些永远看不到,而「那个只用过两次的会话到底花了我多少」——恰恰是我偶尔会问的问题。

改成服务端检索之后,输入名字就能翻到全部历史,哪怕它只有零点几积分。

做这个的时候顺手改了个底层行为:原来每次采集都是清空会话表再重写,上游一旦把某个会话关掉,这边的记录也跟着消失。改成只累积不删除,关掉的会话标一个离线,用灰点画出来,记录永远留着。

积分那条线单独说一句:它没法按时间范围拆——积分是按会话记的账,没有按天的明细,所以那张卡固定显示全部会话累计,界面上也这么标着。

我知道这看着和别的不一致,但硬拆出来的数字是假的,不如说清楚。

它做不到的事

用了这些天,有几件事是明确做不到的,写出来免得你抱着错误的期待装上,然后回来骂我。

看不到思考内容,只能看到思考花了多少。我把当时能扫到的一千四百七十一条响应全翻了一遍,reasoning_content 一条都没有,只有 reasoning_tokens 那个计数——模型确实在思考,网关也统计了思考消耗,但思考文本在中间层就被剥掉了。

所以看板上只能写「思考占输出 52%」,写不出它想了什么。

实时性的上限不在我这边。采集侧是秒级的,但 WorkBuddy 不是每条请求写完立刻落盘,它有自己的批量写入节奏——所以「页面停在一个时间点很久」有两种可能——上游还没写,或者页面没刷新。右上角的状态灯和时间戳能分开这两种情况。

不跨机器。数据不进云端,每台机器只看自己那份,想两台机器合起来看——就得自己想办法搬 trace 文件。这是我故意选的:一旦涉及上传,这东西就不该随便发给别人用。

会话历史从它第一次运行开始攒。更早的会话如果 WorkBuddy 自己已经清掉了,这边也没法凭空补出来,这一点绕不过去,只能早装。我装得不算早,所以最早那批会话的积分是缺的。

想试的话,从 --probe 开始

单文件 exe,八点八兆,Windows 十或十一的 64 位,双击就能跑。前提只有一条——这台机器装过并用过 WorkBuddy,%USERPROFILE%\.workbuddy\ 那个目录在。不需要 Python,不需要联网,也不需要 WorkBuddy 当前开着。

第一次跑会全量扫一遍历史,几秒到几十秒,看数据量。如果窗口一闪就没,别急着说坏了,在命令行里跑一次 --probe,它会告诉你两个数据源各自能不能读到,报错也看得见。这个参数就是我被「双击没反应」投诉之后加的。

我现在的用法很简单——每天收工前开一次,看当天总消耗和缓存命中率。命中率掉到九成以下的,就去翻那天的会话,基本都能找到话题扯太远的那一次,比起盯总量,这个指标更能说明问题。

代码是 MIT,随便改。只有一点想提醒:如果你要加功能,别往 adapters/ 以外的目录写 WorkBuddy 的字段名。那条线守住了,下次它改版本你只改一个文件;守不住,你会花一整个下午在四个文件里找哪个字段对不上。

项目在这两个地址,仓库里带全部源码和验证脚本:

另外,如果你们单位开了 WorkBuddy 企业版,先去管理后台看一眼有没有可观测中心。有的话用它,那才是正路,我这个是给没有的人准备的。

如果你觉得这篇文章不错,有用的话可以点击下面的“赞赏”支持作者,你的支持是我更新的动力(饿饿吃饭)。

  • reward_image1
  • reward_image2
欢迎来到XiaoZou123,这里是一个电脑极客、数码爱好者网站。我平时喜欢关注数码新闻,研究计算机技术。如果你看我头像觉得我是二次元,那我其实还算不上!
最后更新于 2026-11-06