🎚️ Jev, and asking for an answer instead of a sentence · Jev กับการขอคำตอบ แทนที่จะขอประโยค · Jev:要一个答案,而不是一句话

A decision model returns typed answers and real probabilities. Not text you have to parse. Jev is the hosted one. Kev is the open copy of the same contract. It runs on your laptop. Fifteen support tickets, both measured. Including the five it gets wrong, and why. · โมเดลตัดสินใจคืนคำตอบที่มีชนิดและความน่าจะเป็นจริง ไม่ใช่ข้อความที่คุณต้องไปแกะ Jev คือตัวที่โฮสต์ไว้ ส่วน Kev คือสำเนาเปิดของสัญญาเดียวกัน และรันบนแล็ปท็อปของคุณได้ ตั๋วซัพพอร์ตสิบห้าใบ วัดทั้งสองฝั่ง รวมถึงห้าใบที่มันตอบผิดและเหตุผล · 决策模型返回带类型的答案和真正的概率,不是需要你去解析的文字。Jev 是托管的那个。Kev 是同一份契约的开源副本,在你的笔记本上就能跑。十五条客服工单,两边都量,包括它错的那五条和原因。

🇬🇧 English

A chat model writes you a sentence and you pick the decision out of it. A decision model returns the decision.

Not the word yes. A number between 0 and 1, and the same for every option of every question you asked.

Jev is TypeSafe's hosted one and needs a key. Kev is the open copy of the same contract, and runs here.

🇹🇭 ไทย

โมเดลแชตจะเขียนประโยคมาให้ แล้วคุณต้องไปหยิบการตัดสินใจออกมาจากมัน ส่วนโมเดลตัดสินใจจะคืนการตัดสินใจมาเลย

ไม่ใช่คำว่า yes แต่เป็นตัวเลขระหว่าง 0 ถึง 1 และมีให้ทุกตัวเลือกของทุกคำถามที่คุณถาม

Jev เป็นตัวที่ TypeSafe โฮสต์ไว้และต้องใช้คีย์ ส่วน Kev คือสำเนาเปิดของสัญญาเดียวกัน และรันที่นี่ได้

🇨🇳 中文

聊天模型给你写一句话,你再从里面把决定挑出来。决策模型直接把决定还给你。

不是 yes 这个词,而是一个 0 到 1 之间的数字,你问的每个问题的每个选项都有一个。

Jev 是 TypeSafe 托管的那个,要密钥。Kev 是同一份契约的开源副本,在这儿就能跑。

A chat modelโมเดลแชต聊天模型A decision modelโมเดลตัดสินใจ决策模型
What comes backได้อะไรกลับมา回来的是什么chat: a sentence to parseแชต: ประโยคที่ต้องไปแกะ聊天:一句要解析的话decision: a number per optionตัดสินใจ: ตัวเลขต่อหนึ่งตัวเลือก决策:每个选项一个数字
Three questionsสามคำถาม三个问题chat: three calls, or one to unpickแชต: เรียกสามครั้ง หรือครั้งเดียวแล้วมาแกะ聊天:调三次,或者调一次再拆decision: one passตัดสินใจ: รอบเดียว决策:一次前向
Confidenceความมั่นใจ置信度chat: a word it wroteแชต: คำที่มันเขียนขึ้นมา聊天:它自己写的一个词decision: the probability itselfตัดสินใจ: ตัวความน่าจะเป็นเอง决策:概率本身
Measured hereวัดได้ที่นี่这里实测chat: 15 of 15, 1,006 msแชต: 15 จาก 15, 1,006 ms聊天:15/15,1,006 毫秒decision: 12 of 15, 447 msตัดสินใจ: 12 จาก 15, 447 ms决策:12/15,447 毫秒

💻 GET THE OPEN ONE

pip install onnxruntime tokenizers numpy

# Then the model, about 375 MB, from Hugging Face:
#   onnx-community/kev-0.6b-ONNX  ->  kev-0.6b-onnx/
# Jev itself needs a key. Step 6 shows the request either way.
THE INTERFACE IS THE GOOD PART. THE SMALL MODEL IS NOT SMART. Read the last row of that table before you get excited. The open model got twelve of these fifteen tickets right. A chat model got all fifteen. The decision model gives you a number you can act on. Not a better answer. Both halves of that matter, and this page measures both. อ่านแถวสุดท้ายของตารางนั้นก่อนจะตื่นเต้น จากตั๋วสิบห้าใบที่ใช้ตลอดหน้านี้ โมเดลเปิดขนาด 0.6B ตอบถูกสิบสอง ส่วนโมเดลแชตถูกทั้งสิบห้า สิ่งที่โมเดลตัดสินใจให้คุณคือตัวเลขที่เอาไปใช้ตัดสินได้ ไม่ใช่คำตอบที่ดีกว่า ทั้งสองอย่างนั้นสำคัญ และหน้านี้วัดทั้งคู่ 在兴奋之前,先读那张表的最后一行。在这一页通篇用的十五条工单上,开源的 0.6B 模型对了十二条,聊天模型全对。决策模型给你的是一个能拿来做决定的数字,不是更好的答案。这两件事都要紧,这一页两个都量。
Four minutes, covering this whole page: what a decision model returns, the fifteen-ticket measurement, and where it falls over. A computer reads the narration — it is not a person. Every figure is drawn, like the ones below. Subtitles in English, 中文 and ไทย follow the language buttons above, or the player's own menu. สี่นาที ครอบคลุมทั้งหน้านี้ โมเดลตัดสินใจคืนอะไรกลับมา ผลวัดจากตั๋วสิบห้าใบ และมันพังตรงไหน เสียงบรรยายเป็นเสียงคอมพิวเตอร์ ไม่ใช่คนจริง ทุกภาพเป็นภาพวาด เหมือนภาพด้านล่าง ซับไตเติลอังกฤษ จีน ไทย จะตามปุ่มภาษาด้านบน หรือเลือกในตัวเล่นวิดีโอเองก็ได้ 四分钟,覆盖整页内容:决策模型返回什么、十五条工单的实测,以及它在哪里翻车。旁白是合成语音,不是真人。所有图都是绘制的,和下面的一样。英文、中文、泰文字幕会跟随上方的语言按钮,也可以在播放器菜单里自己选。

🎚️ Tutorial 1 — A model that answers · บทที่ 1 — โมเดลที่ตอบ ไม่ใช่พูด · 教程 1 —— 一个会回答的模型

🇬🇧 English

Two files. One yes/no question, then all three question types at once, which is the thing a chat model cannot do.

🇹🇭 ไทย

สองไฟล์ คำถามใช่ไม่ใช่หนึ่งข้อ แล้วตามด้วยคำถามทั้งสามชนิดพร้อมกัน ซึ่งเป็นสิ่งที่โมเดลแชตทำไม่ได้

🇨🇳 中文

两个文件。先一个是非题,然后三种问题一次全问,那是聊天模型做不到的事。

1 One question, one number · หนึ่งคำถาม หนึ่งตัวเลข · 一个问题,一个数字

🇬🇧 English

The engine is kev.py, below the example. Read it once and then forget it; the rest of the page only calls it.

What matters is what comes back. Not the word yes, but how much of the probability landed on yes.

🇹🇭 ไทย

ตัวเครื่องคือ kev.py อยู่ใต้ตัวอย่าง อ่านสักรอบแล้วลืมไปได้เลย ที่เหลือของหน้านี้แค่เรียกใช้มัน

สิ่งที่สำคัญคือสิ่งที่ได้กลับมา ไม่ใช่คำว่า yes แต่คือความน่าจะเป็นที่ตกอยู่ฝั่ง yes มากแค่ไหน

🇨🇳 中文

引擎是 kev.py,在例子下面。读一遍就可以忘了它,这一页剩下的只是调用它。

重要的是回来的东西。不是 yes 这个词,而是有多少概率落在 yes 上。

📄 NEW FILE · decide1.py

"""decide1.py — a model that answers instead of talking."""
import sys

import kev

sys.stdout.reconfigure(encoding="utf-8")

TICKETS = [
    "I was charged twice for the same order. Please refund the extra one.",
    "Just wanted to say the new dashboard is lovely. No reply needed.",
    "The app crashes when I tap export. Android 14, latest build.",
    "Can you send me an invoice for last month for my accountant?",
]

# Telling it the situation, not only the question, is the single biggest
# thing you control. The same model on the same tickets does much worse
# when handed a bare question with no setting.
SITUATION = "A support desk is reading a customer message:\n{ticket}"
ASK = "Is this customer asking for a refund?"


