🎚️ Laya, the open answer to Jev · Laya คำตอบแบบเปิดของ Jev · Laya:Jev 的开源答案

Same idea as Jev — ask typed questions, get probabilities back instead of a sentence to parse — but the weights are on disk under Apache 2.0, and it was published three days after Jev. 421 million parameters, no key, no network. Measured here on the identical fifteen support tickets the Jev page used, which is the only way to say anything honest about it. · แนวคิดเดียวกับ Jev คือถามคำถามที่มีชนิดแล้วได้ความน่าจะเป็นกลับมา แทนที่จะได้ประโยคมาให้แกะ แต่น้ำหนักโมเดลอยู่บนดิสก์ภายใต้ Apache 2.0 และมันถูกปล่อยออกมาสามวันหลัง Jev สี่ร้อยยี่สิบเอ็ดล้านพารามิเตอร์ ไม่ต้องใช้คีย์ ไม่ต้องใช้เน็ต วัดที่นี่ด้วยตั๋วซัพพอร์ตสิบห้าใบชุดเดียวกับที่หน้า Jev ใช้ ซึ่งเป็นวิธีเดียวที่จะพูดอะไรตรง ๆ เกี่ยวกับมันได้ · 和 Jev 一个思路——问带类型的问题,回来的是概率而不是一句要你解析的话——但权重就在磁盘上,Apache 2.0,而且它是在 Jev 之后三天发布的。四亿两千一百万参数,不要密钥,不要联网。这里用的是 Jev 那一页用过的同样十五条客服工单来实测,这是唯一能对它说点实话的办法。

🇬🇧 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 是一个你下载下来的文件。两者的回答方式一样。

有意思的问题不是哪个更好,而是那些概率到底有没有意义,而这需要量。

JevJevJevLayaLayaLaya
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
ITS OWN MODEL CARD SAYS OVERCONFIDENT. ON THIS SET IT WAS THE OPPOSITE. The card warns that the probabilities are overconfident and tells you to refit the temperatures. Measured on these fifteen tickets it was underconfident: mean confidence 0.677 against an actual accuracy of 0.80. That is the right direction to be wrong in, and it is still wrong — either way you are being told not to read the raw numbers as truth. Step 4 does the measuring. การ์ดของโมเดลเตือนว่าความน่าจะเป็นมันมั่นใจเกินจริง และบอกให้คุณปรับค่าอุณหภูมิใหม่ แต่เมื่อวัดบนตั๋วสิบห้าใบนี้ มันกลับ มั่นใจน้อยเกินไป คือความมั่นใจเฉลี่ย 0.677 เทียบกับความแม่นยำจริง 0.80 นั่นเป็นทิศทางที่ผิดพลาดแบบที่ดีกว่า แต่มันก็ยังผิดอยู่ดี ไม่ว่าทางไหนคุณก็กำลังถูกบอกว่าอย่าอ่านตัวเลขดิบเป็นความจริง ขั้นที่ 4 คือการวัดนั้น 它自己的模型卡警告说概率过于自信,并让你重新拟合温度。而在这十五条工单上实测,它是不够自信:平均置信度 0.677,而实际准确率是 0.80。这是错得比较好的那个方向,但它仍然是错的——不管哪个方向,你都被告知别把原始数字当真理读。第 4 步就是去量这件事。

🧰 Step 0 — Install it · ขั้นที่ 0 — ติดตั้ง · 第 0 步 —— 装起来

YOU GET One pip install, one model download, and no account anywhere. pip install หนึ่งครั้ง ดาวน์โหลดโมเดลหนึ่งครั้ง และไม่ต้องสมัครบัญชีที่ไหนเลย 一次 pip install,一次模型下载,哪里都不用注册账号。

