揭秘 Laya 决策模型内部机制:[MASK] 标记、序列布局与 head_max_len 截断如何协同工作

原创2026-10-22 14:53:581,732 阅读

揭秘 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. 先编码全部选项:每个选项 = 1 个 [MASK] + 至多 48 token 的选项文本
  2. 剩余预算给 instructions:optBudget = 192 - 选项总长,问题头被截到 max(8, optBudget) 个 token——保证至少留 8 个 token 的上下文
  3. 选项过多时均匀缩水:若 optBudget < 16,把每个选项统一裁剪到 max(4, ⌊(192-16)/选项数⌋) 个 token(含 [MASK]),而不是丢弃某些选项
  4. 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 的完整旅程

把三个机制串起来,一次调用经历五个阶段:

  1. 渲染:renderOptions 把三种问题类型统一成"一行一个选项"的文本
  2. 拼装 + 截断:buildSequence 生成 token 序列、记录每个 [MASK] 位置,并在 192 / 512 两级预算内裁剪
  3. 批处理:右填充成 [B, L] 张量,附带 marker_pos / marker_mask / qtype
  4. 前向推理:ONNX 会话输出 logits [B,K]
  5. 校准:按"问题类型 + 选项数量分桶"选取温度值,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 内拿到完整、可校准的决策信号,一次前向推理同时回答所有问题。

登录后查看全文
laya