def main():
    """Ask one yes/no question about each ticket, and print the number."""
    for ticket in TICKETS:
        # noul is the model's word for a yes/no question. What comes back
        # is not the word "yes"; it is how much of the probability sat on
        # yes. There is no sentence to parse and nothing to strip.
        sure = kev.noul(SITUATION.format(ticket=ticket), ASK)
        bar = "#" * int(sure * 40)
        print(f"{sure:5.3f} {bar:40} {ticket[:44]}")


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · four tickets, four numbers

0.991 #######################################  I was charged twice. Please refund
0.008                                          Just wanted to say the dashboard is lovely
0.027 #                                        The app crashes when I tap export
0.082 ###                                      Can you send me an invoice for last month
LineWhat it does · ทำอะไร · 做什么
SITUATION = "A support desk is reading a customer message:Telling it the setting is the biggest thing you control.การบอกสถานการณ์ให้มันรู้คือสิ่งที่คุณควบคุมได้มากที่สุด把情境告诉它,是你能控制的最大的一件事。
sure = kev.noul(noul is the model's word for a yes/no question.noul คือคำที่โมเดลใช้เรียกคำถามแบบใช่ไม่ใช่noul 是这个模型对是非题的叫法。

🇬🇧 English

Here is the engine. The only hard part is the arithmetic that works out where to read the answer.

🇹🇭 ไทย

นี่คือตัวเครื่อง ส่วนที่ยากมีแค่การคำนวณว่าจะไปอ่านคำตอบตรงไหน

🇨🇳 中文

这是引擎。唯一难的部分是算出该去哪里读答案。

📄 NEW FILE · kev.py

"""kev.py — run the open decision model, and read its answers.

Jev is TypeSafe's hosted model and you need a key for it. Kev is the open
copy of the same idea, and the contract is the same: choice, score and
noul questions in, a probability for every option out.

One document, called the state. Any number of typed questions. They are
packed into a single sequence with five delimiter tokens, and the model
answers all of them in one forward pass.
"""
import json
import re
from pathlib import Path

import numpy as np
import onnxruntime as ort
from tokenizers import Tokenizer

HERE = Path(__file__).parent / "kev-0.6b-onnx"
CONFIG = json.loads((HERE / "config.json").read_text(encoding="utf-8"))

# The delimiters live in config.json, under "kev". Reading them from there
# beats writing the numbers down and hoping the next version agrees.
D = CONFIG["kev"]["delimiter_ids"]
STATE, QUESTION = D["state"], D["question"]
OPTION, OPTION_END, DECIDE = D["option_start"], D["option_end"], D["decide"]
PAD = CONFIG["pad_token_id"]

_tokenizer = Tokenizer.from_file(str(HERE / "tokenizer.json"))
_session = None

# Text from a person must never be able to fake a delimiter, so <|x|>
# becomes <¦x¦> on the way in.
_FORGE = re.compile(r"<\|([A-Za-z0-9_]+)\|>")


def session():
    """Load the graph once and keep it."""
    global _session
    if _session is None:
        _session = ort.InferenceSession(str(HERE / "onnx" / "model_q4.onnx"),
                                        providers=["CPUExecutionProvider"])
    return _session


def encode(text):
    return _tokenizer.encode(_FORGE.sub(r"<¦\1¦>", text),
                             add_special_tokens=False).ids


def pack(state, questions):
    """Lay out the state and every question, and note where to read.

    Only the logits at each option's closing token mean anything. Get
    this arithmetic wrong and you get confident nonsense, not an error.
    """
    tokens = [STATE] + encode(state)
    reads = []
    for question in questions:
        instruction = encode(question["ask"])
        spans = [[OPTION] + encode(option) + [OPTION_END]
                 for option in question["options"]]

        base = len(tokens)
        cursor = 1 + len(instruction)
        ends = []
        for span in spans:
            cursor = cursor + len(span)
            ends.append(base + cursor - 1)

        tokens.append(QUESTION)
        tokens.extend(instruction)
        for span in spans:
            tokens.extend(span)
        tokens.append(DECIDE)
        reads.append(ends)
    return tokens, reads


def softmax(values):
    shifted = np.array(values, dtype=np.float64) - np.max(values)
    weights = np.exp(shifted)
    return weights / weights.sum()


def ask(state, questions):
    """One forward pass. Returns a probability per option, per question."""
    tokens, reads = pack(state, questions)
    input_ids = np.array([tokens], dtype=np.int64)
    attention_mask = np.ones_like(input_ids)
    outputs = session().run(None, {"input_ids": input_ids,
                                   "attention_mask": attention_mask})
    logits = np.asarray(outputs[0]).reshape(-1)
    return [softmax([logits[i] for i in ends]) for ends in reads]


def noul(state, ask_text):
    """A yes/no question. Returns how sure the model is of yes."""
    # The order is the model's own, set in config.json: ["no", "yes"].
    options = CONFIG["kev"]["noul_options"]
    return float(ask(state, [{"ask": ask_text, "options": options}])[0][1])
LineWhat it does · ทำอะไร · 做什么
D = CONFIG["kev"]["delimiter_ids"]Read the five marker tokens from the model. Not from a blog post.อ่านโทเคนเครื่องหมายห้าตัวจากตัวโมเดล ไม่ใช่จากบล็อกใครสักคน从模型里读那五个标记 token,别从某篇博客里抄。
ends.append(base + cursor - 1)Where each option's closing token sits. Only those logits mean anything.ตำแหน่งโทเคนปิดของแต่ละตัวเลือก มีแค่ค่าตรงนั้นที่มีความหมาย每个选项收尾 token 的位置。只有那些 logits 有意义。
softmax([logits[i] for i in ends])Turn the option logits of one question into probabilities.เปลี่ยนค่า logit ของตัวเลือกในคำถามหนึ่งให้เป็นความน่าจะเป็น把一个问题里各选项的 logits 变成概率。
GET THE OFFSETS WRONG AND YOU GET CONFIDENT NONSENSE The model returns one number per token. Almost all are meaningless. Only the position of each option's closing token carries an answer. Miscount by one and nothing raises an error. You just get a number. It looks exactly like a real one. โมเดลคืนค่ามาหนึ่งตัวต่อหนึ่งโทเคน และเกือบทั้งหมดไม่มีความหมาย มีแค่ตำแหน่งโทเคนปิดของแต่ละตัวเลือกที่มีคำตอบอยู่ ถ้านับพลาดไปหนึ่ง จะไม่มีอะไรฟ้องข้อผิดพลาดเลย คุณจะได้ตัวเลขมา และมันหน้าตาเหมือนตัวเลขจริงทุกประการ 模型每个 token 回一个数字,而绝大多数毫无意义。只有每个选项收尾 token 的位置上有答案。数错一位,不会有任何报错。你只会拿到一个数字,而它看起来和真的一模一样。
YOUR TURN
  1. Run it. Write a ticket of your own and add it.
  2. Remove the SITUATION wrapper. Pass the bare ticket.
  3. Check: the invoice line goes from 0.082 to about 0.5.
  4. Put it back. That one paragraph was doing real work.
  1. รันดู แล้วเขียนตั๋วของคุณเองเพิ่มเข้าไป
  2. ลบตัวครอบ SITUATION ออก แล้วส่งตั๋วเปล่า ๆ เข้าไป
  3. ตรวจ: บรรทัดใบแจ้งหนี้จะขยับจาก 0.082 ไปราว 0.5
  4. ใส่กลับเข้าไป ย่อหน้านั้นทำงานจริงจัง
  1. 运行。写一条你自己的工单加进去。
  2. 把 SITUATION 这层包装去掉,直接传工单。
  3. 检查:发票那行会从 0.082 变到 0.5 左右。
  4. 再加回去。那一段是真的在干活。

2 Three kinds of question, one pass · คำถามสามชนิด รอบเดียว · 三种问题,一次前向

🇬🇧 English

This is the shape Jev defines and Kev copies. Three types, and you can mix them freely.

choice picks one. score places it on a scale. noul is yes or no.

All of them answered together, from one reading of the document.

🇹🇭 ไทย

นี่คือรูปแบบที่ Jev กำหนดไว้และ Kev ลอกมา สามชนิด และผสมกันได้ตามใจ

choice คือเลือกหนึ่งอัน score คือวางลงบนสเกล ส่วน noul คือใช่หรือไม่ใช่

ทั้งหมดถูกตอบพร้อมกัน จากการอ่านเอกสารครั้งเดียว

🇨🇳 中文

这是 Jev 定的、Kev 抄的那个形状。三种类型,可以随意混着用。

choice 选一个,score 放到一个刻度上,noul 是或否。

全部一起回答,文档只读一遍。

📄 NEW FILE · three1.py

"""three1.py — three kinds of question, one document, one forward pass."""
import sys
import time

import kev

sys.stdout.reconfigure(encoding="utf-8")

TICKET = ("I was charged twice for the same order and nobody answers my "
          "emails. I want my money back today or I am calling the bank.")

# The three types Jev defines, and Kev copies.
#   choice - pick one option. Describe each one; the words are the spec.
#   score  - place it on an ordered scale, written worst to best.
#   noul   - a yes/no proposition. Always the options no, then yes.
QUESTIONS = [
    {"ask": "Which team should handle this ticket?",
     "options": ["billing: charges, refunds, invoices",
                 "technical support: bugs, outages, login problems",
                 "sales: pricing and new accounts",
                 "account management: renewals and contracts"]},
    {"ask": "How angry is this customer?",
     "options": ["calm", "mildly annoyed", "frustrated", "furious"]},
    {"ask": "Is the customer threatening a chargeback?",
     "options": ["no", "yes"]},
]


def main():
    """All three questions go in together and come back together."""
    # Loading the graph takes a couple of seconds, once. Do it before the
    # clock starts, or the first measurement is mostly disk.
    kev.session()

    started = time.perf_counter()

    # One call. The state is read once, and every question sees it. Asking
    # a chat model these three things means three prompts, or one prompt
    # and a paragraph to unpick afterwards.
    answers = kev.ask(TICKET, QUESTIONS)
    spent = time.perf_counter() - started

    for question, probabilities in zip(QUESTIONS, answers):
        print(question["ask"])
        order = sorted(range(len(probabilities)),
                       key=lambda i: -probabilities[i])
        for rank in order:
            bar = "#" * int(probabilities[rank] * 30)
            print(f"   {probabilities[rank]:5.3f} {bar:30} "
                  f"{question['options'][rank]}")
        print()

    print(f"three questions, one pass, {spent * 1000:.0f} ms")


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · one ticket, three answers

Which team should handle this ticket?
   0.990 #############################  billing: charges, refunds, invoices
   0.008                                technical support: bugs, outages
   0.002                                sales: pricing and new accounts

How angry is this customer?
   0.639 ###################            frustrated
   0.244 #######                        furious
   0.089 ##                             mildly annoyed
   0.028                                calm

Is the customer threatening a chargeback?
   0.821 ########################       yes

three questions, one pass, 548 ms
LineWhat it does · ทำอะไร · 做什么
{"ask": "Which team should handle this ticket?",A choice question. The option text is the specification.คำถามแบบ choice ข้อความของตัวเลือกคือตัวสเปกเอง一个 choice 问题。选项的文字本身就是规格。
"options": ["calm", "mildly annoyed", "frustrated", "furious"]A score question. Write the levels in order, worst to best.คำถามแบบ score เขียนระดับเรียงตามลำดับ จากแย่ไปดี一个 score 问题。把等级按顺序写,从差到好。
"options": ["no", "yes"]A noul question. Always no first, then yes.คำถามแบบ noul ต้องเป็น no ก่อนเสมอ แล้วค่อย yes一个 noul 问题。永远先 no,再 yes。
answers = kev.ask(TICKET, QUESTIONS)One call. The document is read once and every question sees it.เรียกครั้งเดียว เอกสารถูกอ่านหนึ่งรอบ และทุกคำถามเห็นมัน一次调用。文档只读一遍,每个问题都看得到它。
YOUR TURN
  1. Run it. All three answers are right. Check them yourself.
  2. Add a fourth question of your own.
  3. Reverse the order of the anger levels. Does the answer move?
  4. Describe the teams better. Watch the top number rise.
  1. รันดู คำตอบทั้งสามถูกหมด ลองตรวจเองดู
  2. เพิ่มคำถามที่สี่ของคุณเองเข้าไป
  3. สลับลำดับระดับความโกรธกลับหลัง คำตอบขยับไหม
  4. อธิบายทีมให้ดีขึ้น แล้วดูตัวเลขบนสุดสูงขึ้น
  1. 运行。三个答案都对,自己核一下。
  2. 加一个你自己的第四个问题。
  3. 把生气的等级顺序反过来。答案变了吗?
  4. 把各个团队描述得更清楚,看最高那个数字往上走。

📐 Tutorial 2 — Is the number worth anything · บทที่ 2 — ตัวเลขนั้นมีค่าอะไรไหม · 教程 2 —— 那个数字值钱吗

🇬🇧 English

Any model can print a confidence. The question is whether it moves when the model is actually less sure.

Fifteen tickets, both models, and then the thing the number is actually for.

🇹🇭 ไทย

โมเดลไหนก็พิมพ์ค่าความมั่นใจออกมาได้ คำถามคือมันขยับไหมเวลาที่โมเดลไม่แน่ใจจริง ๆ

ตั๋วสิบห้าใบ ทั้งสองโมเดล แล้วตามด้วยสิ่งที่ตัวเลขนั้นมีไว้ทำ

🇨🇳 中文

什么模型都能打印一个置信度。问题是:当模型真的更不确定时,它会动吗?

十五条工单,两个模型,然后是这个数字真正的用处。

3 The same tickets, both models · ตั๋วชุดเดียวกัน สองโมเดล · 同样的工单,两个模型

🇬🇧 English

The chat model is asked to report its own confidence, which is the usual way people get a number out of one.

Look at the accuracy first, then look at what each confidence column actually contains.

🇹🇭 ไทย

โมเดลแชตถูกขอให้รายงานความมั่นใจของตัวเอง ซึ่งเป็นวิธีปกติที่คนใช้ดึงตัวเลขออกมาจากมัน

ดูความแม่นก่อน แล้วค่อยดูว่าคอลัมน์ความมั่นใจของแต่ละฝั่งมีอะไรอยู่จริง ๆ

🇨🇳 中文

我们让聊天模型报出自己的置信度,这是大家从它那儿拿数字的常规办法。

先看准确率,再看两边的置信度那一列里到底装了什么。

🇬🇧 English

The fifteen tickets, and the answer for each one. They were written before anything was scored against them.

🇹🇭 ไทย

ตั๋วสิบห้าใบ พร้อมคำตอบของแต่ละใบ เขียนขึ้นก่อนที่จะมีการวัดอะไรกับมัน

🇨🇳 中文

十五条工单,每条的答案。它们是在任何评分之前就写好的。

📄 NEW FILE · tickets.py

"""tickets.py — support messages, and whether each is asking for a refund.

Written before anything was measured against them, so the numbers on this
page are not the result of choosing convenient examples.
"""

TICKETS = [
    # ── plainly asking ──────────────────────────────────────────────────
    ("I was charged twice for the same order. Please refund the extra one.", True),
    ("Cancel my subscription and give me my money back.", True),
    ("Your service did not work for a single day. I want a refund.", True),
    ("Please return the payment from 3 September, I did not authorise it.", True),

    # ── asking, without the word refund ─────────────────────────────────
    ("You took 890 baht twice. Put one back please.", True),
    ("I paid for a year and used it once. Can we undo that?", True),
    ("Send the money back to the same card, thanks.", True),

    # ── about money, but not asking for it back ─────────────────────────
    ("Can you send me an invoice for last month for my accountant?", False),
    ("Do you offer a student discount before I subscribe?", False),
    ("What is your refund policy, out of interest?", False),
    ("The price went up. Is that on the website somewhere?", False),

    # ── nothing to do with money ────────────────────────────────────────
    ("The app crashes when I tap export. Android 14, latest build.", False),
    ("Just wanted to say the new dashboard is lovely. No reply needed.", False),
    ("How do I add a second user to my team?", False),
    ("Is there an outage? Nothing has loaded since this morning.", False),
]

# Which desk each one belongs on, for the routing step. Same order.
TEAMS = ["billing", "billing", "billing", "billing",
         "billing", "billing", "billing",
         "billing", "sales", "billing", "billing",
         "technical support", "help", "help", "technical support"]

📄 NEW FILE · sure1.py

"""sure1.py — the number is the point. Is it worth anything?

Both models are asked the same fifteen tickets. Accuracy is one thing.
What matters more is whether the confidence each one reports tells you
anything you can act on.
"""
import json
import sys
import time
import urllib.request

import kev
from tickets import TICKETS

OLLAMA = "http://localhost:11434/api/chat"
MODEL = "llama3.2"

SITUATION = "A support desk is reading a customer message:\n{ticket}"
ASK = "Is this customer asking for a refund?"

PROMPT = (SITUATION + "\n\nIs this customer asking for a refund? Answer with "
          'JSON only:\n{{"refund": true or false, '
          '"confidence": a number from 0 to 1}}')

sys.stdout.reconfigure(encoding="utf-8")


def ask_llm(ticket):
    """The same question to a chat model, asking it to report confidence."""
    body = json.dumps({"model": MODEL, "stream": False, "format": "json",
                       "options": {"temperature": 0},
                       "messages": [{"role": "user",
                                     "content": PROMPT.format(ticket=ticket)}]}).encode()
    request = urllib.request.Request(OLLAMA, data=body,
                                     headers={"Content-Type": "application/json"})
    start = time.perf_counter()
    with urllib.request.urlopen(request, timeout=300) as response:
        answer = json.loads(response.read())
    spent = time.perf_counter() - start
    parsed = json.loads(answer["message"]["content"])
    return bool(parsed.get("refund", False)), float(parsed.get("confidence", 0)), spent


def main():
    kev.session()

    rows = []
    for ticket, wanted in TICKETS:
        start = time.perf_counter()
        sure = kev.noul(SITUATION.format(ticket=ticket), ASK)
        kev_time = time.perf_counter() - start
        said, confidence, llm_time = ask_llm(ticket)
        rows.append((ticket, wanted, sure, kev_time, said, confidence, llm_time))

    kev_right = sum((r[2] >= 0.5) == r[1] for r in rows)
    llm_right = sum(r[4] == r[1] for r in rows)
    print(f"kev       {kev_right}/{len(rows)} right, "
          f"{sum(r[3] for r in rows) / len(rows) * 1000:6.0f} ms each")
    print(f"llama3.2  {llm_right}/{len(rows)} right, "
          f"{sum(r[6] for r in rows) / len(rows) * 1000:6.0f} ms each")

    # The real question. A number you can set a threshold on has to take
    # more than a handful of values.
    kev_values = sorted({round(r[2], 3) for r in rows})
    llm_values = sorted({r[5] for r in rows})
    print()
    print(f"kev gave {len(kev_values)} different numbers, "
          f"from {min(kev_values):.3f} to {max(kev_values):.3f}")
    print(f"llama3.2 gave {len(llm_values)}: {llm_values}")

    print()
    print(f"{'ticket':46} {'want':>5} {'kev':>7} {'llm':>6} {'conf':>6}")
    print("-" * 76)
    for ticket, wanted, sure, _, said, confidence, _ in rows:
        mark = " " if (sure >= 0.5) == wanted else "K"
        mark = mark + (" " if said == wanted else "L")
        print(f"{mark} {ticket[:44]:44} {str(wanted):>5} {sure:7.3f} "
              f"{str(said):>6} {confidence:6.2f}")


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · accuracy, then the numbers themselves

kev       10/15 right,    457 ms each
llama3.2  15/15 right,   2150 ms each

kev gave 15 different numbers, from 0.008 to 0.991
llama3.2 gave 4: [0.2, 0.8, 0.9, 1.0]
kev-0.6bkev-0.6bkev-0.6bllama3.2llama3.2llama3.2
Rightถูก对的kev: 10 of 15kev: 10 จาก 15kev:15 里 10llama3.2: 15 of 15llama3.2: 15 จาก 15llama3.2:15 里 15
Time eachเวลาต่อใบ每条耗时kev: 457 mskev: 457 mskev:457 毫秒llama3.2: 2,150 msllama3.2: 2,150 msllama3.2:2,150 毫秒
Distinct confidencesค่าความมั่นใจที่ต่างกัน不同的置信度个数kev: 15 of themkev: 15 ค่าkev:15 个llama3.2: 4 of themllama3.2: 4 ค่าllama3.2:4 个
THE CHAT MODEL WAS RIGHT MORE OFTEN AND TOLD YOU NOTHING It answered 0.80 for eleven of the fifteen tickets. Easy ones and hard ones alike. There is no threshold you can set on that column. It barely moves. The small model was wrong more often. Its number spread across the whole range. Step 4 is what that buys you. มันตอบ 0.80 ไปสิบเอ็ดจากสิบห้าใบ ทั้งใบที่มันว่าง่ายและใบที่มันว่ายาก คุณตั้งเกณฑ์อะไรกับคอลัมน์นั้นไม่ได้เลย เพราะมันแทบไม่ขยับ ส่วนโมเดลเล็กผิดบ่อยกว่า แต่ตัวเลขของมันกระจายเต็มช่วง ขั้นที่ 4 คือสิ่งที่คุณได้จากตรงนั้น 十五条里它有十一条答 0.80,简单的和难的都一样。你没法在那一列上设任何阈值,因为它几乎不动。小模型错得更多,但它的数字铺满了整个区间。第 4 步就是这件事换来的东西。
YOUR TURN
  1. Run it. Count how many times llama3.2 says 0.80.
  2. Ask it for a confidence to three decimal places. Does it help?
  3. Add five tickets of your own to tickets.py.
  4. Does the spread hold up on yours?
  1. รันดู แล้วนับดูว่า llama3.2 ตอบ 0.80 กี่ครั้ง
  2. ลองขอค่าความมั่นใจเป็นทศนิยมสามตำแหน่ง ช่วยไหม
  3. เพิ่มตั๋วของคุณเองห้าใบลงใน tickets.py
  4. การกระจายตัวยังอยู่ไหมกับตั๋วของคุณ
  1. 运行。数一数 llama3.2 说了几次 0.80。
  2. 让它给到小数点后三位。有用吗?
  3. 往 tickets.py 里加五条你自己的。
  4. 在你的数据上,那个分布还成立吗?

4 What a real probability is for · ความน่าจะเป็นจริง ๆ มีไว้ทำอะไร · 真正的概率是拿来做什么的

🇬🇧 English

Route the tickets to a team. Send the confident ones straight through and put the rest in front of a person.

Then sweep the threshold, which is the whole product decision in one table.

🇹🇭 ไทย

ส่งตั๋วไปให้ทีมที่ควรรับ ใบที่มั่นใจก็ปล่อยผ่านไปเลย ที่เหลือเอาไปวางตรงหน้าคน

แล้วกวาดค่าเกณฑ์ดู นั่นคือการตัดสินใจเรื่องผลิตภัณฑ์ทั้งหมดในตารางเดียว

🇨🇳 中文

把工单派给对应的组。有把握的直接放行,其余的送到人面前。

然后扫一遍阈值,整个产品决策就在这一张表里。

📄 NEW FILE · route1.py

"""route1.py — the choice type, and what a real probability is for.

A number between 0 and 1 is only worth having if you do something
different at different values. This sends the sure ones straight through
and puts the rest in front of a person.
"""
import sys
import time

import kev
from tickets import TEAMS as WANTED
from tickets import TICKETS

SITUATION = "A support desk is reading a customer message:\n{ticket}"

TEAMS = {"ask": "Which team should handle this message?",
         "options": ["billing: charges, refunds, invoices, prices",
                     "technical support: bugs, crashes, outages, logins",
                     "sales: discounts, new accounts, upgrades",
                     "help: how do I questions about using the product"]}

# Act above this. Below it, a person looks. Chosen by reading the numbers
# below, not by taste.
SURE_ENOUGH = 0.80

sys.stdout.reconfigure(encoding="utf-8")


def route(ticket):
    """Pick a team, and say how sure that pick was."""
    answers = kev.ask(SITUATION.format(ticket=ticket), [TEAMS])[0]
    best = max(range(len(answers)), key=lambda i: answers[i])

    # The team name is everything before the colon.
    return TEAMS["options"][best].split(":")[0], float(answers[best])


def main():
    kev.session()
    started = time.perf_counter()

    picks = []
    for (ticket, _), wanted in zip(TICKETS, WANTED):
        team, sure = route(ticket)
        picks.append((sure, team, wanted, ticket))
        if sure >= SURE_ENOUGH:
            what = "-> " + team
        else:
            what = "?? " + team + ", ask a person"
        mark = " " if team == wanted else "X"
        print(f"{mark} {sure:5.3f}  {what:34} {ticket[:38]}")

    spent = time.perf_counter() - started
    print()
    print(f"{spent / len(TICKETS) * 1000:.0f} ms per ticket")
    print()

    # The threshold is the whole product decision. Higher means fewer
    # handled on their own, and fewer mistakes let through.
    print(f"{'act above':>10} {'routed':>7} {'of those, right':>17}")
    for cut in (0.60, 0.70, 0.80, 0.90, 0.95):
        acted = [p for p in picks if p[0] >= cut]
        right = sum(1 for sure, team, wanted, _ in acted if team == wanted)
        print(f"{cut:10.2f} {len(acted):7} {right:10} of {len(acted)}")


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · the sweep, at the bottom

 act above  routed   of those, right
      0.60      13         11 of 13
      0.70      10          9 of 10
      0.80       7          6 of 7
      0.90       5          5 of 5
      0.95       4          4 of 4

🇬🇧 English

Every row is a real product. Route thirteen and let two mistakes through, or route five and let none.

That curve is what the number was for. With the chat model's four values you cannot draw it at all.

🇹🇭 ไทย

ทุกแถวคือผลิตภัณฑ์จริงหนึ่งแบบ จะส่งอัตโนมัติสิบสามใบแล้วยอมให้ผิดสองใบ หรือส่งห้าใบแล้วไม่ผิดเลย

เส้นโค้งนั้นคือสิ่งที่ตัวเลขมีไว้ทำ ถ้ามีแค่สี่ค่าแบบโมเดลแชต คุณวาดมันไม่ได้เลย

🇨🇳 中文

每一行都是一个真实的产品选择。自动派十三条、放过两个错误,或者只派五条、一个错都没有。

那条曲线就是这个数字的用处。用聊天模型那四个值,你根本画不出来。

YOUR TURN
  1. Run it. Pick the row you would actually ship.
  2. Change SURE_ENOUGH to that number.
  3. Describe one team better and run it again.
  4. Did the curve move, or just one ticket?
  1. รันดู แล้วเลือกแถวที่คุณจะเอาไปใช้จริง
  2. เปลี่ยน SURE_ENOUGH เป็นตัวเลขนั้น
  3. อธิบายทีมหนึ่งทีมให้ดีขึ้น แล้วรันใหม่
  4. เส้นโค้งขยับ หรือขยับแค่ตั๋วใบเดียว
  1. 运行。挑一行你真的会上线的。
  2. 把 SURE_ENOUGH 改成那个数。
  3. 把其中一个组描述得更清楚,再跑一次。
  4. 是曲线动了,还是只有一条工单动了?

🪤 Tutorial 3 — Where it falls over · บทที่ 3 — มันพังตรงไหน · 教程 3 —— 它在哪里翻车

🇬🇧 English

Five of the fifteen went wrong. They are not random, and the pattern is worth more than the score.

🇹🇭 ไทย

ห้าจากสิบห้าใบผิด และมันไม่ได้ผิดแบบสุ่ม รูปแบบของมันมีค่ามากกว่าตัวคะแนน

🇨🇳 中文

十五条里错了五条。它们不是随机错的,那个规律比分数本身更值钱。

5 It scores the topic, not the request · มันให้คะแนนหัวข้อ ไม่ใช่คำขอ · 它打的是主题的分,不是请求的分

🇬🇧 English

Sort the failures into two piles and the whole thing becomes obvious.

🇹🇭 ไทย

แยกสิ่งที่ผิดออกเป็นสองกอง แล้วทุกอย่างจะชัดขึ้นมาทันที

🇨🇳 中文

把出错的分成两堆,整件事就一目了然。

📄 NEW FILE · where1.py

"""where1.py — the five it got wrong, and the pattern in them.

A model that gives you a dial is only useful if you know which way it
bends. These are sure1.py's failures, sorted into the two shapes they
actually take.
"""
import sys

import kev

sys.stdout.reconfigure(encoding="utf-8")

SITUATION = "A support desk is reading a customer message:\n{ticket}"
ASK = "Is this customer asking for a refund?"

# About refunds, but not asking for one. The model scores the topic.
TOPIC_NOT_INTENT = [
    "What is your refund policy, out of interest?",
    "Do you offer a student discount before I subscribe?",
    "I read your refund terms and they seem fair.",
]

# Asking for one, without sounding like it. Plain words, no keywords.
INTENT_NOT_TOPIC = [
    "Send the money back to the same card, thanks.",
    "You took 890 baht twice. Put one back please.",
    "I paid for a year and used it once. Can we undo that?",
]

# For contrast: says it plainly, and is scored plainly.
PLAIN = ["I was charged twice. Please refund the extra one."]


def show(label, tickets, wanted):
    print(label)
    for ticket in tickets:
        sure = kev.noul(SITUATION.format(ticket=ticket), ASK)
        mark = " " if (sure >= 0.5) == wanted else "X"
        print(f"  {mark} {sure:5.3f}  {ticket}")
    print()


def main():
    kev.session()
    show("asking plainly (wants a high number)", PLAIN, True)
    show("about refunds, not asking (wants a low number)",
         TOPIC_NOT_INTENT, False)
    show("asking, but never says refund (wants a high number)",
         INTENT_NOT_TOPIC, True)


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · the pattern, in three groups

asking plainly (wants a high number)
    0.994  I was charged twice. Please refund the extra one.

about refunds, not asking (wants a low number)
  X 0.979  What is your refund policy, out of interest?
  X 0.599  I read your refund terms and they seem fair.

asking, but never says refund (wants a high number)
  X 0.031  Send the money back to the same card, thanks.
  X 0.195  You took 890 baht twice. Put one back please.
  X 0.055  I paid for a year and used it once. Can we undo that?
IT IS TRACKING THE WORD, NOT THE MEANING Say refund and ask, and it scores 0.994. Say refund and do not ask, and it still scores 0.979. Ask plainly without the word and it scores 0.031. A 0.6B model has learned that the word is a strong signal. It has not learned to tell discussing a thing from requesting it. Every wrong answer on this page is one of those two shapes. พูดคำว่า refund แล้วขอ มันให้ 0.994 พูดคำว่า refund แต่ไม่ได้ขอ มันก็ยังให้ 0.979 ส่วนขอตรง ๆ โดยไม่มีคำนั้น มันให้ 0.031 โมเดลขนาด 0.6B เรียนรู้ว่าคำนั้นเป็นสัญญาณที่แรงมาก แต่ยังไม่ได้เรียนรู้ความต่างระหว่างการพูดถึงสิ่งหนึ่งกับการขอสิ่งนั้น คำตอบที่ผิดทุกอันในหน้านี้เป็นหนึ่งในสองรูปแบบนั้น 说了 refund 又在要,它给 0.994。说了 refund 但没在要,它照样给 0.979。直接在要但没说那个词,它给 0.031。一个 0.6B 的模型学会了那个词是个很强的信号,但没学会「谈论一件事」和「要求一件事」的区别。这一页上每一个错答案都是这两种形状之一。
YOUR TURN
  1. Run it. Write three more of each kind.
  2. Find a sentence that says refund and scores below 0.5.
  3. Find one that asks without the word and scores above 0.5.
  4. Challenge 1 is how you move the whole boundary.
  1. รันดู แล้วเขียนเพิ่มอีกแบบละสามประโยค
  2. หาประโยคที่มีคำว่า refund แต่ได้คะแนนต่ำกว่า 0.5
  3. หาประโยคที่ขอโดยไม่มีคำนั้น แต่ได้คะแนนเกิน 0.5
  4. โจทย์ข้อ 1 คือวิธีขยับเส้นแบ่งทั้งเส้น
  1. 运行。每一类再写三句。
  2. 找一句带 refund 但分数低于 0.5 的。
  3. 找一句不带那个词、但分数高于 0.5 的。
  4. 第 1 题就是怎么把整条边界挪动。

☁️ Tutorial 4 — Jev, the hosted one · บทที่ 4 — Jev ตัวที่โฮสต์ไว้ · 教程 4 —— Jev,托管的那个

🇬🇧 English

Same contract, a bigger model, somebody else's computer, and a key.

The file builds the request whether or not you have one, so you can read the shape for nothing.

🇹🇭 ไทย

สัญญาเดียวกัน โมเดลใหญ่กว่า คอมพิวเตอร์ของคนอื่น และต้องมีคีย์

ไฟล์นี้สร้างคำขอไม่ว่าคุณจะมีคีย์หรือไม่ คุณจึงอ่านรูปแบบของมันได้ฟรี ๆ

🇨🇳 中文

同一份契约,更大的模型,别人的电脑,还要一把密钥。

这个文件不管你有没有密钥都会把请求构造出来,所以你可以免费读到它的形状。

6 The request, and what it costs · หน้าตาของคำขอ และราคาของมัน · 请求长什么样,以及它多少钱

🇬🇧 English

Jev speaks the ordinary chat shape with one extra field. response_format carries the questions.

Each one is named, typed, and given its options. That is the same three types from step 2.

🇹🇭 ไทย

Jev พูดรูปแบบแชตธรรมดา บวกฟิลด์เพิ่มมาหนึ่งอัน response_format เป็นตัวพาคำถามไป

แต่ละคำถามมีชื่อ มีชนิด และมีตัวเลือกของมัน ก็คือสามชนิดเดียวกับในขั้นที่ 2

🇨🇳 中文

Jev 说的是普通的聊天格式,多一个字段。response_format 负责带着那些问题。

每个问题有名字、有类型、有自己的选项。就是第 2 步那三种类型。

📄 NEW FILE · jev1.py

"""jev1.py — the hosted model, and the exact shape of the request.

Everything up to here ran on your own machine with the open model. Jev is
TypeSafe's hosted one, and it needs a key. This file builds the request
either way, prints it, and only sends it if a key is there.

So you can read the contract without paying for it, and the day you do
have a key nothing about the file changes.
"""
import json
import os
import sys
import urllib.error
import urllib.request

# Jev speaks the OpenAI chat shape, with one extra field. Providers differ
# only in the address and the model name.
PROVIDERS = {
    "openrouter": ("https://openrouter.ai/api/v1/chat/completions",
                   "typesafe/jev-1.13"),
    "requesty": ("https://router.requesty.ai/v1/chat/completions",
                 "typesafe/jev-latest"),
}
WHICH = os.environ.get("JEV_PROVIDER", "openrouter")
KEY = os.environ.get("JEV_API_KEY")

TICKET = ("I was charged twice for the same order and nobody answers my "
          "emails. I want my money back today or I am calling the bank.")

sys.stdout.reconfigure(encoding="utf-8")


def build(state):
    """The request body. This is the whole difference from a chat call.

    response_format carries the questions. Each one is named, typed, and
    given its options as criteria. Jev answers all of them at once, the
    same way Kev does, because Kev copied this contract.
    """
    url, model = PROVIDERS[WHICH]
    body = {
        "model": model,
        "messages": [{"role": "user", "content": state}],
        "response_format": {
            "type": "questions",
            "questions": {
                "team": {
                    "type": "choice",
                    "instructions": "Which team should handle this ticket?",
                    "criteria": {
                        "billing": "charges, refunds, invoices",
                        "technical support": "bugs, outages, login problems",
                        "sales": "pricing and new accounts",
                    },
                },
                "anger": {
                    "type": "score",
                    "instructions": "How angry is this customer?",
                    "criteria": {
                        "calm": "no sign of irritation",
                        "annoyed": "mildly put out",
                        "frustrated": "clearly unhappy",
                        "furious": "threatening to escalate",
                    },
                },
                "chargeback": {
                    "type": "noul",
                    "instructions": "Is the customer threatening a chargeback?",
                },
            },
        },
    }
    return url, body


def main():
    url, body = build(TICKET)
    print("POST", url)
    print(json.dumps(body, indent=2)[:900])
    print()

    if not KEY:
        # Nothing to apologise for. The contract above is the lesson, and
        # it is the same contract the local model answers in step 2.
        print("No JEV_API_KEY set, so nothing was sent.")
        print("Set one from openrouter.ai or requesty.ai and run it again.")
        print("Pricing at the time of writing: $0.042 per million input")
        print("tokens, output free. A ticket like this is a few hundred.")
        return

    request = urllib.request.Request(
        url, data=json.dumps(body).encode(),
        headers={"Authorization": "Bearer " + KEY,
                 "Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(request, timeout=60) as response:
            answer = json.load(response)
    except urllib.error.HTTPError as refused:
        print("refused:", refused.code, refused.read().decode()[:300])
        return

    # The answers arrive as JSON inside the assistant message, keyed by
    # the names you gave the questions.
    print(json.dumps(json.loads(answer["choices"][0]["message"]["content"]),
                     indent=2)[:700])


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · with no key set

POST https://openrouter.ai/api/v1/chat/completions
{
  "model": "typesafe/jev-1.13",
  "messages": [{"role": "user", "content": "I was charged twice..."}],
  "response_format": {
    "type": "questions",
    "questions": {
      "team":       {"type": "choice", "criteria": {...}},
      "anger":      {"type": "score",  "criteria": {...}},
      "chargeback": {"type": "noul"}
    }
  }
}

No JEV_API_KEY set, so nothing was sent.
Jev, hostedJev แบบโฮสต์Jev,托管Kev, hereKev ที่เครื่องนี้Kev,在本机
Where it runsรันที่ไหน在哪跑Jev: TypeSafe, or a routerJev: TypeSafe หรือผ่าน routerJev:TypeSafe,或某个 routerKev: this laptop, no networkKev: แล็ปท็อปเครื่องนี้ ไม่ต้องมีเน็ตKev:这台笔记本,不用联网
What it costsราคาเท่าไหร่多少钱Jev: $0.042 per million inJev: 0.042 ดอลลาร์ต่อล้านโทเคนเข้าJev:每百万输入 0.042 美元Kev: a 375 MB downloadKev: ดาวน์โหลด 375 MBKev:下载 375 MB
Reported accuracyความแม่นที่รายงานไว้报告的准确率Jev: 0.857 out of domainJev: 0.857 นอกโดเมนJev:域外 0.857Kev: 0.620 out of domainKev: 0.620 นอกโดเมนKev:域外 0.620
THE ACCURACY ROW IS THEIR NUMBER, NOT MINE 0.857 and 0.620 both come from the Kev model card. They were measured on its own held-out sets. Nothing on this page tested Jev. Testing it needs a key and a card. Everything else here was run. Treat the gap as the thing you would be paying for. Measure it yourself before you believe it. ทั้ง 0.857 และ 0.620 มาจากโมเดลการ์ดของ Kev เอง ซึ่งวัดจากชุดข้อมูลที่กันไว้ของเขาเอง ไม่มีอะไรในหน้านี้ที่ทดสอบ Jev เพราะการทดสอบต้องใช้คีย์กับบัตร ส่วนอย่างอื่นทั้งหมดในหน้านี้ถูกรันจริง ให้มองช่องว่างนั้นเป็นสิ่งที่คุณจะจ่ายเงินซื้อ และวัดเองก่อนจะเชื่อ 0.857 和 0.620 都出自 Kev 自己的模型卡,是在它自己留出的数据集上量的。这一页没有任何东西测过 Jev,因为测它要密钥和信用卡。其余的都真的跑过。把那个差距当成你要花钱买的东西,相信之前先自己量。
YOUR TURN
  1. Run it with no key. Read the JSON it would send.
  2. Compare it with the questions in three1.py.
  3. Change a criteria description. The wire format is the spec.
  4. If you get a key, set JEV_API_KEY and run it again.
  1. รันโดยไม่ต้องมีคีย์ แล้วอ่าน JSON ที่มันจะส่ง
  2. เทียบกับคำถามใน three1.py
  3. ลองเปลี่ยนคำอธิบายใน criteria รูปแบบบนสายคือตัวสเปก
  4. ถ้าได้คีย์มา ให้ตั้ง JEV_API_KEY แล้วรันใหม่
  1. 不带密钥运行,读它要发的 JSON。
  2. 和 three1.py 里的问题对比一下。
  3. 改一条 criteria 的描述。线上的格式就是规格。
  4. 拿到密钥后,设好 JEV_API_KEY 再跑一次。

🏆 Tutorial 5 — Three challenges · บทที่ 5 — โจทย์สามข้อ · 教程 5 —— 三道题

🇬🇧 English

Each one was run before it was written down. Two of them did not go the way I expected.

🇹🇭 ไทย

ทุกข้อถูกรันจริงก่อนจะถูกเขียนลงมา และสองข้อไม่ได้เป็นอย่างที่ผมคิดไว้

🇨🇳 中文

每一道都是先跑过才写下来的。其中两道没按我预期的走。

7 Try first. The answers are folded away. · ลองเองก่อน เฉลยพับซ่อนไว้ · 先自己试。答案折起来放着。

🇬🇧 English

There is no training here and no prompt to tune. Nearly everything you can change is the wording of the question.

🇹🇭 ไทย

ตรงนี้ไม่มีการเทรน และไม่มีพรอมต์ให้ปรับ แทบทุกอย่างที่คุณเปลี่ยนได้คือถ้อยคำของคำถาม

🇨🇳 中文

这里没有训练,也没有 prompt 可调。你几乎唯一能改的,就是问题的措辞。

1Move the boundary by rewording the question · ขยับเส้นแบ่งด้วยการเปลี่ยนถ้อยคำของคำถาม · 靠改问法来挪动边界

🇬🇧 English

Step 5 showed it scoring the word rather than the request. You cannot retrain it. You can only ask differently.

Try three wordings and measure all fifteen tickets each time.

🇹🇭 ไทย

ขั้นที่ 5 แสดงให้เห็นว่ามันให้คะแนนคำ ไม่ใช่คำขอ คุณเทรนมันใหม่ไม่ได้ ทำได้แค่ถามให้ต่างออกไป

ลองสามถ้อยคำ แล้ววัดกับตั๋วทั้งสิบห้าใบทุกครั้ง

🇨🇳 中文

第 5 步显示它打的是词的分,不是请求的分。你没法重新训练它,只能换个问法。

试三种问法,每次都量完十五条工单。

Show the answerดูเฉลย看答案

📄 fix1.py

"""fix1.py — challenge 1: the question is the only dial you have.

where1.py showed the model scoring the word refund rather than the
request. There is no training here and no prompt to tune. All you can
change is how the question is put. So change it, and measure.
"""
import sys

import kev
from tickets import TICKETS

SITUATION = "A support desk is reading a customer message:\n{ticket}"

WORDINGS = {
    "the first attempt":
        "Is this customer asking for a refund?",
    "say what counts as yes":
        "Is this customer asking us to give them money back, in any words?",
    "rule the near miss out":
        "Is this customer asking us to return money they already paid? "
        "Asking about the policy does not count.",
}

sys.stdout.reconfigure(encoding="utf-8")


def score(ask_text):
    """Run every ticket through one wording and count the hits."""
    right, wrong = 0, []
    for ticket, wanted in TICKETS:
        sure = kev.noul(SITUATION.format(ticket=ticket), ask_text)
        if (sure >= 0.5) == wanted:
            right = right + 1
        else:
            wrong.append((sure, wanted, ticket))
    return right, wrong


def main():
    kev.session()
    for label, ask_text in WORDINGS.items():
        right, wrong = score(ask_text)
        print(f"{label:26} {right:2}/{len(TICKETS)}")
        for sure, wanted, ticket in wrong:
            print(f"     {sure:5.3f} wanted {str(wanted):5} {ticket[:44]}")
        print()


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · three wordings, three scores

the first attempt          10/15
say what counts as yes     12/15
rule the near miss out      6/15

🇬🇧 English

The middle one, asking us to give them money back, in any words, fixed three of the keyword misses.

The third is the careful one a person would write. It was the worst by a long way.

🇹🇭 ไทย

อันกลาง คือ asking us to give them money back, in any words แก้การพลาดเพราะคำได้สามใบ

ส่วนอันที่สามคืออันที่เขียนอย่างระมัดระวังแบบที่คนจะเขียน และมันแย่ที่สุดแบบทิ้งห่าง

🇨🇳 中文

中间那个,asking us to give them money back, in any words,修好了三个因为关键词而漏掉的。

第三个是人会写的那种小心谨慎的写法。它差得最远。

THE CAREFUL WORDING MADE IT MUCH WORSE The third wording added asking about the policy does not count. That is what you would write in a brief for a colleague. It took the score from 10 to 6. Every number bunched between 0.35 and 0.67. A 0.6B model cannot hold an exception in its head. The extra clause just blurred the question. Short and broad beat long and precise, every time. Asking about the policy does not count คือประโยคที่คุณจะเขียนเพิ่มในบรีฟให้เพื่อนร่วมงาน และมันทำให้คะแนนตกจาก 10 เหลือ 6 ตัวเลขทุกตัวไปกองอยู่ระหว่าง 0.35 ถึง 0.67 โมเดลขนาด 0.6B เก็บข้อยกเว้นไว้ในหัวไม่ไหว อนุประโยคที่เพิ่มเข้าไปแค่ทำให้คำถามพร่ามัว สั้นและกว้างชนะยาวและแม่นยำ ทุกครั้งที่ผมลอง Asking about the policy does not count 正是你写给同事的说明里会加的那一句,而它把分数从 10 拉到了 6。所有数字都挤在 0.35 到 0.67 之间。一个 0.6B 的模型脑子里放不下一个例外,多出来的那个从句只是把问题弄糊了。我每次试,短而宽都赢过长而精确。

2Let the fast model answer what it is sure of · ให้โมเดลที่เร็วตอบเฉพาะที่มันมั่นใจ · 让快的那个只回答它有把握的

🇬🇧 English

Kev is about five times faster and gets fewer right. Instead of choosing, use the number.

Act on Kev outside a band, and spend the chat model only on the middle. Measure what you actually save.

🇹🇭 ไทย

Kev เร็วกว่าราวห้าเท่า แต่ถูกน้อยกว่า อย่าเลือกข้าง ให้ใช้ตัวเลขแทน

เชื่อ Kev เมื่ออยู่นอกช่วง แล้วจ่ายให้โมเดลแชตเฉพาะตรงกลาง แล้ววัดว่าประหยัดได้จริงเท่าไหร่

🇨🇳 中文

Kev 快大约五倍,但对得更少。别二选一,用那个数字。

在区间之外就信 Kev,只在中间那段花钱请聊天模型。然后量一量到底省了多少。

Show the answerดูเฉลย看答案

📄 gate1.py

"""gate1.py — challenge 2: let the fast model answer what it is sure of.

Kev is about five times faster and gets fewer right. Instead of choosing
one, use the number: act on Kev where it is confident, and spend the slow
model only on the middle.
"""
import json
import sys
import time
import urllib.request

import kev
from tickets import TICKETS

OLLAMA = "http://localhost:11434/api/chat"
MODEL = "llama3.2"

SITUATION = "A support desk is reading a customer message:\n{ticket}"
ASK = "Is this customer asking us to give them money back, in any words?"
PROMPT = (SITUATION + "\n\n" + ASK + "\nAnswer with JSON only:\n"
          '{{"refund": true or false}}')

sys.stdout.reconfigure(encoding="utf-8")


def ask_llm(ticket):
    body = json.dumps({"model": MODEL, "stream": False, "format": "json",
                       "options": {"temperature": 0},
                       "messages": [{"role": "user",
                                     "content": PROMPT.format(ticket=ticket)}]}).encode()
    request = urllib.request.Request(OLLAMA, data=body,
                                     headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(request, timeout=300) as response:
        answer = json.loads(response.read())
    return bool(json.loads(answer["message"]["content"]).get("refund", False))


def main():
    kev.session()

    # Score everything once with both, then try bands without paying again.
    scored = []
    kev_total, llm_total = 0.0, 0.0
    for ticket, wanted in TICKETS:
        start = time.perf_counter()
        sure = kev.noul(SITUATION.format(ticket=ticket), ASK)
        kev_total += time.perf_counter() - start

        start = time.perf_counter()
        said = ask_llm(ticket)
        llm_total += time.perf_counter() - start
        scored.append((sure, said, wanted))

    kev_ms = kev_total / len(TICKETS) * 1000
    llm_ms = llm_total / len(TICKETS) * 1000
    print(f"kev alone       {sum((s >= 0.5) == w for s, _, w in scored)}"
          f"/{len(scored)}   {kev_ms:6.0f} ms each")
    print(f"llama3.2 alone  {sum(d == w for _, d, w in scored)}"
          f"/{len(scored)}   {llm_ms:6.0f} ms each")
    print()

    print(f"{'band':>14} {'right':>7} {'asked the llm':>15} {'mean ms':>9}")
    for low, high in [(0.5, 0.5), (0.3, 0.7), (0.2, 0.8), (0.1, 0.9), (0.05, 0.95)]:
        right, sent = 0, 0
        for sure, said, wanted in scored:
            if sure <= low or sure >= high:
                call = sure >= 0.5
            else:
                call = said
                sent = sent + 1
            right += int(call == wanted)
        mean = kev_ms + (sent / len(scored)) * llm_ms
        print(f"  {low:.2f}-{high:.2f} {right:8}/{len(scored)} "
              f"{sent:10}/{len(scored)} {mean:9.0f}")


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · the band, and what it buys

kev alone       12/15      447 ms each
llama3.2 alone  15/15     1006 ms each

          band   right   asked the llm   mean ms
  0.50-0.50       12/15          0/15       447
  0.30-0.70       13/15          3/15       648
  0.20-0.80       13/15          5/15       783
  0.10-0.90       14/15          7/15       917
  0.05-0.95       14/15         10/15      1118
THE WIDEST BAND IS WORSE THAN NOT BOTTHERING Look at the last row. At 0.05 to 0.95 it asks the chat model ten times out of fifteen. That takes 1,118 ms and still gets only 14. Using the chat model alone is faster at 1,006 ms and gets 15. Past a point the gate costs more than it saves. It never reaches the big model's accuracy. One of the small model's errors is confident. The useful row is 0.30 to 0.70: 35% faster, one more mistake. ดูแถวสุดท้าย ที่ช่วง 0.05 ถึง 0.95 มันถามโมเดลแชตสิบจากสิบห้าครั้ง ใช้เวลา 1,118 ms และยังได้แค่ 14 ในขณะที่ใช้โมเดลแชตอย่างเดียวเร็วกว่าที่ 1,006 ms และได้ 15 เลยจุดหนึ่งไป ตัวกั้นแพงกว่าที่มันช่วยประหยัด และมันไม่มีทางไปถึงความแม่นของโมเดลใหญ่ เพราะหนึ่งในความผิดของโมเดลเล็กนั้นมั่นใจมาก แถวที่ใช้ได้จริงคือ 0.30 ถึง 0.70 เร็วขึ้น 35% ผิดเพิ่มหนึ่งใบ 看最后一行。0.05 到 0.95 这一档,十五条里有十条要问聊天模型,花 1,118 毫秒,还是只对 14 条。而只用聊天模型更快,1,006 毫秒,对 15 条。过了某个点,这道闸门花的比省的多,而且它永远追不上大模型的准确率,因为小模型的错误里有一个是很自信的。真正有用的是 0.30 到 0.70 那行:快 35%,多错一条。

3The same model, two machines, different numbers · โมเดลเดียวกัน สองเครื่อง ตัวเลขคนละอย่าง · 同一个模型,两台机器,不同的数字

🇬🇧 English

Run it here, run it on another computer, and compare the scores ticket by ticket.

A 4-bit model does its arithmetic slightly differently on different processors.

🇹🇭 ไทย

รันที่นี่ รันบนอีกเครื่อง แล้วเทียบคะแนนทีละใบ

โมเดลแบบ 4 บิตคำนวณต่างกันเล็กน้อยบนหน่วยประมวลผลคนละแบบ

🇨🇳 中文

在这儿跑一遍,在另一台电脑上跑一遍,一条一条比分数。

一个 4 位量化的模型,在不同处理器上算出来会略有差别。

Show the answerดูเฉลย看答案

📄 drift1.py

"""drift1.py — challenge 3: the same model, two machines, different numbers.

Run this on one computer, then on another, then run it again on either.
It compares the two files. A 4-bit model does its arithmetic slightly
differently on different processors, and near the middle that changes
the answer.
"""
import json
import platform
import sys
from pathlib import Path

import kev
from tickets import TICKETS

SITUATION = "A support desk is reading a customer message:\n{ticket}"
ASK = "Is this customer asking us to give them money back, in any words?"

HERE = Path(__file__).parent
MINE = HERE / ("scores-" + platform.machine().lower() + ".json")

sys.stdout.reconfigure(encoding="utf-8")


def main():
    kev.session()
    scores = [kev.noul(SITUATION.format(ticket=t), ASK) for t, _ in TICKETS]
    MINE.write_text(json.dumps(scores), encoding="utf-8")
    print(f"wrote {MINE.name} on {platform.machine()}")

    others = sorted(p for p in HERE.glob("scores-*.json") if p != MINE)
    if not others:
        print("run it on another machine, copy the file here, run it again")
        return

    other = others[0]
    theirs = json.loads(other.read_text(encoding="utf-8"))
    print(f"comparing with {other.name}")
    print()

    flips, biggest = 0, 0.0
    for (ticket, _), mine, theirsb in zip(TICKETS, scores, theirs):
        gap = abs(mine - theirsb)
        biggest = max(biggest, gap)
        if (mine >= 0.5) != (theirsb >= 0.5):
            flips = flips + 1
            print(f"  FLIPPED  {mine:5.3f} -> {theirsb:5.3f}  {ticket[:42]}")
        elif gap > 0.05:
            print(f"           {mine:5.3f} -> {theirsb:5.3f}  {ticket[:42]}")

    print()
    print(f"{flips} of {len(TICKETS)} answers changed side")
    print(f"biggest difference in the numbers: {biggest:.3f}")


if __name__ == "__main__":
    main()

🖥️ WHAT IT PRINTED · an x86 laptop against an arm64 box

comparing with scores-aarch64.json

           0.668 -> 0.594  I was charged twice for the same order
           0.211 -> 0.287  Can you send me an invoice for last month
           0.476 -> 0.408  Do you offer a student discount before I
           0.239 -> 0.356  What is your refund policy, out of interest

0 of 15 answers changed side
biggest difference in the numbers: 0.118

🇬🇧 English

No answer flipped here, and one moved by 0.118. That is not nothing.

Whether it flips depends on how many of your cases sit near the line. Look at 0.476 going to 0.408.

A threshold you tuned on one machine is a number you tuned on one machine.

🇹🇭 ไทย

ที่นี่ไม่มีคำตอบไหนพลิก และมีอันหนึ่งขยับไป 0.118 ซึ่งไม่ใช่เรื่องเล็ก

มันจะพลิกหรือไม่ขึ้นอยู่กับว่ามีกี่เคสของคุณที่อยู่ใกล้เส้น ดู 0.476 ที่ขยับไป 0.408

เกณฑ์ที่คุณจูนบนเครื่องหนึ่ง ก็คือตัวเลขที่จูนบนเครื่องนั้นเครื่องเดียว

🇨🇳 中文

这里没有答案翻面,有一条移动了 0.118。那不算小。

会不会翻,取决于你有多少条正好压在线附近。看 0.476 变成 0.408 那条。

你在一台机器上调好的阈值,就只是一台机器上的数字。

🇬🇧 English

Eleven files. The idea is worth keeping even where this model is not: ask for a typed answer, not a sentence.

You stop writing parsers, you stop guessing what confident means, and you get a dial you can turn.

What you do not get, at 0.6B, is judgement. Measure before you trust it, on your own sentences.

🇹🇭 ไทย

สิบเอ็ดไฟล์ แนวคิดนี้ควรเก็บไว้แม้ในที่ที่โมเดลตัวนี้ใช้ไม่ได้ คือขอคำตอบที่มีชนิด ไม่ใช่ขอประโยค

คุณจะเลิกเขียนตัวแกะข้อความ เลิกเดาว่าคำว่ามั่นใจแปลว่าอะไร และได้ปุ่มหมุนมาหนึ่งอัน

สิ่งที่คุณไม่ได้จากขนาด 0.6B คือวิจารณญาณ ให้วัดก่อนจะเชื่อ ด้วยประโยคของคุณเอง

🇨🇳 中文

十一个文件。这个想法值得留着,哪怕这个模型不行:要一个带类型的答案,不要一句话。

你不用再写解析器,不用再猜「自信」是什么意思,而且你拿到了一个可以转的旋钮。

在 0.6B 这个尺寸上你拿不到的,是判断力。用你自己的句子,量过再信。

You wantคุณอยากได้你想要Look upไปหาคำว่า去查
More accuracy, same contractแม่นขึ้น สัญญาเดิม更准,同一份契约kev-4b or kev-8bkev-4b หรือ kev-8bkev-4b 或 kev-8b
The hosted oneตัวที่โฮสต์ไว้托管的那个openrouter.ai, requesty.aiopenrouter.ai, requesty.aiopenrouter.ai、requesty.ai
In a browser, no serverในเบราว์เซอร์ ไม่ต้องมีเซิร์ฟเวอร์在浏览器里,不要服务器transformers.js, the q4f16 buildtransformers.js บิลด์ q4f16transformers.js,q4f16 那个构建
Why the numbers behaveทำไมตัวเลขถึงเป็นแบบนั้น这些数字为什么这样calibration, Brier score, ECEcalibration, Brier score, ECE校准、Brier 分数、ECE

🇬🇧 English

Next: what an embedding is, which fails the same way for the same reason, the mail bot where this question first came up, or agents and tools.

🇹🇭 ไทย

ต่อไป: เอมเบดดิงคืออะไร ซึ่งพังแบบเดียวกันด้วยเหตุผลเดียวกัน บอทอีเมล ที่คำถามนี้โผล่มาครั้งแรก หรือ เอเจนต์และเครื่องมือ

🇨🇳 中文

接下来:嵌入是什么,它以同样的原因、同样的方式出错;那个邮件机器人,这个问题最早就是从那儿来的;或者 智能体和工具。

Fifteen lines, your own address, and an app password. That is the whole trick. · สิบห้าบรรทัด อีเมลของคุณเอง และ app password หนึ่งอัน เคล็ดลับมีแค่นี้ · 十五行代码、你自己的邮箱地址,加一个应用专用密码。诀窍就这些。

All eleven files on this page were run on 25 September 2026 and every output shown is copied from those runs, with one stated exception: Jev itself was never called, because that needs a paid key. Step 6 prints the request it would send and says plainly that nothing went out, and the 0.857 against 0.620 accuracy row is TypeSafe and the Kev model card quoting their own held-out sets, not a measurement from here. Everything about Kev is measured: the fifteen tickets were written before anything was scored against them, and the model got twelve right at 447 ms each where llama3.2 got fifteen at 2,150 ms. Two challenges did not go the way I expected. The careful, precisely-worded question scored 6 of 15 where the short broad one scored 12. And the widest gate band is strictly worse than not gating at all: 1,118 ms for 14 right, against 1,006 ms for 15. Kev ran on an x86 laptop and an arm64 DGX Spark for the drift challenge; no answer flipped on these fifteen, and the largest difference in the numbers was 0.118.