💻 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
TWO THINGS THAT WENT WRONG HERE, BOTH LIKELY TO HAPPEN TO YOU On Windows this would not run at all: Application Control blocked 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.
DO NOT FILTER THAT WARNING AWAY It is telling you that a calibration parameter shipped in the checkpoint is out of range and has been replaced with a default, so some confidence values are not calibrated at all. A model whose selling point is honest probabilities warning you that its probabilities are not to be trusted is worth reading twice. It does not stop the model working, and Step 4 shows the numbers are still usefully ordered even where they are not accurate — but that is a much weaker promise than the pitch. มันกำลังบอกคุณว่าพารามิเตอร์การปรับเทียบที่มากับเช็กพอยต์อยู่นอกช่วงและถูกแทนด้วยค่าเริ่มต้น ดังนั้นค่าความมั่นใจบางค่าไม่ได้ถูกปรับเทียบเลย โมเดลที่ขายเรื่องความน่าจะเป็นที่ซื่อสัตย์ แล้วเตือนคุณว่าความน่าจะเป็นของมันเชื่อไม่ได้ เป็นเรื่องที่ควรอ่านสองรอบ มันไม่ได้ทำให้โมเดลใช้ไม่ได้ และขั้นที่ 4 แสดงว่าตัวเลขยัง เรียงลำดับ ได้อย่างมีประโยชน์แม้ในที่ที่มันไม่แม่น แต่นั่นเป็นคำสัญญาที่อ่อนกว่าคำโฆษณามาก 它在告诉你:checkpoint 里带的某个校准参数超出了范围,已经被替换成默认值,所以有些置信度根本没有被校准。一个卖点就是「诚实概率」的模型,反过来警告你它的概率不可信——这值得读两遍。它不会让模型不能用,而且第 4 步显示这些数字即使不准确,排序仍然有用——但那是一个比宣传弱得多的承诺。

🎚️ 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 · หนึ่งคำถาม หนึ่งตัวเลข · 一个问题,一个数字

YOU GET The whole interface, and the line in the output that explains what kind of model this is. อินเทอร์เฟซทั้งหมด และบรรทัดในเอาต์พุตที่อธิบายว่านี่คือโมเดลชนิดไหน 整个接口,以及输出里那一行说明了这是哪一类模型。

📄 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
output_tokens: 0 That is the whole argument in one field. The model read 52 tokens and generated none, because there is nothing to generate — the answer is a number on a head, not text sampled one token at a time. Nothing to parse means nothing to fail at parsing: no JSON mode, no regex, no retry when it answers "Yes, absolutely!" instead of "yes". It also means the cost does not depend on how long the answer is, because there is no answer to be long. นั่นคือข้อโต้แย้งทั้งหมดในฟิลด์เดียว โมเดลอ่าน 52 โทเคนและไม่ได้สร้างอะไรเลย เพราะไม่มีอะไรให้สร้าง คำตอบคือตัวเลขบนหัวโมเดล ไม่ใช่ข้อความที่สุ่มมาทีละโทเคน การไม่มีอะไรให้แกะแปลว่าไม่มีอะไรให้แกะพลาด ไม่ต้องมีโหมด JSON ไม่ต้องมี regex ไม่ต้องลองใหม่เมื่อมันตอบว่า "Yes, absolutely!" แทน "yes" และยังแปลว่าต้นทุนไม่ขึ้นกับความยาวของคำตอบ เพราะไม่มีคำตอบให้ยาว 整个论点就在这一个字段里。模型读了 52 个 token,一个都没生成,因为没有东西要生成——答案是模型头上的一个数字,不是一个 token 一个 token 采样出来的文字。没有东西要解析,就没有东西会解析失败:不用 JSON 模式,不用正则,也不用在它回「Yes, absolutely!」而不是「yes」时重试。这还意味着成本不取决于答案有多长,因为根本没有答案可以变长。
YOUR TURN
  1. Delete the criteria block and run it again. The question still works, and the number moves — those two sentences were doing real work.
  2. Feed it "What is your refund policy?" and watch P(true) sit near the fence. Step 3 is about exactly that ticket.
  1. ลบบล็อก criteria ออกแล้วรันอีกครั้ง คำถามยังทำงานได้ และตัวเลขขยับ สองประโยคนั้นทำงานจริง
  2. ป้อน "What is your refund policy?" เข้าไปแล้วดู P(true) ไปนั่งอยู่แถวรั้ว ขั้นที่ 3 ว่าด้วยตั๋วใบนั้นพอดี
  1. 把 criteria 那块删掉再跑一次。问题照样能用,而数字动了——那两句话是在干实活的。
  2. 喂它一句 "What is your refund policy?",看 P(true) 停在栅栏附近。第 3 步讲的正是这条工单。

