在 ArkTS 上 1:1 移植最大正向匹配分词算法

发布时间:2026/7/24 1:13:41
在 ArkTS 上 1:1 移植最大正向匹配分词算法 B07 讲了词库怎么加载。词库装进内存之后核心问题是给定一段转写文本怎么找出里面的填充词、犹豫词、笼统词答案是经典的最大正向匹配Maximum Forward Matching分词。算法本身不难难的是需求里的四个字1:1 移植。原版产品一个 JS 实现已经在线上跑着用户对哪些词会被标出来有稳定预期。移植后如果同一个然后呢我觉得吧在旧版标 2 个词、新版标 3 个词那就是产品行为回归不是技术升级。所以这个任务的验收标准不是算法对了而是和原版机器行为一致——分词、命中、密度、建议全都一致。1. 最大正向匹配贪心但贪得有规矩算法主循环很短const MAX_LEN: number 6; // Fixed max match length; must stay 6 (oracle / ADR-003). export function segmentTextWithRanges(text: string, dict: Setstring): SpeakLabInternalToken[] { const tokens: SpeakLabInternalToken[] []; let i 0; const n text.length; while (i n) { let matched false; const remaining n - i; let len remaining MAX_LEN ? remaining : MAX_LEN; for (; len 2; len--) { // 从最长 6 往下试到 2 const word text.substring(i, i len); if (dict.has(word)) { tokens.push(new SpeakLabInternalToken(word, tokenIndex, i, len)); tokenIndex; i len; matched true; break; } } if (!matched) { // 单字不成词前进 1 个 code unit tokens.push(new SpeakLabInternalToken(text.substring(i, i 1), tokenIndex, i, 1)); tokenIndex; i 1; } } return tokens; }规矩有三条每条都是行为契约最长优先从min(剩余长度, 6)开始往下试到 2命中即取。这保证也就是说不会被切成也就是说——贪心方向决定分词结果。匹配下限是 2单字不参与词典匹配。这避免了的了这种单字被误标是原版定下的精度/召回平衡点。未命中前进 1 个 UTF-16 code unit——不是 1 个字符、不是 1 个字节。注释写死了JS string index behavior。emoji 在这里会被拆成高、低代理两个单 token——这看起来错但它是原版行为移植必须连这种错一起搬。B06 讲过公开层会把相邻未匹配代理对合并回 NORMAL 片段但那是呈现投影层的事算法层的 token 流必须和原版一致。MAX_LEN 6后面跟着注释must stay 6 (oracle / ADR-003)。为什么 6因为原版就是 6且原版词典最长词就是按这个上限设计的。改成 7 不会更好只会不一样——而不一样就是验收失败。这类常量的注释就是给后来的优化冲动打预防针。2. 双坐标tokenIndex 对内UTF-16 区间对外内部 token 带两套坐标export class SpeakLabInternalToken { text: string; tokenIndex: number; // 第几个 token —— oracle 对拍用 start: number; // UTF-16 起始 —— 公开命中区间 length: number; // UTF-16 长度 }为什么要两套因为两个消费方的需求不同对拍验证oracle按tokenIndex比对原版 JS 只输出 token 序列逐 index 比较文本是否一致是最严格也最直接的行为等价判据。UI 高亮按 UTF-16start/length消费B06 的渲染层拿区间去染色、做可点 span根本不关心 token 序号。还有一条不变式撑住这个设计公开片段的合并比如代理对合并、相邻普通片段拼接不改变 token 顺序、tokenIndex、命中和统计。算法产出是单一事实源公开呈现只是它的投影——投影可以变换源不能动。这和 B06 报告侧样式可丢内容不丢是同构的。3. 匹配字典的收录边界不是什么词表都进算法字典构建函数有一段醒目的注释/** * Build match dictionary: realtime 16/14/20 keys emotion keys when present overlay. * Must NOT include extensionCatalog / tiered / auxiliary tables. */参与实时匹配的只有三样实时词库的 16/14/20 三组词、情感词库可用时、用户自定义的内存填充词 overlay。扩展词表、分层候选库、各种辅助表——一律不进实时算法。这个边界值得专门立规矩的原因词典是分词算法的输入多收词不是覆盖更全而是改变分词行为。辅助表里的词一旦进字典原本切成 AB 的文本可能变成命中 C后续命中、密度、建议全链路偏移。这些词表有各自的消费场景比如报告期的候选建议但实时分词这个入口必须钉死。冻结决策里写明了这一点门禁脚本也会审计字典构建的来源。overlay 是纯内存的用户自定义填充词进字典参与匹配但不落盘进词库文件、不改统计口径——词库文件是冻结资产个性化是运行时叠加。4. 验证方法论Node oracle 对拍怎么证明1:1不是写一堆我觉得对的用例而是让原版代码自己当裁判。scripts/p2-lexicon-oracle.js的做法SHA 锁定原版文件。原版目录只读oracle 先校验两个源文件的 SHA-256漂移即退出const EXPECTED_LEXICON_SHA 10d9b06356982adb0c219841974eb25ea0be7e9a2455d5dd3eb93df42bfb1b99; function assertSha(filePath, expected, label) { const actual sha256File(filePath); if (actual ! expected) { console.error(JSON.stringify({ error: SOURCE_SHA_DRIFT, label, expected, actual })); process.exit(2); } }这一步防的是裁判自己变了原版文件如果被谁动了对拍结果就没有意义。内存 patch 导出绝不写原文件。原版lexicon.js没有导出内部的segmentTextoracle 在内存里把源码拼上一段测试专用导出再用vm编译执行const patched sourceBytes \n// P2-T02 oracle memory export only\n module.exports.segmentText segmentText;\n; // …Module.wrap(patched) vm.runInThisContext…原文件一个字节都不动——只读资产的纪律和对拍的需求同时满足。然后同一批用例含 emoji、长词边界、混合标点、30k 长文本喂给两侧原版 JS 的segmentText输出 token 序列ArkTS 实现输出 token 序列Hypium 侧 20 条用例同口径按tokenIndex逐个比对。机器 JSON 进 stdout不比对人眼读报告——一致必须是 diff 为空不是看起来一样。这套锁定裁判 → 内存改造 → 机器对拍的三件套适用任何移植已知正确实现的场景加密算法、协议解析、计价规则……凡是存在权威旧实现的都别靠重写者的自信验收。5. 小结最大正向匹配三条行为契约最长优先、匹配下限 2、未命中前进 1 个 UTF-16 code unitmaxLen6是冻结常量不是可调参数。双坐标分离tokenIndex 服务对拍、UTF-16 区间服务 UI公开投影可变换算法源数据不动。匹配字典边界冻结实时 16/14/20 emotion 内存 overlay辅助表一律不进词典即行为多收词就是改行为。1:1 移植的验收靠 oracle 对拍SHA 锁裁判、内存 patch 不写原文件、机器 diff 为空才算过。