返回 AICAP-180
B1 · Day 9evals 方法论 + tokenization 起步

全量编码 + token 报告

B1 tokenization 支线到今天才接上「为什么要自写 tokenizer」这个动机:token 数直接决定 OpenRouter 计费(cost ≈ token × 单价)。

阶段: B1 · evals 方法论 + tokenization 起步(Day 1-10) 标签: #tokenization #token-report #cost-estimation #no-api-key

今日导引(由浅入深)

B1 tokenization 支线到今天才接上「为什么要自写 tokenizer」这个动机:token 数直接决定 OpenRouter 计费(cost ≈ token × 单价)

Day 7 写了编码器、Day 8 训了 merge 表,今天把两者用起来——用自写 tokenizer 编码全部 29 个 eval prompt,导出每条 + 总计的 token 计量,即可在花一分钱前预估成本。

这是「implemented, not noted」的高光时刻:机制是代码、数字是报告,全程不需要任何 API key

最小可判定产出:29 prompt 的 token 报告,实测 totalChars=3536 → totalTokens=1760,meanCharsPerToken≈2.01,测试绿

明天 Day 10 把这个估算与真实 eval cost 并排,算偏差% 收口 B1。

1. 机理精读

token 计量是成本估算的前置,而成本估算是 eval 工程的现实约束。

跑一轮 agent eval 要对几十条任务各发请求、各拿几百 token 的输出,OpenRouter 按 input/output token 分别计价。

如果不知道每条任务多少 token,就只能跑完看账单——而跑完才知道贵,对迭代是灾难。先估后跑 才能做预算、做任务取舍、做「这条任务值不值得放进套件」的决策。

核心等式:cost ≈ token × 单价。

input token 用自写 tokenizer 在发请求前可精确算出(prompt 是已知的);output token 不可预估(模型生成多少由模型决定),只能用历史均值或上界估。

所以自写 tokenizer 解决的是等式里 input 那一半——已经能把「这套 29 任务的输入成本量级」算清楚。

token 报告输出什么。

本仓 tokenReport 输出每条一行(index / chars / tokens / charsPerToken)+ 总计(totalChars / totalTokens / meanCharsPerToken)。

meanCharsPerToken(c/t 比)是压缩率的直观度量:c/t 越高,单 token 装的字符越多,越省。

为什么本批 c/t 偏低(≈2.01)?

因为语料是英文 + JSON(eval prompt 里有结构化字段、标点、数字)。

JSON 的大括号、引号、逗号大量是单字节单 token,数字也常被细切,所以平均每 token 只装约 2 个字符。

c/t 比随语料而变——纯英文散文会更高(3-4+),中文按字节切会更低。这正是「不能用一个语料的 c/t 外推另一个」的原因。

关键边界:自写 tokenizer 的切分与模型官方 tokenizer(DeepSeek-V4 / Qwen3 各自的 tokenizer)不同。

算出的 token 数只是量级估算,不等于 OpenRouter 实际计费 token。两者差异是 Day 10 要量化的「偏差%」。

所以今天的 1760 是一个有用的估算锚点,不是计费真值。

2. 推导 / 手算 / 代码走读

代码日。Read src/agent/eval/tokenizer.tstokenReport,真实走读:

  • TokenReportRow(interface)index / chars / tokens / charsPerToken——每条 prompt 一行。
  • TokenReport(interface)rows[] + totalChars + totalTokens + meanCharsPerToken
  • tokenReport(texts, model)
    • rows = texts.map((t, index) => {...}):对每条文本 tokens = encode(t, model).length(用 Day 7 的 encode)、chars = t.lengthcharsPerToken = tokens ? chars / tokens : 0(防 0 除)。
    • totalChars = rows.reduce((s, r) => s + r.chars, 0)totalTokens 同理。
    • meanCharsPerToken = totalTokens ? totalChars / totalTokens : 0——注意是 total/total(加权),不是逐行 c/t 的平均,更准确反映整体压缩率。
  • 文件头注释:「used to measure tokens-per-task (→ cost) over the eval prompts WITHOUT any API key」——直接点明今天的动机。

手算验证 c/t(逐字对上 seed):

meanCharsPerToken = totalChars / totalTokens
                  = 3536 / 1760
                  = 2.0090…
                  ≈ 2.01 c/t

逐字对上 seed 的「meanCharsPerToken=2.009(≈2.01 c/t)」。