2 Three shapes, one forward pass · สามรูปแบบ รอบเดียว · 三种形状,一次前向

YOU GET choice, score and noul together, and a small surprise about what asking three questions does to the first one. choice, score และ noul พร้อมกัน และเรื่องน่าแปลกใจเล็ก ๆ ว่าการถามสามคำถามทำอะไรกับคำถามแรก choice、score 和 noul 一起问,以及一个小意外:问三个问题会对第一个问题做什么。

📄 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}
THE REFUND ANSWER CHANGED, AND ONLY THE COMPANY IT KEEPS CHANGED Step 1 asked the refund question alone and got 0.8401. Step 2 asked the identical question about the identical ticket and got 0.8188, because two unrelated questions were in the same dict. The questions share one forward pass, so they are not independent: adding a question perturbs the others. It is small here and it will not flip a decision at this distance from 0.5, but if you tune a threshold with one question and then ship three, you have moved the thing you tuned. Pin your question set before you pick your thresholds. ขั้นที่ 1 ถามคำถามเรื่องคืนเงินเดี่ยว ๆ แล้วได้ 0.8401 ขั้นที่ 2 ถามคำถามเดียวกันเป๊ะเกี่ยวกับตั๋วใบเดียวกันเป๊ะ แล้วได้ 0.8188 เพราะมีคำถามที่ไม่เกี่ยวกันอีกสองข้ออยู่ใน dict เดียวกัน คำถามทั้งหมดใช้การคำนวณรอบเดียวร่วมกัน จึงไม่เป็นอิสระต่อกัน การเพิ่มคำถามไปรบกวนคำถามอื่น ที่นี่มันเล็กและจะไม่พลิกการตัดสินใจที่ระยะห่างจาก 0.5 ขนาดนี้ แต่ถ้าคุณจูนเกณฑ์ด้วยคำถามเดียวแล้วไปใช้งานจริงสามคำถาม คุณได้ขยับสิ่งที่คุณจูนไปแล้ว ให้ล็อกชุดคำถามก่อนจะเลือกเกณฑ์ 第 1 步单独问退款问题,得到 0.8401。第 2 步对完全相同的工单问了完全相同的问题,得到 0.8188,因为同一个 dict 里还有两个不相干的问题。这些问题共享一次前向计算,所以它们并不独立:加一个问题会扰动其他问题。这里幅度很小,在离 0.5 这么远的地方不会翻转决策;但如果你用一个问题调好阈值,然后上线三个问题,你就已经把自己调过的那个东西挪动了。先把问题集定下来,再去挑阈值。
Shapeรูปแบบ形状What comes backได้อะไรกลับมา回来的是什么
noulA single float, P(true). No yes, no no, no word at all — you compare it with a threshold you chose.ทศนิยมตัวเดียว คือ P(true) ไม่มีใช่ ไม่มีไม่ใช่ ไม่มีคำใด ๆ คุณเอาไปเทียบกับเกณฑ์ที่คุณเลือกเอง一个浮点数,P(true)。没有是、没有否、一个词都没有——你拿它去和自己选的阈值比。
choiceThe winning key, plus a probability for every option. The runner-up is often the more useful number.คีย์ที่ชนะ พร้อมความน่าจะเป็นของทุกตัวเลือก อันดับสองมักเป็นตัวเลขที่มีประโยชน์กว่า胜出的那个键,外加每个选项的概率。第二名往往才是更有用的那个数字。
scoreA 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,实测

