🇬🇧 English
A decision model answers questions instead of writing prose. Three shapes: choice picks an option, score places you on a rubric, noul is a calibrated yes/no.
Jev is hosted and needs a key. Laya is a file you download. Both answer the same way.
The interesting question is not which is nicer. It is whether the probabilities mean anything, and that needs measuring.
🇹🇭 ไทย
โมเดลตัดสินใจตอบคำถาม แทนที่จะเขียนข้อความ มีสามรูปแบบ choice เลือกตัวเลือก score วางคุณบนเกณฑ์ และ noul คือใช่หรือไม่ที่ปรับเทียบแล้ว
Jev โฮสต์ไว้และต้องใช้คีย์ ส่วน Laya คือไฟล์ที่คุณดาวน์โหลด ทั้งคู่ตอบแบบเดียวกัน
คำถามที่น่าสนใจไม่ใช่ว่าอันไหนดีกว่า แต่คือความน่าจะเป็นพวกนั้นมีความหมายไหม และนั่นต้องวัด
🇨🇳 中文
决策模型回答问题,而不是写文章。三种形状:choice 挑一个选项,score 把你放在一把标尺上,noul 是一个校准过的是非题。
Jev 是托管的,要密钥。Laya 是一个你下载下来的文件。两者的回答方式一样。
有意思的问题不是哪个更好,而是那些概率到底有没有意义,而这需要量。
| JevJevJev | LayaLayaLaya | |
|---|---|---|
| How you get itได้มายังไง怎么拿到 | hosted service, API keyบริการที่โฮสต์ไว้ ต้องมีคีย์托管服务,要 API 密钥 | pip install, weights on diskpip install น้ำหนักอยู่บนดิสก์pip install,权重在本地磁盘 |
| Licenceสัญญาอนุญาต许可证 | commercial, per callเชิงพาณิชย์ คิดต่อครั้ง商业,按次计费 | Apache 2.0Apache 2.0Apache 2.0 |
| Sizeขนาด大小 | not publishedไม่เปิดเผย未公布 | 421M, a ModernBERT-large encoder421M ตัวเข้ารหัส ModernBERT-large4.21 亿,ModernBERT-large 编码器 |
| Languagesภาษา语言 | not stated hereไม่ได้ระบุไว้ที่นี่这里未说明 | English; a separate multilingual build existsอังกฤษ มีบิลด์หลายภาษาแยกต่างหาก英文;另有一个多语版本 |
| On these fifteen ticketsบนตั๋วสิบห้าใบนี้在这十五条工单上 | kev-0.6b, the open copy: 12 of 15, 447 mskev-0.6b สำเนาเปิด: 12 จาก 15, 447 mskev-0.6b,那个开源副本:12/15,447 毫秒 | 12 of 15, 260 ms — and 9 of 9 once gated12 จาก 15, 260 ms และ 9 จาก 9 เมื่อใส่เกต12/15,260 毫秒——加上门控后是 9/9 |
🧰 Step 0 — Install it · ขั้นที่ 0 — ติดตั้ง · 第 0 步 —— 装起来
💻 INSTALL
python -m venv ~/laya-venv # keep the path short on Windows
source ~/laya-venv/bin/activate # Windows: ~\laya-venv\Scripts\activate
pip install laya
# pulls torch, transformers, safetensors and huggingface_hub
python -c "import laya; print(laya.QTYPES)"
# {'choice': 0, 'score': 1, 'noul': 2}
# The weights come down on first load, from either checkpoint:
# convaiinnovations/laya general
# convaiinnovations/laya-typed-decisions fine-tuned on support and invoices
torch\lib\shm.dll with OSError WinError 4551, so the import failed before any model was fetched. Everything on this page was therefore run on a Linux box instead. And on that box the CUDA path failed too — Triton tries to compile a small driver shim with gcc at import time, and that build failed against the system libraries. Passing device="cpu" sidesteps both, and a 421M encoder does not need the GPU: 260 ms per ticket on CPU.
บนวินโดวส์มันรันไม่ได้เลย Application Control บล็อก torch\lib\shm.dll ด้วย OSError WinError 4551 ทำให้การ import ล้มเหลวก่อนที่จะดาวน์โหลดโมเดลด้วยซ้ำ ทุกอย่างในหน้านี้จึงรันบนเครื่องลินุกซ์แทน และบนเครื่องนั้นเส้นทาง CUDA ก็ล้มเหลวเช่นกัน เพราะ Triton พยายามคอมไพล์ตัวเชื่อมไดรเวอร์เล็ก ๆ ด้วย gcc ตอน import แล้วการบิลด์นั้นล้มเหลวกับไลบรารีของระบบ การส่ง device="cpu" เลี่ยงได้ทั้งสองอย่าง และตัวเข้ารหัสขนาด 421M ก็ไม่ต้องใช้ GPU คือ 260 ms ต่อตั๋วบน CPU
在 Windows 上这根本跑不起来:Application Control 以 OSError WinError 4551 拦下了 torch\lib\shm.dll,于是 import 在任何模型被下载之前就失败了。所以这一页的所有内容都改在一台 Linux 机器上跑。而在那台机器上,CUDA 那条路也失败了——Triton 会在 import 时用 gcc 编译一个小小的驱动垫片,那次构建在系统库上失败了。传 device="cpu" 可以绕开这两个问题,而且一个 4.21 亿参数的编码器本来也不需要 GPU:CPU 上每条工单 260 毫秒。
📋 AND IT WARNS YOU ON LOAD, EVERY TIME
RuntimeWarning: laya: this checkpoint ships invalid temperatures or values
outside [0.5, 5]; using choice:11+=0.10058280825614929 -> 0.5. Treat
confidence from the affected entries as uncalibrated.
🎚️ Tutorial 1 — Asking it something · บทที่ 1 — ลองถามมันดู · 教程 1 —— 问它点东西
🇬🇧 English
Two files. One question, then all three answer shapes at once, which is the thing a chat model cannot do in a single pass.
🇹🇭 ไทย
สองไฟล์ คำถามหนึ่งข้อ แล้วตามด้วยรูปแบบคำตอบทั้งสามพร้อมกัน ซึ่งเป็นสิ่งที่โมเดลแชตทำในรอบเดียวไม่ได้
🇨🇳 中文
两个文件。先一个问题,然后三种答案形状一次全要——那是聊天模型在一次前向里做不到的事。
1 One question, one number · หนึ่งคำถาม หนึ่งตัวเลข · 一个问题,一个数字
📄 laya1.py
"""The smallest typed decision there is: one question, one probability.
Nothing is generated. The model reads the state, answers the question you
defined, and hands back a number you can compare with a threshold.
"""
import laya
# device="cpu" because a 421M encoder does not need a GPU, and because the
# CUDA path wants Triton to compile a shim that not every machine can build.
agent = laya.load("convaiinnovations/laya", device="cpu")
ticket = "I was charged twice for the same order. Please refund the extra one."
# A question is a dict, not a sentence. "noul" is a calibrated yes/no, and
# the criteria spell out what each side means, so the model is not guessing
# at your definition of the word.
questions = {
"refund": {
"type": "noul",
"instructions": "Is the customer asking for money back?",
"criteria": {
"true": "They want a payment returned or reversed.",
"false": "They are asking about something else.",
},
}
}
result = agent.predict(ticket, questions)
answer = result["answers"]["refund"]
# For a noul the value is P(true). There is no yes or no to parse, and no
# string anywhere in the reply.
print("P(true) :", answer["noul"])
print("confidence:", answer["confidence"])
print("usage :", result["usage"])
print("act on it :", answer["noul"] > 0.5)
📋 WHAT IT PRINTS
P(true) : 0.8401
confidence: 0.8401
usage : {'input_tokens': 52, 'output_tokens': 0}
act on it : True
- Delete the
criteriablock and run it again. The question still works, and the number moves — those two sentences were doing real work. - Feed it "What is your refund policy?" and watch P(true) sit near the fence. Step 3 is about exactly that ticket.
- ลบบล็อก
criteriaออกแล้วรันอีกครั้ง คำถามยังทำงานได้ และตัวเลขขยับ สองประโยคนั้นทำงานจริง - ป้อน "What is your refund policy?" เข้าไปแล้วดู P(true) ไปนั่งอยู่แถวรั้ว ขั้นที่ 3 ว่าด้วยตั๋วใบนั้นพอดี
- 把
criteria那块删掉再跑一次。问题照样能用,而数字动了——那两句话是在干实活的。 - 喂它一句 "What is your refund policy?",看 P(true) 停在栅栏附近。第 3 步讲的正是这条工单。
2 Three shapes, one forward pass · สามรูปแบบ รอบเดียว · 三种形状,一次前向
📄 laya2.py
"""All three answer shapes, asked at once.
choice picks an option, score places the state on a rubric, noul is a
calibrated yes/no. They go in one dict and come back from one forward
pass, which is the thing a chat model cannot do.
"""
import laya
agent = laya.load("convaiinnovations/laya", device="cpu")
ticket = "I was charged twice for the same order. Please refund the extra one."
questions = {
"refund": {
"type": "noul",
"instructions": "Is the customer asking for money back?",
"criteria": {"true": "They want a payment returned.",
"false": "They are asking about something else."},
},
"team": {
"type": "choice",
"instructions": "Which desk should this go to?",
"criteria": {"billing": "payments, invoices, refunds",
"technical support": "crashes, bugs, outages",
"help": "general questions"},
},
"anger": {
# A score takes an ordered list, and the position in that list is
# the meaning. The answer comes back as a float between the ends.
"type": "score",
"instructions": "How angry does the customer sound?",
"criteria": ["calm", "annoyed", "angry", "furious"],
},
}
result = agent.predict(ticket, questions)
answers = result["answers"]
print("refund P(true):", answers["refund"]["noul"])
print("team :", answers["team"]["choice"],
answers["team"]["probabilities"])
print("anger :", round(answers["anger"]["score"], 2),
"on", list(answers["anger"]["legend"].values()))
print()
print("usage:", result["usage"])
📋 WHAT IT PRINTS
refund P(true): 0.8188
team : billing {'billing': 0.9021, 'technical support': 0.0296, 'help': 0.0683}
anger : 2.14 on ['calm', 'annoyed', 'angry', 'furious']
usage: {'input_tokens': 153, 'output_tokens': 0}
| Shapeรูปแบบ形状 | What comes backได้อะไรกลับมา回来的是什么 |
|---|---|
noul | A single float, P(true). No yes, no no, no word at all — you compare it with a threshold you chose.ทศนิยมตัวเดียว คือ P(true) ไม่มีใช่ ไม่มีไม่ใช่ ไม่มีคำใด ๆ คุณเอาไปเทียบกับเกณฑ์ที่คุณเลือกเอง一个浮点数,P(true)。没有是、没有否、一个词都没有——你拿它去和自己选的阈值比。 |
choice | The winning key, plus a probability for every option. The runner-up is often the more useful number.คีย์ที่ชนะ พร้อมความน่าจะเป็นของทุกตัวเลือก อันดับสองมักเป็นตัวเลขที่มีประโยชน์กว่า胜出的那个键,外加每个选项的概率。第二名往往才是更有用的那个数字。 |
score | A float between the rungs, not one of them: 2.14 on calm/annoyed/angry/furious. The legend comes back with it so you can read what 2.14 means.ทศนิยมที่อยู่ ระหว่าง ขั้น ไม่ใช่ขั้นใดขั้นหนึ่ง เช่น 2.14 บน calm/annoyed/angry/furious คำอธิบายขั้นกลับมาด้วย คุณจึงอ่านได้ว่า 2.14 แปลว่าอะไร一个落在刻度之间的浮点数,而不是某一档:在 calm/annoyed/angry/furious 上是 2.14。图例会一起回来,好让你读懂 2.14 是什么意思。 |
📏 Tutorial 2 — The same fifteen tickets · บทที่ 2 — ตั๋วสิบห้าใบชุดเดิม · 教程 2 —— 同样那十五条工单
🇬🇧 English
The Jev page measured fifteen support tickets, written before anything was scored against them. Reusing that exact file is the only way to compare honestly, so this step imports it unchanged.
🇹🇭 ไทย
หน้า Jev วัดด้วยตั๋วซัพพอร์ตสิบห้าใบ ซึ่งเขียนขึ้นก่อนที่จะมีการให้คะแนนอะไรกับมัน การใช้ไฟล์นั้นซ้ำแบบเป๊ะ ๆ เป็นวิธีเดียวที่จะเทียบได้อย่างซื่อสัตย์ ขั้นนี้จึง import มันมาโดยไม่แก้อะไร
🇨🇳 中文
Jev 那一页用十五条客服工单做过测量,而那些工单是在任何评分之前就写好的。原样复用那个文件是唯一能诚实比较的办法,所以这一步把它原封不动地 import 进来。
3 Laya against Kev, measured · Laya ปะทะ Kev วัดจริง · Laya 对 Kev,实测
📄 bench.py
"""The same fifteen tickets the Jev page used, put to Laya.
Two checkpoints: the general one, and the one fine-tuned on customer
service among other workflows. Same tickets, same question, same
threshold, so the only thing that differs is the weights.
"""
import time
import laya
from tickets import TICKETS
QUESTION = {
"refund": {
"type": "noul",
"instructions": "Is the customer asking for money back?",
"criteria": {"true": "They want a payment returned or reversed.",
"false": "They are asking about something else."},
}
}
CHECKPOINTS = ["convaiinnovations/laya",
"convaiinnovations/laya-typed-decisions"]
def run(model_id):
agent = laya.load(model_id, device="cpu")
right = 0
wrong = []
started = time.time()
for text, expected in TICKETS:
probability = agent.predict(text, QUESTION)["answers"]["refund"]["noul"]
said = probability > 0.5
if said == expected:
right += 1
else:
wrong.append((text, expected, round(probability, 3)))
each = (time.time() - started) / len(TICKETS) * 1000
return right, each, wrong
for model_id in CHECKPOINTS:
right, each, wrong = run(model_id)
print(f"{model_id}")
print(f" {right} of {len(TICKETS)} right, {each:.0f} ms each")
for text, expected, probability in wrong:
print(f" missed P(true)={probability} wanted {expected}: {text[:58]}")
print()
# For comparison, copied from the Jev page, which measured the same
# fifteen tickets on the same threshold:
# kev-0.6b 12 of 15, 447 ms each
# llama3.2 15 of 15, 2150 ms each
📋 MEASURED ON A CPU, 26 SEPTEMBER 2026
convaiinnovations/laya
11 of 15 right, 268 ms each
missed P(true)=0.463 wanted True: I paid for a year and used it once. Can we undo that?
missed P(true)=0.537 wanted False: Can you send me an invoice for last month for my accountan
missed P(true)=0.631 wanted False: What is your refund policy, out of interest?
missed P(true)=0.558 wanted False: The price went up. Is that on the website somewhere?
convaiinnovations/laya-typed-decisions
12 of 15 right, 260 ms each
missed P(true)=0.465 wanted True: I paid for a year and used it once. Can we undo that?
missed P(true)=0.489 wanted True: Send the money back to the same card, thanks.
missed P(true)=0.589 wanted False: What is your refund policy, out of interest?
| Modelโมเดล模型 | Rightถูก对 | Eachต่อใบ每条 |
|---|---|---|
| laya | 11 / 15 | 268 ms |
| laya-typed-decisions | 12 / 15 | 260 ms |
| kev-0.6b, from the Jev pagekev-0.6b จากหน้า Jevkev-0.6b,来自 Jev 那页 | 12 / 15 | 447 ms |
| llama3.2, a chat modelllama3.2 โมเดลแชตllama3.2,一个聊天模型 | 15 / 15 | 2,150 ms |
📐 Tutorial 3 — Is the number worth anything · บทที่ 3 — ตัวเลขนั้นมีค่าไหม · 教程 3 —— 那个数字值钱吗
🇬🇧 English
Every miss in Step 3 came back between 0.46 and 0.63 — all of them within a whisker of the fence. If that is a pattern and not luck, the probability is telling you something the yes/no cannot.
🇹🇭 ไทย
ทุกข้อที่พลาดในขั้นที่ 3 กลับมาอยู่ระหว่าง 0.46 ถึง 0.63 ทั้งหมดอยู่ห่างจากรั้วแค่เส้นยาแดง ถ้านั่นเป็นแบบแผนไม่ใช่ความบังเอิญ ความน่าจะเป็นกำลังบอกอะไรบางอย่างที่คำว่าใช่หรือไม่ใช่บอกไม่ได้
🇨🇳 中文
第 3 步里每一次失手,值都落在 0.46 到 0.63 之间——全都贴着栅栏。如果这是规律而不是运气,那么概率正在告诉你一些「是/否」说不出来的东西。
4 Refusing to answer, and 9 of 9 · ปฏิเสธที่จะตอบ แล้วได้ 9 จาก 9 · 拒绝作答,然后 9 比 9
📄 calib.py
"""Every miss in bench.py sat near 0.5. So test the obvious idea:
refuse to answer when the model is unsure, and hand those to a person.
This is the whole argument for a probability over a yes/no. If the
number is worth anything, throwing away the uncertain ones should leave
the rest cleaner.
"""
import numpy as np
import laya
from tickets import TICKETS
QUESTION = {
"refund": {
"type": "noul",
"instructions": "Is the customer asking for money back?",
"criteria": {"true": "They want a payment returned or reversed.",
"false": "They are asking about something else."},
}
}
agent = laya.load("convaiinnovations/laya-typed-decisions", device="cpu")
probabilities = []
expected = []
for text, wanted in TICKETS:
probabilities.append(agent.predict(text, QUESTION)["answers"]["refund"]["noul"])
expected.append(wanted)
probabilities = np.array(probabilities)
expected = np.array(expected)
correct = (probabilities > 0.5) == expected
# Distance from the fence. 0.5 is maximum doubt, 0.0 and 1.0 are certainty.
margin = np.abs(probabilities - 0.5)
print("band kept right accuracy to a human")
for width in [0.0, 0.05, 0.10, 0.15, 0.20]:
keep = margin >= width
kept = int(keep.sum())
right = int(correct[keep].sum())
accuracy = right / kept if kept else float("nan")
print(f"±{width:.2f} {kept:>4} {right:>5} {accuracy:>8.2f} {len(TICKETS) - kept:>10}")
# The library ships the calibration measure too. Confidence here is how
# far the model committed, which is what the gate above is sorting on.
confidence = np.maximum(probabilities, 1 - probabilities)
print()
print("expected calibration error:", round(laya.ece_score(confidence, correct), 3))
print("mean confidence :", round(float(confidence.mean()), 3))
print("actual accuracy :", round(float(correct.mean()), 3))
📋 THE SWEEP
band kept right accuracy to a human
±0.00 15 12 0.80 0
±0.05 13 12 0.92 2
±0.10 9 9 1.00 6
±0.15 7 7 1.00 8
±0.20 7 7 1.00 8
expected calibration error: 0.191
mean confidence : 0.677
actual accuracy : 0.8
- Run the sweep on the general checkpoint instead. Does the gate rescue it to 100% too, or were its four misses further from the fence?
- Print the six tickets the ±0.10 gate hands over. Would you have wanted a person on those anyway?
- ลองรันการกวาดค่าบนเช็กพอยต์ทั่วไปแทน เกตช่วยให้มันขึ้นเป็น 100% ด้วยไหม หรือสี่ข้อที่มันพลาดอยู่ไกลจากรั้วกว่า
- พิมพ์ตั๋วหกใบที่เกต ±0.10 ส่งต่อออกมาดู คุณอยากให้คนดูตั๋วพวกนั้นอยู่แล้วหรือเปล่า
- 改在通用 checkpoint 上跑这次扫描。门控也能把它救到 100% 吗,还是它那四次失手离栅栏更远?
- 把 ±0.10 门控交出去的那六条工单打印出来。那几条你本来也会想让人来看吗?
🪤 Where it falls over · มันพังตรงไหน · 它在哪里翻车
| Problemปัญหา问题 | What actually happensเกิดอะไรขึ้นจริง实际会怎样 |
|---|---|
| Topic, not requestหัวข้อ ไม่ใช่คำขอ主题,而不是请求 | Step 3. Both checkpoints, and Kev before them, read "what is your refund policy" as a refund request. An encoder scores what a sentence is about.ขั้นที่ 3 เช็กพอยต์ทั้งสองและ Kev ก่อนหน้านั้น อ่าน "what is your refund policy" ว่าเป็นคำขอคืนเงิน ตัวเข้ารหัสให้คะแนนว่าประโยคเกี่ยวกับอะไร第 3 步。两个 checkpoint,以及在它们之前的 Kev,都把 "what is your refund policy" 读成退款请求。编码器打的是「这句话在讲什么」的分。 |
| The probabilities are not probabilitiesความน่าจะเป็นไม่ใช่ความน่าจะเป็น概率不是概率 | ECE 0.191 measured, 0.213 published, and a RuntimeWarning at load saying some entries are uncalibrated outright. Useful for ranking, not for reading as odds.ECE 0.191 ที่วัดได้ 0.213 ที่เผยแพร่ และ RuntimeWarning ตอนโหลดที่บอกว่าบางรายการไม่ได้ถูกปรับเทียบเลย ใช้จัดอันดับได้ แต่อย่าอ่านเป็นอัตราต่อรอง实测 ECE 0.191,公布值 0.213,加上加载时一条说某些条目根本未校准的 RuntimeWarning。拿来排序有用,别当赔率读。 |
| Questions interfereคำถามรบกวนกันเอง问题之间会互相干扰 | Step 2. The same question about the same text gave 0.8401 alone and 0.8188 alongside two others. Fix your question set before tuning thresholds.ขั้นที่ 2 คำถามเดียวกันกับข้อความเดียวกันให้ 0.8401 เมื่อถามเดี่ยว และ 0.8188 เมื่อถามพร้อมอีกสองข้อ ให้ล็อกชุดคำถามก่อนจูนเกณฑ์第 2 步。对同一段文字问同一个问题,单独问是 0.8401,和另外两个一起问是 0.8188。先把问题集固定下来再调阈值。 |
| English onlyอังกฤษเท่านั้น只支持英文 | The typed-decisions checkpoint is English. There is a separate multilingual build, unmeasured here, so do not assume these numbers carry to Thai or Chinese.เช็กพอยต์ typed-decisions เป็นภาษาอังกฤษ มีบิลด์หลายภาษาแยกต่างหากซึ่งไม่ได้วัดที่นี่ อย่าคิดว่าตัวเลขเหล่านี้ใช้ได้กับภาษาไทยหรือจีนtyped-decisions 这个 checkpoint 是英文的。另有一个多语版本,这里没有测过,所以别以为这些数字能搬到泰文或中文上。 |
| It will not out-think a chat modelมันคิดสู้โมเดลแชตไม่ได้它想不过聊天模型 | 12 of 15 against llama3.2's 15 of 15. You are buying a shape of answer and eight times the speed, not a better answer.12 จาก 15 เทียบกับ llama3.2 ที่ได้ 15 จาก 15 คุณกำลังซื้อรูปแบบของคำตอบและความเร็วแปดเท่า ไม่ใช่คำตอบที่ดีกว่า12/15 对 llama3.2 的 15/15。你买到的是答案的形状和八倍的速度,不是更好的答案。 |
| Getting torch to loadทำให้ torch โหลดได้让 torch 能加载 | Blocked outright on this Windows machine by Application Control, and the CUDA path needed Triton to compile a shim that would not build. device="cpu" on Linux was the combination that worked.ถูกบล็อกทั้งดุ้นบนเครื่องวินโดวส์นี้โดย Application Control และเส้นทาง CUDA ต้องให้ Triton คอมไพล์ตัวเชื่อมที่บิลด์ไม่ผ่าน device="cpu" บนลินุกซ์คือชุดที่ใช้ได้在这台 Windows 机器上被 Application Control 直接拦死,而 CUDA 那条路又要 Triton 编译一个编译不出来的垫片。最后能用的组合是 Linux 上的 device="cpu"。 |
🇬🇧 English
Next: Jev and Kev, where the fifteen tickets came from, LangGraph if you want the routing decision to drive a graph, or what an embedding is, which fails in the same way for the same reason.
🇹🇭 ไทย
ต่อไป: Jev และ Kev ที่มาของตั๋วสิบห้าใบ LangGraph ถ้าคุณอยากให้การตัดสินใจเรื่องเส้นทางไปขับกราฟ หรือ เอมเบดดิงคืออะไร ซึ่งพังแบบเดียวกันด้วยเหตุผลเดียวกัน