33毫秒分拣一封邮件:Laya决策模型 × OpenVINO™ 端侧部署实战
导语:用生成式 LLM 做分类,是"高射炮打蚊子"
邮件路由、工单分类、内容审核、紧急度打分——这类"决策型"任务几乎存在于每一条业务流水线里。现在很多团队的做法是:接一个 GPT 或者本地 LLM,写一大段 prompt 让它输出 JSON,再用正则和重试去兜底解析。这条路走得通,但每一步都在制造新问题:
- 输出是自由文本:模型偶尔不听话,JSON 解析一崩,整条链路就崩;
- 幻觉没有边界:分类任务不需要任何创造性,但 LLM 天生会"编";
- 延迟不确定:token 一个个蹦出来,p99 没法承诺;
- 成本按 token 计费:一封邮件问三个问题,账单翻三倍。
Convai Innovations 开源的 Laya 走的是另一条路:它是一个非自回归的 System 1 决策模型——你给它一个状态(一封邮件、一张工单、任何文本或 JSON 文档)和一组带类型的问题,它就在一次前向传播(约 33 ms)里返回带校准概率的类型化答案,支持 100+ 种语言。它不生成任何自由文本:没有输出要解析、没有幻觉可言、延迟是确定的。
本文会带你完整走一遍这条部署路径:用 optimum-cli 一条命令导出 OpenVINO™ IR(顺便用 --weight-format fp16 把权重量减半),用 optimum-intel 的 OVModelForLayaDecision 在 Intel CPU / GPU 上完成推理,最后跑一个交互式邮件分拣 Demo。文中所有推理输出均为本文在 Intel Core Ultra 9 288V(Lunar Lake)的 iGPU 上实测的结果,可以直接对照运行。
先看一眼效果
把一封"逾期发票停服警告"邮件连同三个问题喂给 Laya——该路由给哪个团队?是不是垃圾邮件?紧急程度如何?
模型一次前向传播全部作答:
category -> billing (p=95.9%) # 路由给账务团队,置信度 95.9%
is_spam -> 2.9% # 几乎可以肯定不是垃圾邮件
urgency -> 1.79 / 2 # 紧急度 1.79(0=不急, 1=需尽快, 2=阻塞级)
三个问题、一次前向、每个答案都自带概率——这就是 Laya 的全部工作方式。下面从头部署。
Laya:一个"只做决策"的模型
▪三种带类型的问题
Laya 的推理接口只有 system_one(state, questions),问题的"类型"只有三种:
- choice —— 多选一。给一组带描述的选项,返回选中的选项和每个选项的校准概率。典型用途:路由、审核、分类。
- score —— 有序打分。给一列从低到高的等级描述,返回期望分数(连续值)。典型用途:紧急度、严重度、优先级。
- noul —— 带概率的"是/否"。返回命题成立的概率。典型用途:垃圾邮件、钓鱼检测、流失风险标记。
三种问题可以混在同一个 questions 字典里,一次前向传播全部出结果。
▪为什么它的概率"可信"
Laya 使用 RLCD(Reinforcement Learning for Calibrated Decisions)训练:奖励函数基于严格合规评分规则(strictly proper scoring rules)——这类规则的数学性质是,只有报告"诚实"的概率分布才能最大化奖励。说人话就是:模型虚报置信度会吃亏,所以它给出的概率可以直接拿去做业务阈值和护栏,而不用再人工调"温度"。
架构上它也很轻:一个 421M 参数的双向编码器(ModernBERT-large)加一个小的 transformer 决策头,没有任何自回归解码器——快、小、确定性强。
▪三个开源 checkpoint
三个 checkpoint 全部 Apache 2.0 开源:

注意 Laya 的 checkpoint 不是标准 transformers 模型:权重在单个 model.safetensors 里,决策头的超参数和校准温度在 rl_agent_config.json 里,分词器在 tokenizer/ 子目录里。本文默认使用英语 checkpoint,同样的代码对三个 checkpoint 都适用。
官方给出的 laya-multilingual 长上下文测试:把 20 条覆盖 8 种语言的 support request 放在不相关长文本的末尾,考察 8192 token 窗口内准确率与延迟随上下文长度的变化:

Laya 多语言模型长上下文测试
端到端部署
▪环境准备
Laya 支持还没有合入 optimum-intel 正式版,本文使用携带该集成的 fork 分支(laya):
git+https://github.com/xiangfuxwang/optimum-intel-dev.git@laya
其余依赖照常从 PyPI 安装。注意 torch 和 torchvision 必须成对安装 CPU 版本——optimum-intel 依赖 torchvision,如果先装 CPU 版 torch、再从 PyPI 装 torchvision,会拉下 CUDA 版本,和 CPU 版 torch 配对时 import transformers 直接崩溃:
from pip_helper import pip_install
# 先卸载预装版本避免冲突
%pip uninstall -q -y optimum optimum-intel optimum-onnx
# torch 和 torchvision 必须从 PyTorch CPU 源成对安装
pip_install(
"-q",
"torch>=2.1",
"torchvision",
"--extra-index-url", "https://download.pytorch.org/whl/cpu",
)
pip_install(
"-q",
"openvino>=2025.4.0",
"openvino-tokenizers",
"nncf>=3.3.0",
"optimum>=1.24.0",
"transformers>=4.53.0",
"safetensors",
"huggingface_hub",
"ipywidgets",
)
# Laya 支持:在合入上游之前,安装携带集成的 fork 分支
pip_install("-q", "git+https://github.com/xiangfuxwang/optimum-intel-dev.git@laya")
▪下载模型
从 Hugging Face Hub 拉取 checkpoint(国内网络可以取消注释走镜像):
import ipywidgets as widgets
from huggingface_hub import snapshot_download
# 访问 huggingface.co 较慢时可启用镜像:
# import os; os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
model_options = {
"laya (English)": "convaiinnovations/laya",
"laya-multilingual": "convaiinnovations/laya-multilingual",
"laya-typed-decisions (fine-tuned)": "convaiinnovations/laya-typed-decisions",
}
model_selector = widgets.Dropdown(options=list(model_options.items()), description="Checkpoint:")
display(model_selector)
from pathlib import Path
model_dir = Path(model_selector.value.split("/")[-1])
if not (model_dir / "rl_agent_config.json").exists():
snapshot_download(repo_id=model_selector.value, local_dir=model_dir)
print(f"Checkpoint downloaded to: {model_dir.resolve()}")
print(f"Files: {sorted(p.name for p in model_dir.iterdir())}")
下载完成后可以看到 checkpoint 的结构(不是标准 transformers 布局):
Files: ['assets', 'config.json', 'email_utils.py', 'encoder', 'eval',
'model.safetensors', 'multilingual', 'README.md',
'rl_agent_api.py', 'rl_agent_config.json', 'rl_common.py',
'tokenizer', 'typed-decisions']
▪一条命令导出 OpenVINO IR
模型用 optimum-cli export openvino 导出为 OpenVINO IR,等效的命令行调用是:
optimum-cli export openvino --model convaiinnovations/laya --task feature-extraction --weight-format fp16 laya-ov-fp16
notebook 里通过下拉框选择权重压缩格式(见下一节的对比表),再用 cmd_helper 封装调用:
import ipywidgets as widgets
# 导出时应用的权重压缩格式
weight_format_options = {
"FP16 (default)": "fp16",
"INT8": "int8",
"INT4": "int4",
}
weight_format_selector = widgets.Dropdown(
options=list(weight_format_options.items()), value="fp16", description="Weights:"
)
display(weight_format_selector)
from cmd_helper import optimum_cli
weight_format = weight_format_selector.value
ov_model_dir = Path(f"{model_dir.name}-ov-{weight_format}")
if not (ov_model_dir / "openvino_model.xml").exists():
print("Exporting to OpenVINO IR, this may take a few minutes ...")
optimum_cli(
model_dir,
ov_model_dir,
additional_args={"task": "feature-extraction", "weight-format": weight_format},
)
else:
print(f"Found existing conversion in {ov_model_dir}/, skipping export")
print(f"Files: {sorted(p.name for p in ov_model_dir.iterdir())}")
导出产出一个单一 OpenVINO IR,接口非常干净:
输入:input_ids、attention_mask([batch, sequence]);marker_pos、marker_mask([batch, num_options]);qtype([batch],0=choice、1=score、2=noul)
输出:logits([batch, num_options],每个选项标记位一个分数)和 act_logits([batch, 2],"作答 vs 上报"头)
这里有个关键机制:每个答案选项在输入序列里以一个标记 token 前缀,marker_pos 指向这些标记的位置,决策头只对这些位置打分——这就是"一次前向、多个问题"的实现方式。
导出还会把运行时需要的资产放在 IR 旁边:rl_agent_config.json(校准温度、序列预算)、tokenizer/ 目录,以及 OpenVINO™ 分词器 / 去分词器 IR:
Files: ['openvino_model.xml', 'openvino_model.bin',
'openvino_tokenizer.xml', 'openvino_tokenizer.bin',
'openvino_detokenizer.xml', 'openvino_detokenizer.bin',
'rl_agent_config.json', 'tokenizer', ...]
▪压缩模型权重
默认导出保持 checkpoint 的 fp32 精度(约 1.6 GB)。--weight-format 选项通过 NNCF 权重压缩在导出时直接压缩权重:

因为 Laya 返回的是校准概率、且往往直接喂给业务阈值和护栏,使用 int8 / int4 压缩后请先确认决策仍然可接受,再投入生产。如果需要完整 fp32 保真,导出时不加这个标志即可。
▪加载模型与选择推理设备
导出完成后,加载过程和原生 transformers 几乎一模一样:
from notebook_utils import device_widget
from optimum.intel.openvino.modeling_laya import OVModelForLayaDecision
device = device_widget(default="CPU", exclude=["NPU"])
ov_model = OVModelForLayaDecision.from_pretrained(ov_model_dir, device=device.value)
print(f"Inputs : {list(ov_model.input_names)}")
print(f"Outputs: {list(ov_model.output_names)}")
Inputs : ['input_ids', 'attention_mask', 'marker_pos', 'marker_mask', 'qtype']
Outputs: ['logits', 'act_logits']
设备选择上:
CPU 开箱即用;
Intel GPU(集成显卡 / 独立显卡) 与上面导出的压缩 IR 是天作之合——权重导出时已经是低精度格式,加载时无需再做转换;
NPU 对该模型暂未验证,设备列表里已排除。
想对比自己机器上不同设备的延迟?下文"延迟实测"一节给出了测量代码。
▪推理:对齐官方 system_one API
运行时暴露的是官方 system_one(state, questions) API,所以转换后的模型是 PyTorch 原版的直接替代品。一个问题由三部分定义:
type —— "choice"、"score" 或 "noul";
instructions —— 问题文本;
criteria —— 选项:choice 用字典,score 用有序等级列表,noul 可选地给正 / 反命题描述。
下面是经典的 Laya 用例——邮件分拣:路由到正确的团队、标记垃圾邮件、给紧急度打分,一次前向全部完成:
import json
state = {
"subject": "Invoice #4382 overdue - service suspension warning",
"body": (
"Hi, our accounting team flagged invoice #4382 (issued Sep 12, $1,240) as unpaid. "
"Payment was sent on Sep 15 via wire transfer, please check on your side. "
"If the payment is not located by Friday the service will be suspended."
),
}
questions = {
"category": {
"type": "choice",
"instructions": "Which team should handle the email in `body`?",
"criteria": {
"billing": "invoices, payments, refunds",
"technical": "bugs, outages, integrations",
"sales": "pricing, demos, new purchases",
"other": "none of the above",
},
},
"is_spam": {"type": "noul", "instructions": "Is this email unsolicited spam or bulk marketing?"},
"urgency": {
"type": "score",
"instructions": "How urgent is the issue described in `body`?",
"criteria": ["no time pressure", "needs attention soon", "blocking issue or hard deadline"],
},
}
result = ov_model.system_one(state, questions)
print(json.dumps(result, indent=2, ensure_ascii=False))
实测输出(Lunar Lake 的 iGPU):
{
"model": "rl-agent",
"answers": {
"category": {
"type": "choice",
"choice": "billing",
"probabilities": {
"billing": 0.9587,
"technical": 0.0183,
"sales": 0.012,
"other": 0.0109
},
"confidence": 0.8441,
"rl_agent": { "act_probability": 1.0 }
},
"is_spam": {
"type": "noul",
"noul": 0.0287,
"rl_agent": { "act_probability": 1.0 }
},
"urgency": {
"type": "score",
"score": 1.7927,
"legend": {
"0": "no time pressure",
"1": "needs attention soon",
"2": "blocking issue or hard deadline"
},
"probabilities": { "0": 0.0055, "1": 0.1963, "2": 0.7982 },
"confidence": 0.5192,
"rl_agent": { "act_probability": 1.0 }
}
},
"usage": { "input_tokens": 358, "output_tokens": 0 }
}
怎么读这个输出:
choice 问题返回选中的选项加每个选项的校准概率——这里是 billing,约 96%;
score 问题返回期望分数(连续值)——这里 1.79(0..2 尺度),接近"阻塞级";
noul 问题返回命题成立的概率——noul: 0.03,几乎可以肯定不是垃圾邮件;
confidence 是答案分布的 1 − 归一化熵,衡量这个答案"多笃定";
rl_agent.act_probability 是模型自己对"该不该作答(还是上报人工)"的估计;
usage.input_tokens 是实际处理的 token 数——注意三个问题只消耗了一次前向传播。
换一封完全不同的邮件,用同样的代码消费结果:
state2 = {
"subject": "App crashes when opening settings",
"body": (
"Hello, since the last update the Android app force-closes every time I open "
"the settings screen. Reinstalling did not help. I use a Pixel 8 with Android 15."
),
}
result2 = ov_model.system_one(state2, questions)
for qid, ans in result2["answers"].items():
if ans["type"] == "choice":
top = ans["choice"]
print(f"{qid:10s} -> {top} (p={ans['probabilities'][top]:.1%})")
elif ans["type"] == "score":
print(f"{qid:10s} -> {ans['score']:.2f} / {len(ans['legend']) - 1}")
else:
print(f"{qid:10s} -> {ans['noul']:.1%}")
实测输出:
category -> technical (p=82.6%)
is_spam -> 6.4%
urgency -> 1.59 / 2
同样的三个问题,App 崩溃邮件被正确路由给技术团队——不需要改 prompt、不需要换模型、不需要第二次前向。
▪延迟实测
下面这段代码测量一次 system_one 调用的平均延迟。几次预热调用不计入测量——每个设备的首次推理包含一次性的模型编译(GPU 上还有 kernel JIT)开销。要对比不同设备,回到上面的设备选择单元格换个选项,重新执行后续单元格即可:
import time
for _ in range(3): # 预热:排除一次性的编译 / JIT 开销
ov_model.system_one(state, questions)
n_runs = 10
start = time.perf_counter()
for _ in range(n_runs):
ov_model.system_one(state, questions)
avg_ms = (time.perf_counter() - start) / n_runs * 1e3
print(f"Device: {device.value}")
print(f"Average latency over {n_runs} runs: {avg_ms:.1f} ms per system_one call")
本文实测(Intel Core Ultra 9 288V,同一示例):CPU 稳态延迟约 571 ms;换到 Lunar Lake 的 iGPU 后稳态约 30 ms,加速接近 20 倍,与官方标称的约 33 ms 吻合。
交互式 Demo
教程最后提供了一个可以直接上手的 ipywidgets 界面:粘贴任意邮件正文,点一下按钮,同一组问题实时分拣。
注意输入长度限制。 序列化后的 state 必须装进模型的上下文窗口——英语 laya checkpoint 是 512 token(laya-typed-decisions 为 1024,laya-multilingual 为 1024–8192)。装不下时,运行时会静默截断序列化 state 的末尾,长邮件结尾处的细节(截止时间、联系方式、签名)可能被悄悄丢掉。保持输入精炼、把重要信息放在前面——Demo 每次运行后会打印实际处理的输入 token 数:
from IPython.display import clear_output
body_input = widgets.Textarea(
value=state["body"], description="", layout=widgets.Layout(width="100%", height="140px")
)
run_button = widgets.Button(description="Triage email", button_style="primary")
truncation_note = widgets.HTML(
"<b>Note:</b> inputs that exceed the model's context window are truncated at the end — "
"keep the email concise and put important details first."
)
demo_output = widgets.Output()
def triage(_):
with demo_output:
clear_output()
res = ov_model.system_one({"subject": "(user input)", "body": body_input.value}, questions)
for qid, ans in res["answers"].items():
if ans["type"] == "choice":
top = ans["choice"]
print(f"{qid:10s}: {top} (p={ans['probabilities'][top]:.1%})")
elif ans["type"] == "score":
print(f"{qid:10s}: {ans['score']:.2f} / {len(ans['legend']) - 1}")
else:
print(f"{qid:10s}: {ans['noul']:.1%}")
print(f"({res['usage']['input_tokens']} input tokens processed)")
run_button.on_click(triage)
display(
widgets.VBox(
[
widgets.Label("Email body:"),
body_input,
truncation_note,
run_button,
demo_output,
]
)
)
总结与资源链接
Laya 把"LLM 流水线里最难缠的那段——解析生成文本"整个消灭掉了:带类型的问题进、带校准概率的决策出,一次前向、确定延迟、Apache 2.0 开源。而 OpenVINO™ + Optimum Intel 让它在 Intel CPU / GPU 上的部署简化成"一条 optimum-cli + 几十行 Python"——fp16 权重减半、Lunar Lake 的 iGPU 再加速近 20 倍。如果你的业务里有一堆"其实只需要一个分类器 / 一个打分器"的环节,Laya 值得一试。
相关资源:
- Laya × OpenVINO™ 教程 notebook(已合并至 openvino_notebooks 主仓库):https://github.com/openvinotoolkit/openvino_notebooks/tree/latest/notebooks/laya-decision-models
- 携带 Laya 集成的 optimum-intel fork 分支(等待上游合入):https://github.com/xiangfuxwang/optimum-intel-dev/tree/laya
- Laya 模型卡:https://huggingface.co/convaiinnovations/laya
- Laya 多语言模型卡:https://huggingface.co/convaiinnovations/laya-multilingual
- Laya 类型化决策模型卡:https://huggingface.co/convaiinnovations/laya-typed-decisions
- Laya 官方代码仓库:https://github.com/NandhaKishorM/laya
- Laya 官方文档:https://nandhakishorm.github.io/laya/
- Optimum Intel:https://github.com/huggingface/optimum-intel
- OpenVINO™:https://github.com/openvinotoolkit/openvino
更多推荐



所有评论(0)