YOU GET Both Laya checkpoints on the same fifteen tickets, every failure printed, and a number to set beside the Jev page's. เช็กพอยต์ทั้งสองของ Laya บนตั๋วสิบห้าใบชุดเดียวกัน พิมพ์ทุกข้อที่พลาด และตัวเลขไว้วางเทียบกับของหน้า Jev 两个 Laya checkpoint 跑同样十五条工单,每一次失手都打印出来,还有一个可以和 Jev 那页并排放的数字。

📄 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ต่อใบ每条
laya11 / 15268 ms
laya-typed-decisions12 / 15260 ms
kev-0.6b, from the Jev pagekev-0.6b จากหน้า Jevkev-0.6b,来自 Jev 那页12 / 15447 ms
llama3.2, a chat modelllama3.2 โมเดลแชตllama3.2,一个聊天模型15 / 152,150 ms
THE FINE-TUNE BOUGHT ONE TICKET, AND MOVED WHICH ONE IT LOST The general checkpoint got 11, the one fine-tuned on customer service got 12 — the same score as Kev, at a little over half the time. But look at the failures rather than the totals. The general model failed on three questions about money that were not requests for money. The fine-tuned one fixed two of those and broke a new one instead: "Send the money back to the same card, thanks." is about as plain a refund request as exists, and it came back 0.489. Fine-tuning moved the boundary, it did not straighten it. เช็กพอยต์ทั่วไปได้ 11 ส่วนตัวที่ fine-tune กับงานบริการลูกค้าได้ 12 ซึ่งเท่ากับ Kev โดยใช้เวลาแค่ราวครึ่งเดียว แต่ให้ดูข้อที่พลาดแทนที่จะดูยอดรวม โมเดลทั่วไปพลาดกับสาม คำถามเกี่ยวกับ เงินที่ไม่ได้เป็นการขอเงินคืน ส่วนตัวที่ fine-tune แก้ได้สองข้อ แล้วไปพังข้อใหม่แทน คือ "Send the money back to the same card, thanks." ซึ่งเป็นคำขอคืนเงินที่ตรงไปตรงมาที่สุดเท่าที่จะมีได้ แล้วมันกลับมาที่ 0.489 การ fine-tune ขยับเส้นแบ่ง ไม่ได้ทำให้เส้นตรงขึ้น 通用 checkpoint 拿到 11 分,在客服数据上微调过的那个拿到 12 分——和 Kev 一样,用时只要一半多一点。但要看失手的那几条,别看总分。通用模型错在三条关于钱、但并不是在要钱的问题上。微调过的那个修好了其中两条,却新弄坏了一条:"Send the money back to the same card, thanks." 大概是最直白不过的退款请求了,而它回来是 0.489。微调挪动了边界,并没有把它拉直。
BOTH CHECKPOINTS MISSED THE SAME TICKET, AND SO DID KEV "What is your refund policy, out of interest?" is a question about refunds from someone who is not asking for one, and every small decision model on both pages calls it a refund request. The Jev page named this exact failure: it scores the topic, not the request. That is what a 400M encoder trained on sentence-level similarity is good at, and a request is a different thing from a subject. Cheap models will keep failing this until something in the pipeline asks about the act, not the theme. "What is your refund policy, out of interest?" คือคำถามเกี่ยวกับการคืนเงินจากคนที่ไม่ได้กำลังขอคืนเงิน และโมเดลตัดสินใจขนาดเล็กทุกตัวในทั้งสองหน้าตัดสินว่ามันคือคำขอคืนเงิน หน้า Jev ระบุความล้มเหลวนี้ไว้ตรง ๆ ว่ามันให้คะแนนหัวข้อ ไม่ใช่คำขอ นั่นคือสิ่งที่ตัวเข้ารหัสขนาด 400M ที่ฝึกมาบนความคล้ายระดับประโยคทำได้ดี และคำขอเป็นคนละเรื่องกับหัวข้อ โมเดลราคาถูกจะพลาดข้อนี้ต่อไปจนกว่าจะมีอะไรสักอย่างในไปป์ไลน์ถามถึง การกระทำ ไม่ใช่ธีม "What is your refund policy, out of interest?" 是一个关于退款、但提问者并没有在要退款的问题,而这两页上的每一个小决策模型都把它判成了退款请求。Jev 那一页点名过这个失败:它打的是主题的分,不是请求的分。这正是一个在句子级相似度上训练出来的 4 亿参数编码器擅长的事,而请求和主题是两码事。便宜的模型会一直错在这里,直到流水线里有什么东西开始问那个动作,而不是那个话题。

