关键词先摆在前面内容质量评分、token 与字符的区别、中文分词、实体提取、结构判据、单元测试反例。这篇文章记录的是我们在一个内容质量评分服务里的真实排查过程十几条把文章对 AI 抓取是否友好翻译成可计算指标的结构判据中有两处被证实失效——一处把字符当 token 用first200TokensCheck 字段实际执行 plainText.slice(0, 200)一处中文实体提取的过滤条件恒假实体恒为空匹配度恒等于 0.5。两处的共同特征是代码能跑、不报错、返回值结构完全正确而且没有任何单元测试覆盖。以下按现象 → 根因 → 为什么没被发现 → 修法取舍 → 检查清单的顺序展开。一、背景结构判据体系是干什么的这个评分服务的目标是把AI 友好这种无法执行的模糊目标翻译成能在文本上直接计算的判据。判据分四类类别检查内容典型规则位置类结论出现在哪里首段 ≤150 字、H2 区块首句即结论、开篇窗口含关键信息形态类内容的组织形式问答块FAQ格式、表格、问句标题密度类关键内容的出现频率核心词频期望值、标题实体在首段的覆盖率可信度类可验证引证来源标注、更新时间需要坦白这是v0 版本的启发式规则价值在于把模糊目标变成可计算指标代价是判据偏机械、会误伤上线后必须真实样本回归。下面两个缺陷正是这个土壤里长出来的。二、缺陷一first200TokensCheck 切的是字符不是 token字段名与实际实现代码节选已脱敏constfirst200plainText.slice(0,200);plainText 是去掉标签与空白后的纯文本slice(0, 200) 取前 200 个字符。但字段名叫 token词元。字符数与 token 数不是 1:1且比例随语言剧烈变化200 个英文字符大约只有三十几个单词往往还没读完第一句200 个汉字已经是完整的一到两段。同一个开篇是否包含关键信息的判据在中文内容和英文内容上实际检查的文本范围完全不同两类内容的得分放进同一个分数体系没有可比性。这个缺陷最麻烦的地方是它不产生任何异常信号返回对象结构正确、字段齐全测试断言返回值里有这个字段也通过。根因是命名与实现不符——写代码的人当时可能知道这是有意的近似但这个决定没有留下注释或命名痕迹。命名撒谎之后所有后来读代码的人包括作者自己都会按命名理解语义。三、缺陷二单字拆分后再按长度过滤条件恒假第二个判据计算标题问句的关键实体在首段的覆盖率实现分三步节选consttitleEntitiestitle.split()// 第一步按单字拆分.filter((ch)合法字符!疑问词.has(ch))// 第二步过滤疑问词与非法字符.filter((w)w.length2||/\d/.test(w));// 第三步保留长度 2 或数字手工推演数据流split(‘’) 之后每个元素长度恒为 1第三步要求长度大于等于 2——对一个全是单字符的数组这个条件永远为假。中文实体被全部清空只剩能通过数字正则的单个数字字符。用真实标题实测node 直接运行这段逻辑标题: 洛阳装修公司怎么选 filter 后实体: [] 走未提取到关键实体分支matchScore 固定返回 0.5 标题: 2026年装修报价怎么算 实体: [2,0,6]两个结果都意味着判据死了第一个说明所有纯中文标题的匹配度都是常数 0.5信息量为零第二个说明它认为这句话的关键实体是 2、0、6 三个字符覆盖率毫无意义。两个缺陷的根因是同一个中文没有空格分词不能套用英文按词切分tokenization的隐含假设。正确做法要么接入中文分词库要么改用 n-gram二元/三元语法单元共现而不是手工维护疑问词表再按字符切。四、为什么没有任何东西拦住它先说一个更根本的事实这两个判据一个测试用例都没有。服务确实有单元测试但不到十条用例里有断言价值的覆盖的是标题缺核心词、核心词堆砌、标题缺需求动作词、空内容。开篇窗口和实体覆盖率这两处没有单独断言只是作为总分的一部分被间接跑过。但补测试不等于解决问题。假设补这样一条测试constscorematchScore(某某怎么选);expect(score).toBeGreaterThanOrEqual(0);expect(score).toBeLessThanOrEqual(1);这条断言在缺陷存在时依然通过——固定返回的 0.5 确实落在 0 到 1 之间。测试通过判据失效两者可以同时为真。区别在于这类断言检查的是代码正确性给定输入返回预期结构而判据需要的是判据正确性对内容的判断是否成立两者不能互相替代。由此得到两条通用于判据类代码的规则判据测试必须正例与反例成对只写优质内容得高分发现不了所有内容都得同一个分必须写一条明显不合格的内容断言它确实被扣分。反例夹具不应只由判据作者自己写作者构造的反例只有他想象得到的形态。这两个缺陷能上线很大程度因为写夹具的人和写判据的人是同一个人。五、修法与取舍截至写作时均未修复先说明状态这两处还没有修以下是方案与取舍不是已上线的结果。第一处把单位定死。方向一接入 tokenizer 真按 token 切——准确但多一个依赖且不同模型分词结果不同“按哪个模型算本身又变成新问题。方向二改按句切分取开篇前 N 个句子——句子边界语言无关代价是句长浮动。我们倾向方向二这个判据的语义本来就是开篇有没有把话说清楚”句子比 token 更贴近语义。无论选哪个第一步都是把单位写进命名。第二处换掉切分方式。接入中文分词库如 jieba 类方案实体提取基于词而非字或改用 n-gram 共现计算标题与首段的重合度不依赖分词器。同时手工疑问词表应换成按词性过滤——手写词表的问题不是不全而是永远不知道漏了什么它不报错只会静默地少过滤。修复后盯什么指标分数分布。修复前纯中文标题的匹配度全部集中在 0.5修复后分布才真正拉开。分数不分化通常是判据失效的重要征兆——某个维度上绝大多数样本落在同一个值附近时先别调阈值先查判据是不是根本没在算东西。六、七条可复用检查清单单位写进命名——每个判据的单位字符 / token / 句 / 词必须出现在字段名里禁止暂时近似语言无关性——中文无空格分词是否被特殊处理按词切分的假设从哪来恒真恒假扫描——是否存在恒真或恒假的过滤条件典型先按单字拆分、再按长度过滤聚合语义——some 还是 every与门禁强度是否匹配正反例成对——测试必须包含正例与必须被扣分的反例夹具换人——反例夹具由非判据作者提供分布监控——上线后监控分数分布不分化即判据失效其中 1、3 直接来自本文两个缺陷5、6 来自零覆盖且测试也抓不到的教训7 是最便宜的长期报警手段。作者注本文基于线上服务的真实代码与实测输出改写代码已脱敏部分变量名做了泛化处理。—— 逐曜AI