压缩有效的证据:totalTokens(1760) < totalChars(3536)——merge 把 3536 个字符压成了 1760 个 token,压缩比约 2:1。

测试走读tokenizer.test.ts 第 3 个用例 'compresses'):

  • const rep = tokenReport(corpus, model)(corpus = 29 个 prompt)。
  • expect(rep.rows.length).toBe(EVAL_TASKS.length)——29 行,逐条覆盖。
  • expect(rep.totalTokens).toBeGreaterThan(0)expect(rep.totalTokens).toBeLessThan(rep.totalChars)——压缩生效(1760 < 3536)。
  • expect(rep.meanCharsPerToken).toBeGreaterThan(1)——c/t > 1(实测 2.01)。

input 成本估算骨架(待 key 填单价):

cost_input估算 ≈ totalTokens(1760) × input单价(/Mtok) / 1e6

注意这只是 input 半边;output 半边不可预估,留 Day 10 与实测对齐时讨论。

3. 今日实战

照 seed:用 tokenizer.tstokenReport 编码全部 29 prompt,导出每条 token 数 + 总计,存为 token 报告(CSV 29 行 + 总计)。

  1. corpus = EVAL_TASKS.map(t => t.prompt)model = trainBpe(corpus, 200)
  2. const rep = tokenReport(corpus, model)
  3. rep.rows(index/chars/tokens/charsPerToken)导成 CSV 29 行 + 一行总计(totalChars=3536 / totalTokens=1760 / meanCharsPerToken=2.01)。
  4. 进 vitest 全量绿('compresses' 用例)。

4. 今日实测 / 产出

  • 已完成——实测 totalChars=3536 → totalTokens=1760,meanCharsPerToken=2.009(≈2.01 c/t),覆盖 29 prompt,测试绿。
  • 产出:token 报告(CSV 29 行 + 总计),全程无需 API key
  • 状态:已完成(非待跑/待建);与真实 eval cost 的偏差% 校验留 Day 10。

5. 常见误区 / 陷阱

  • 把自写 c/t 当 OpenRouter 计费真值:切分不同,1760 是量级估算,不是计费 token。Day 10 量化偏差。
  • 用逐行 c/t 平均代替 total/total:本仓 meanCharsPerToken 是加权(总字符/总 token),逐行平均会被短 prompt 拉偏。
  • 忽略 output token:cost = input + output token × 单价,自写 tokenizer 只覆盖 input;output 不可预估,别只报 input 就当总成本。
  • 拿英文+JSON 的 2.01 c/t 套别的语料:中文按字节切更低、英文散文更高,c/t 必须按语料实测。

6. 学习资源(每条带 YYYY-MM)

  • OpenRouter 计费文档(input/output token 分别计价,2026-06 当周重验)— cost ≈ token × 单价 的出处。
  • OpenAI tiktoken(byte-level BPE token 计量,2024 起维护)— 生产侧 token 计量参照,对照自写偏差。
  • Karpathy minbpe(Zero-to-Hero, 2024)— encode → token 计数的实现心智。
  • 本仓 src/agent/eval/tokenizer.ts tokenReport(2026-06,repo 内)— 真实 3536→1760、2.01 c/t 的产出代码 + tokenizer.test.ts 压缩断言。

SOTA检查 (2026-06 更新)

  • 当前主流:用模型自带 tokenizer 精确计费仍是生产标准;自写 tokenizer 仅作量级估算仍是有效的 no-key 成本预估手段
  • 过时黑名单避免用自写 c/t 精确等同 OpenRouter 计费 token——c/t 比随语料而变(此处英文+JSON 偏低 2.01),真实计费以模型自带 tokenizer 为准。
  • 下次复查点:OpenRouter 单价/模型在架状态执行当周重验;DeepSeek-V4-Flash / V4-Pro、Qwen3 的官方 tokenizer 若更新则估算偏差需重测。

衔接

  • 昨天:Day 8 — byte-level 编码器 (2)(merge 训练算法,vocab=456)。
  • 今天:用训好的表对 29 prompt 出 token 报告,实测 totalTokens=1760、≈2.01 c/t,no-key 成本估算锚点就位。
  • 明天:Day 10 — 双数对齐收口,把 token 估算(1760×单价)与真实 eval cost 并排算偏差%,合并 commit 收束 B1。