📐 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

YOU GET A threshold sweep that turns 12 of 15 into 9 of 9, and the price of it printed in the same table. การกวาดค่าเกณฑ์ที่เปลี่ยน 12 จาก 15 ให้เป็น 9 จาก 9 และราคาของมันพิมพ์อยู่ในตารางเดียวกัน 一次阈值扫描,把 12/15 变成 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
THIS IS THE ANSWER TO "WHY NOT JUST A YES/NO" Refuse to answer within ±0.10 of the fence and the model gets every remaining ticket right: nine of nine, with six sent to a person. Even the gentlest gate, ±0.05, turns 12 of 15 into 12 of 13 by handing over two. A yes/no cannot do this at all, because a yes/no has nowhere to put "I am not sure" — you would be choosing which tickets to escalate at random. The number is not accurate, as the next box shows, but it is ordered, and ordered is enough to sort the easy from the hard. ปฏิเสธที่จะตอบในระยะ ±0.10 จากรั้ว แล้วโมเดลตอบตั๋วที่เหลือถูกหมด คือเก้าจากเก้า โดยส่งหกใบให้คน แม้แต่เกตที่อ่อนที่สุดคือ ±0.05 ก็เปลี่ยน 12 จาก 15 เป็น 12 จาก 13 โดยส่งต่อสองใบ คำตอบแบบใช่หรือไม่ใช่ทำแบบนี้ไม่ได้เลย เพราะมันไม่มีที่ให้วางคำว่า "ฉันไม่แน่ใจ" คุณจะต้องเลือกสุ่มว่าจะส่งตั๋วใบไหนต่อ ตัวเลขนี้ไม่แม่นยำอย่างที่กล่องถัดไปแสดง แต่มัน เรียงลำดับได้ และการเรียงลำดับได้ก็พอที่จะแยกง่ายออกจากยาก 在离栅栏 ±0.10 以内拒绝作答,模型剩下的每一条都答对了:九比九,把六条交给人。就连最温和的门控 ±0.05,也能把 12/15 变成 12/13,只交出去两条。「是/否」根本做不到这件事,因为「是/否」没有地方放「我不确定」——你只能随机挑哪些工单要升级。这个数字并不准确,下一个框会说明,但它是有序的,而有序就足以把容易的和困难的分开。
UNDERCONFIDENT, NOT OVERCONFIDENT — THE CARD HAS IT BACKWARDS HERE Mean confidence 0.677, actual accuracy 0.80, expected calibration error 0.191. The model card warns about overconfidence and tells you to refit the temperatures; on this set it is hedging, not boasting. Both are miscalibration and the practical advice is the same — do not read 0.68 as "68% likely" — but the direction matters if you were planning to trust the warning instead of measuring. The published ECE was 0.213 on the authors' own benchmark, so 0.191 here is in the same unhappy neighbourhood. Fifteen tickets is a small set; run it on a few hundred of your own before you set a threshold you care about. ความมั่นใจเฉลี่ย 0.677 ความแม่นยำจริง 0.80 ค่าความคลาดเคลื่อนของการปรับเทียบที่คาดไว้ 0.191 การ์ดของโมเดลเตือนเรื่องความมั่นใจเกินและบอกให้ปรับค่าอุณหภูมิใหม่ แต่บนชุดนี้มันออมมือ ไม่ได้คุยโว ทั้งสองอย่างคือการปรับเทียบที่ผิด และคำแนะนำเชิงปฏิบัติก็เหมือนกัน คืออย่าอ่าน 0.68 ว่า "มีโอกาส 68%" แต่ทิศทางสำคัญถ้าคุณคิดจะเชื่อคำเตือนแทนที่จะวัดเอง ค่า ECE ที่เผยแพร่ไว้คือ 0.213 บนเบนช์มาร์กของผู้เขียนเอง ดังนั้น 0.191 ที่นี่ก็อยู่ในย่านที่ไม่น่าพอใจเหมือนกัน ตั๋วสิบห้าใบเป็นชุดที่เล็กมาก ให้ลองรันกับของคุณเองสักสองสามร้อยใบก่อนจะตั้งเกณฑ์ที่คุณให้ความสำคัญ 平均置信度 0.677,实际准确率 0.80,期望校准误差 0.191。模型卡警告的是过度自信,并让你重新拟合温度;而在这个集合上它是在保守,不是在吹牛。两者都是校准不良,实践建议也一样——别把 0.68 读成「68% 的可能」——但如果你本来打算相信那条警告而不是自己去量,方向就重要了。作者自己基准上公布的 ECE 是 0.213,所以这里的 0.191 属于同一个不太妙的区间。十五条工单是个很小的集合;在你设定一个真正在意的阈值之前,先拿你自己的几百条跑一遍。
YOUR TURN
  1. 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?
  2. Print the six tickets the ±0.10 gate hands over. Would you have wanted a person on those anyway?
  1. ลองรันการกวาดค่าบนเช็กพอยต์ทั่วไปแทน เกตช่วยให้มันขึ้นเป็น 100% ด้วยไหม หรือสี่ข้อที่มันพลาดอยู่ไกลจากรั้วกว่า
  2. พิมพ์ตั๋วหกใบที่เกต ±0.10 ส่งต่อออกมาดู คุณอยากให้คนดูตั๋วพวกนั้นอยู่แล้วหรือเปล่า
  1. 改在通用 checkpoint 上跑这次扫描。门控也能把它救到 100% 吗,还是它那四次失手离栅栏更远?
  2. 把 ±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 ถ้าคุณอยากให้การตัดสินใจเรื่องเส้นทางไปขับกราฟ หรือ เอมเบดดิงคืออะไร ซึ่งพังแบบเดียวกันด้วยเหตุผลเดียวกัน

