揭秘 Laya 决策模型内部机制:[MASK] 标记、序列布局与 head_max_len 截断如何协同工作
揭秘 Laya 决策模型内部机制:[MASK] 标记、序列布局与 head_max_len 截断如何协同工作
Laya 是一个开源的 System-1 决策模型(Jev 兼容):它不生成文本,而是接收一个状态(工单、邮件或 JSON 对象)和若干结构化问题,通过 ONNX Runtime 在 Node.js / TypeScript 环境中一次前向推理,返回每个选项的校准概率。本文将带你深入 Laya 的 token 级实现:[MASK] 标记如何充当"答案锚点"、序列如何布局、head_max_len 截断如何分配 192 个 token 的预算。
🧩 先认识 Laya:不做文本生成的"决策模型"
与 LLM 不同,Laya 的输出不是文字,而是概率。支持三种问题类型:
| 问题类型 | 用途 | 输出示例 |
|---|---|---|
choice |
从多个选项中选一个 | 每个选项的概率 + 最佳选择 |
score |
有序量表打分 | 期望等级(如 1.3886 / 0~3)+ 分布 |
noul |
是/否陈述 | 校准的 P(true)(如 0.0988) |
底层是 ModernBERT-large 编码器 + 决策头(英文检查点约 4.21 亿参数)。本项目将其导出为单个 ONNX 图,运行时不需要 PyTorch 和 Python,输出与 Python 参考实现一致到四位小数。完整调用示例见 examples/basic.ts。
📍 [MASK] 标记:每个选项背后的"答案锚点"
为什么是"填空"而不是"生成"
Laya 采用"填空式"打分:序列中每个选项前都放一个 [MASK] 特殊 token,模型前向结束后,直接读取每个 [MASK] 位置对应的 logit 作为该选项的得分,再经温度缩放与 softmax 得到概率。
[MASK]的位置被记录在marker_pos张量中(形状[B, K],K 为选项数)- 模型输出
logits形状为[B, K],未使用的槽位填充-1e4,保证不干扰 softmax noul问题固定按[false, true]顺序渲染,p[1]即为 noul 值
选项的渲染规则(choice 渲染为 key: desc、score 渲染为 level i: ...、noul 渲染为默认的 true/false 文案)都在 src/sequence.ts 的 renderOptions 中,是纯函数,不依赖模型权重,便于单测。
用户文本里的 [MASK] 会被自动清洗
如果工单正文里恰好出现字符串 <a href="https://link.gitcode.com/i/3356d425d4f9296ddc6ee31087ba65a8" target="_blank">MASK],它会伪造一个"假答案锚点",直接干扰模型读数。因此构建序列前会先做清洗(scrub):把 instructions、选项文本和 state 中所有 [MASK] 替换为空格,见 [src/sequence.ts。对应的行为测试在 test/test_sequence.ts。
🗺️ 序列布局:一条工单如何变成 token 序列
以 README 中的退款工单为例,choice 问题最终被拼装成如下结构(buildSequence):
[CLS] choice question: Which team should handle this ticket? [SEP]
[MASK] billing: payments, refunds, invoices
[MASK] support: product help and bugs
[MASK] sales: new purchases and upgrades
[SEP] {"subject": "Refund not received", "body": "..."} [SEP]
| 区段 | 内容 | 作用 |
|---|---|---|
[CLS] + head |
问题类型 + instructions | 告诉模型"在回答什么类型的问题" |
[MASK] + 选项 |
每个选项一行 | 答案锚点,位置写入 marker_pos |
| state | 传入的状态对象 | 决策依据 |
[SEP] |
分节符 | 区隔各段 |
两个细节值得注意:
- 每个选项的文本最多编码 48 个 token(src/sequence.ts),防止单个超长选项挤爆预算
- JSON 状态的序列化刻意对齐 Python 的
json.dumps(ensure_ascii=False)(分隔符、键顺序、非 ASCII 字符都一致),确保 token 与参考实现逐字对齐,见 pyJsonDumps
批量推理:右填充与 marker 张量
同一次 systemOne 调用的所有问题会被拼成一个 batch,做单次前向推理。实现上按最长序列右填充 <a href="https://link.gitcode.com/i/0f1515f837bffdda6c9c3470e3184560" target="_blank">PAD]、按最宽选项数填充 marker_pos / marker_mask([src/laya.ts)。ONNX 会话的输入输出为:
- 输入:
input_ids[B,L]、attention_mask[B,L]、marker_pos[B,K]、marker_mask[B,K]、qtype[B] - 输出:
logits[B,K](未校准)、act_probs[B,2]
详见 onnx/README.md 与导出脚本 export/export_onnx.py。
📏 head_max_len 截断:192 token 预算怎么花
配置文件 onnx/laya_config.json 里有两个关键数字:
max_len = 512:整条序列的总长度上限(英文检查点)head_max_len = 192:问题头 + 全部选项的预算
分配规则:先保选项,再挤问题
buildSequence 的截断策略(src/sequence.ts)按以下优先级执行:
- 先编码全部选项:每个选项 = 1 个
[MASK]+ 至多 48 token 的选项文本 - 剩余预算给 instructions:
optBudget = 192 - 选项总长,问题头被截到max(8, optBudget)个 token——保证至少留 8 个 token 的上下文 - 选项过多时均匀缩水:若
optBudget < 16,把每个选项统一裁剪到max(4, ⌊(192-16)/选项数⌋)个 token(含[MASK]),而不是丢弃某些选项 - state 拿剩下的空间:
room = 512 - 已用长度 - 1,超长工单在此被截断
放不下怎么办:宁可报错也不静默丢答案
构建序列后会校验:<a href="https://link.gitcode.com/i/fe8dd8822456cf37d44ad30c543f6e77" target="_blank">MASK] 标记数量必须等于选项数量,否则 systemOne 直接抛出 options do not fit in head_max_len=192 tokens 错误([src/laya.ts),避免把残缺的选项集静默交给模型。模型侧的官方建议是每个 choice 问题少于 20 个选项。
state 截断到 max_len
state 位于序列尾部,按剩余空间截断后,整条序列最终再统一 slice(0, 512)。换句话说:问题头和选项永远完整,超长的工单状态先被截断。
🔗 三者如何协同工作:一次 systemOne 的完整旅程
把三个机制串起来,一次调用经历五个阶段:
- 渲染:
renderOptions把三种问题类型统一成"一行一个选项"的文本 - 拼装 + 截断:
buildSequence生成 token 序列、记录每个[MASK]位置,并在 192 / 512 两级预算内裁剪 - 批处理:右填充成
[B, L]张量,附带marker_pos/marker_mask/qtype - 前向推理:ONNX 会话输出
logits [B,K] - 校准:按"问题类型 + 选项数量分桶"选取温度值,logits 除以温度后 softmax 得到概率,置信度 = 1 − 归一化熵
温度分桶逻辑在 tempBucket:选项数分为 2 / 3-5 / 6-10 / 11+ 四档。laya_config.json 中的校准值很能说明设计意图:noul:2 用 1.9834(偏保守),而 choice:11+ 只用 0.1006——选项很多时压低温度、让分布更尖锐,避免高熵选项集输出"各占 10%"的假均衡。温度选择与 softmax 见 src/laya.ts。
🚀 快速上手:本地运行 Laya 决策模型
npm install @receptron/laya
- 需要 Node.js 20 及以上;首次使用会从 Hugging Face 下载约 1.7 GB(fp32)的 ONNX 权重,缓存到
~/.cache/receptron-laya(可用LAYA_CACHE覆盖) - 建议预留约 2 GB 内存
- 模型预热后,一次包含 3 个问题的调用在 Apple 芯片 CPU 上约 140 ms
📂 关键文件导航
| 文件 | 内容 |
|---|---|
| src/sequence.ts | 序列布局、选项渲染、截断策略、温度分桶(纯逻辑,无权重依赖) |
| src/laya.ts | Laya.load / systemOne:ONNX 会话、批量张量与概率校准 |
| src/types.ts | 三种问题与答案的类型定义 |
| onnx/laya_config.json | max_len、head_max_len 与按选项数分桶的温度值 |
| export/export_onnx.py | 将 PyTorch 检查点导出为 ONNX 并做一致性校验 |
| test/test_sequence.ts | 布局、截断与清洗行为的单元测试 |
一句话总结:[MASK] 是锚点,序列布局是语法,head_max_len 是预算——三者协同保证了 Laya 在 512 个 token 内拿到完整、可校准的决策信号,一次前向推理同时回答所有问题。