🇬🇧 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.
🎚️ 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
| Line | What 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])
| Line | What 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 变成概率。 |
- Run it. Write a ticket of your own and add it.
- Remove the
SITUATIONwrapper. Pass the bare ticket. - Check: the invoice line goes from 0.082 to about 0.5.
- Put it back. That one paragraph was doing real work.
- รันดู แล้วเขียนตั๋วของคุณเองเพิ่มเข้าไป
- ลบตัวครอบ
SITUATIONออก แล้วส่งตั๋วเปล่า ๆ เข้าไป - ตรวจ: บรรทัดใบแจ้งหนี้จะขยับจาก 0.082 ไปราว 0.5
- ใส่กลับเข้าไป ย่อหน้านั้นทำงานจริงจัง
- 运行。写一条你自己的工单加进去。
- 把
SITUATION这层包装去掉,直接传工单。 - 检查:发票那行会从 0.082 变到 0.5 左右。
- 再加回去。那一段是真的在干活。
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
| Line | What 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.เรียกครั้งเดียว เอกสารถูกอ่านหนึ่งรอบ และทุกคำถามเห็นมัน一次调用。文档只读一遍,每个问题都看得到它。 |
- Run it. All three answers are right. Check them yourself.
- Add a fourth question of your own.
- Reverse the order of the anger levels. Does the answer move?
- Describe the teams better. Watch the top number rise.
- รันดู คำตอบทั้งสามถูกหมด ลองตรวจเองดู
- เพิ่มคำถามที่สี่ของคุณเองเข้าไป
- สลับลำดับระดับความโกรธกลับหลัง คำตอบขยับไหม
- อธิบายทีมให้ดีขึ้น แล้วดูตัวเลขบนสุดสูงขึ้น
- 运行。三个答案都对,自己核一下。
- 加一个你自己的第四个问题。
- 把生气的等级顺序反过来。答案变了吗?
- 把各个团队描述得更清楚,看最高那个数字往上走。
📐 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.6b | llama3.2llama3.2llama3.2 | |
|---|---|---|
| Rightถูก对的 | kev: 10 of 15kev: 10 จาก 15kev:15 里 10 | llama3.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 个 |
- Run it. Count how many times llama3.2 says 0.80.
- Ask it for a confidence to three decimal places. Does it help?
- Add five tickets of your own to
tickets.py. - Does the spread hold up on yours?
- รันดู แล้วนับดูว่า llama3.2 ตอบ 0.80 กี่ครั้ง
- ลองขอค่าความมั่นใจเป็นทศนิยมสามตำแหน่ง ช่วยไหม
- เพิ่มตั๋วของคุณเองห้าใบลงใน
tickets.py - การกระจายตัวยังอยู่ไหมกับตั๋วของคุณ
- 运行。数一数 llama3.2 说了几次 0.80。
- 让它给到小数点后三位。有用吗?
- 往
tickets.py里加五条你自己的。 - 在你的数据上,那个分布还成立吗?
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.
🇹🇭 ไทย
ทุกแถวคือผลิตภัณฑ์จริงหนึ่งแบบ จะส่งอัตโนมัติสิบสามใบแล้วยอมให้ผิดสองใบ หรือส่งห้าใบแล้วไม่ผิดเลย
เส้นโค้งนั้นคือสิ่งที่ตัวเลขมีไว้ทำ ถ้ามีแค่สี่ค่าแบบโมเดลแชต คุณวาดมันไม่ได้เลย
🇨🇳 中文
每一行都是一个真实的产品选择。自动派十三条、放过两个错误,或者只派五条、一个错都没有。
那条曲线就是这个数字的用处。用聊天模型那四个值,你根本画不出来。
- Run it. Pick the row you would actually ship.
- Change
SURE_ENOUGHto that number. - Describe one team better and run it again.
- Did the curve move, or just one ticket?
- รันดู แล้วเลือกแถวที่คุณจะเอาไปใช้จริง
- เปลี่ยน
SURE_ENOUGHเป็นตัวเลขนั้น - อธิบายทีมหนึ่งทีมให้ดีขึ้น แล้วรันใหม่
- เส้นโค้งขยับ หรือขยับแค่ตั๋วใบเดียว
- 运行。挑一行你真的会上线的。
- 把
SURE_ENOUGH改成那个数。 - 把其中一个组描述得更清楚,再跑一次。
- 是曲线动了,还是只有一条工单动了?
🪤 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?
- Run it. Write three more of each kind.
- Find a sentence that says refund and scores below 0.5.
- Find one that asks without the word and scores above 0.5.
- Challenge 1 is how you move the whole boundary.
- รันดู แล้วเขียนเพิ่มอีกแบบละสามประโยค
- หาประโยคที่มีคำว่า refund แต่ได้คะแนนต่ำกว่า 0.5
- หาประโยคที่ขอโดยไม่มีคำนั้น แต่ได้คะแนนเกิน 0.5
- โจทย์ข้อ 1 คือวิธีขยับเส้นแบ่งทั้งเส้น
- 运行。每一类再写三句。
- 找一句带 refund 但分数低于 0.5 的。
- 找一句不带那个词、但分数高于 0.5 的。
- 第 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,或某个 router | Kev: 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.857 | Kev: 0.620 out of domainKev: 0.620 นอกโดเมนKev:域外 0.620 |
- Run it with no key. Read the JSON it would send.
- Compare it with the questions in
three1.py. - Change a criteria description. The wire format is the spec.
- If you get a key, set
JEV_API_KEYand run it again.
- รันโดยไม่ต้องมีคีย์ แล้วอ่าน JSON ที่มันจะส่ง
- เทียบกับคำถามใน
three1.py - ลองเปลี่ยนคำอธิบายใน criteria รูปแบบบนสายคือตัวสเปก
- ถ้าได้คีย์มา ให้ตั้ง
JEV_API_KEYแล้วรันใหม่
- 不带密钥运行,读它要发的 JSON。
- 和
three1.py里的问题对比一下。 - 改一条 criteria 的描述。线上的格式就是规格。
- 拿到密钥后,设好
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,修好了三个因为关键词而漏掉的。
第三个是人会写的那种小心谨慎的写法。它差得最远。
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
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.
🇹🇭 ไทย
ต่อไป: เอมเบดดิงคืออะไร ซึ่งพังแบบเดียวกันด้วยเหตุผลเดียวกัน บอทอีเมล ที่คำถามนี้โผล่มาครั้งแรก หรือ เอเจนต์และเครื่องมือ