🇨🇳 中文

接下来:Jev 和 Kev,那十五条工单的出处;LangGraph,如果你想让这个路由决策去驱动一张图;或者 嵌入是什么,它以同样的原因、同样的方式出错。

A number you can threshold beats a word you have to parse — even when the number is wrong. · ตัวเลขที่ตั้งเกณฑ์ได้ดีกว่าคำที่ต้องมาแกะ แม้ในตอนที่ตัวเลขนั้นผิด · 一个你能设阈值的数字,胜过一个你得去解析的词——哪怕那个数字是错的。

All four files were written and run on 26 September 2026 with laya 0.3.20 from PyPI, published by Convai Innovations under Apache 2.0, and every output block is copied from those runs. Both checkpoints ran on CPU on an arm64 Linux box, because torch would not import on this Windows machine at all — Application Control blocked torch\lib\shm.dll with OSError WinError 4551 — and because the CUDA path then failed when Triton could not compile its driver shim with gcc. The fifteen tickets are imported unchanged from the Jev page, where they were written before anything was scored against them; the kev-0.6b and llama3.2 rows are quoted from that page's measurements, not re-run here. Three things came out against expectation and all three are kept: the fine-tuned checkpoint fixed two failures and introduced a new one on a plainly worded refund request, both checkpoints failed the same policy question that Kev also failed, and the model measured as underconfident here (mean confidence 0.677 against 0.80 accuracy) where its own card warns about overconfidence. Model size, licence, context window and the published ECE of 0.213 are read from the model card, not verified here. Fifteen items is far too small a set to set a production threshold on, and the page says so rather than implying otherwise. No paid API was called at any point.