提示

返回博客列表

一个月账单多了四百块,我才知道 token 是怎么数的

月初看服务商账单,比上个月多了四百来块。

第一反应是被人刷了。客服那个 AI 兜底是九月初开的,量不大,一天二十次封顶,按理说怎么都花不到这个数。我把调用日志拉出来按天分组,没有哪天突然冒尖,也没有同一个身份连着刷的记录,每天都是平平的十几二十次。

那就不是被刷,是每次比我想的贵。

钱花在哪儿了

我原来对 token 的理解特别粗糙,基本当成"字数"。中文一个字算一个,一句三十字的话就是三十。这个理解用来估个大概还行,用来算钱就差得远了。

模型处理的不是字,是切好的片段。中文常见是一个字一个 token 左右,但不绝对;标点、数字、英文单词、代码里的符号,切法都不一样。同一句"差旅住宿标准是多少",不同模型切出来的段数能差好几个。真要准,得用对应模型的计数器:

import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
print(len(enc.encode("差旅住宿标准是多少")))

不过这库主要对得上 OpenAI 那几家的切法,换别家只能估。我后来干脆不纠结精确值,改成在日志里记服务商返回的 usage,那个才是最后按它收钱的数。

真正让我意外的是第二件事:模型没有记忆。

你跟它聊到第五轮,它并不是"接着上一轮想",而是把前四轮的问答、加上系统提示词、加上这一轮的新问题,从头到尾重新发一遍。所以第五轮的输入,不是"一个问题",是"前四轮 + 新问题 + 系统提示词"。

我们那段代码里其实已经做了限制:

for item in (history or [])[-6:]:
    role = item.get('role')
    content = (item.get('content') or '').strip()
    if role in ('user', 'assistant') and content:
        messages.append({'role': role, 'content': content[:500]})
messages.append({'role': 'user', 'content': user_content[:1000]})

只带最近 6 条,每条裁到 500 字,当前问题裁到 1000 字。当时写这个是怕上下文太长拖慢响应,附带也把成本压住了。但"压住"不等于"没有"。

把一次典型的对话拆开看:

组成部分 大致 token
系统提示词(5 条规则) 280–320
最近 6 条历史 500–900
当前问题 30–80
输出(max_tokens=400) 100–400

一轮下来一千五上下,其中真正属于"用户这次问了什么"的,不到 5%。剩下全是为了让模型知道前面聊过啥而重复交的钱。

聊得越久,这个比例越难看。前十轮的每一轮都要把前面九轮再发一次,累计输入量差不多是按轮数的平方在涨。我以前只知道长对话会变慢,没把"慢"和"贵"联系到同一件事上。

还有个我一开始理解错的地方:max_tokens=400 不是"最多花 400 token 的钱"。它只限制输出长度,输入该多少还是多少;而且不同服务商对超长输出是截断还是报错也不一致。真要控成本,得从输入侧下手。

后来改的三件事

第一件是把 usage 记进日志。之前只记了"这次调用成功/失败",看不出贵在哪。加上 prompt_tokens、completion_tokens 之后,第二天就能画出一句话成本随轮次变化的曲线,比我拍脑袋猜准得多。

第二件是日限额。站里原本就有:

AI_CHAT_DAILY_LIMIT = 20

按「用户 id 或游客 token + 日期」在缓存里计数,超了就静默降级成普通留言。这条当初是防被刷的,现在看更像是防我自己算错账——至少天花板是硬的。

第三件是尽量让前缀稳定。有些服务商对完全相同的提示词前缀有缓存价,命中了能便宜不少。麻烦在于前缀必须从第一个 token 就一模一样,系统提示词里但凡夹个时间戳、随机会话号,缓存就全废。我们把提示词固定成常量,动态信息一律往后放,这条改动没动任何业务逻辑,账单上却能看出来。

顺带说,客服在线的时候我们压根不调 AI,这条也在省钱,只是当初写它纯粹是因为"人正在回,别让机器人抢话"。

没算清的部分

到现在我也没把那四百块精确对回去。日志是月中才加 usage 的,前半个月的调用只有成功失败,没有 token 数,只能按后半个月的均值倒推,误差不小。真要复盘得从一开始就埋点,这个是教训。

另外还有个口子没堵:用户可以一次贴一大段文字进来。user_content[:1000] 裁的是字符数,中文一千字差不多就是一千多个 token,比正常问题贵十几倍。目前靠 3 秒发送频控和每日 20 次限额挡着,谈不上精细。

月中加 usage 之前,我还试过拿字符数硬估。中文按 1 字 1.5 token 算,跟实际能对到八成;碰上用户把报错日志整段贴进来就完全不准了,英文单词、路径里的斜杠、大括号,切法跟中文根本不是一回事。有一回一段 800 字的报错,我估 1200 token,账单上是 2100 多。

财务那边也问过一次,说这四百多块记到哪个科目、能不能按部门分摊。我说分不了,全站共用一个 key,日志里那时候连调用点都没打标签。后来给每个入口加了个 source 字段,虽然到现在也没真去分摊过,但至少下次问起来能查。

还有个坑是提示词缓存。有些服务商对完全相同的前缀给折扣价,命中了能省一截。我们第一版在系统提示词末尾塞了句「当前时间」,本意是让模型知道活动日期,结果每一次请求前缀都不一样,缓存一次没命中过。把时间挪到用户消息那一侧之后,命中率才上来。这类省钱手段不看 usage 明细根本发现不了,你只会觉得这个月怎么又贵了。

这篇记在 VidDown 技术博客,站上那些免费工具的账也是这么一笔笔算的。

现在我每周看一次用量曲线,比看月度账单勤多了。账单那东西到手的时候,钱已经花完了。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部