// 可插拔计数器:把「数 token」从「必须拥有我们的词表」降成一个函数。
//
// 为什么是结构体 + 闭包,而不是 trait:
// * 接外部 tokenizer 只需包一层闭包,不必给外部类型写 `impl`;
// * 核心包保持零第三方依赖,四后端一致;
// * `name` 与 `exact` 是报告的一部分——「这个数是估的还是精确的、谁算的」必须可追。
//
// 代价:`Counter` 不能 derive、不能序列化。它只需要「能数」。
///|
/// 计数器:给定任意字节序列,给出 token 数。
///
/// `exact=true` 表示逐 token 精确计数;`exact=false` 表示估算,
/// 报告会把它标出来,**估算不允许冒充精确**。
pub(all) struct Counter {
name : String
exact : Bool
count_bytes : (Bytes) -> Int64
}
///|
/// 数一段字节的 token 数。
pub fn Counter::count(self : Counter, bytes : Bytes) -> Int64 {
(self.count_bytes)(bytes)
}
///|
/// 字符数(Unicode 码点):按 UTF-8 起始字节计数,续字节 `10xxxxxx` 不计。
///
/// 非法 UTF-8 里的孤立续字节会被并入前一个字符,孤立起始字节各算一个字符;
/// 估算本来就是估算,不需要为此报错。
fn count_chars(bytes : Bytes) -> Int64 {
let mut n = 0L
for i = 0; i < bytes.length(); i = i + 1 {
if is_char_start(bytes.unsafe_get(i)) {
n = n + 1L
}
}
n
}
///|
/// 估算计数器:`chars_per_token` 个**字符**当 1 token(向上取整)。
///
/// 按字符而不是按字节:中文一个字 3 字节,按字节算会把预算放大 3 倍。
/// 只用于「还没有词表/不知道目标模型」的场合。`exact=false` 会写进报告。
pub fn Counter::estimate(chars_per_token : Int) -> Counter {
let cpt = if chars_per_token <= 0 { 4 } else { chars_per_token }
{
name: "estimate@" + cpt.to_string(),
exact: false,
count_bytes: fn(bytes) {
(count_chars(bytes) + cpt.to_int64() - 1L) / cpt.to_int64()
},
}
}