🇬🇧 English
Most models answer by writing. They make one word, then the next, then the next. That takes time.
CLM never writes. It turns the situation into numbers. It turns each action into numbers too. Then it picks the closest pair.
Picking is much faster than writing. This page shows why, and what it costs you.
🇹🇭 ไทย
โมเดลส่วนใหญ่ตอบด้วยการเขียน มันสร้างคำหนึ่ง แล้วคำถัดไป แล้วคำถัดไป นั่นใช้เวลา
CLM ไม่เขียนเลย มันเปลี่ยนสถานการณ์ให้เป็นตัวเลข และเปลี่ยนการกระทำแต่ละอย่างเป็นตัวเลขด้วย แล้วมันเลือกคู่ที่ใกล้กันที่สุด
การเลือกเร็วกว่าการเขียนมาก หน้านี้จะบอกว่าทำไม และคุณต้องจ่ายอะไรแลกมา
🇨🇳 中文
大多数模型靠写字来回答。先造一个词,再下一个,再下一个。这要花时间。
CLM 从不写字。它把处境变成数字,也把每个动作变成数字,然后挑出最接近的一对。
挑,比写快得多。这一页说明为什么,以及你要为此付出什么。
| Jev and LayaJev กับ LayaJev 和 Laya | CLM-8BCLM-8BCLM-8B | |
|---|---|---|
| What comes outสิ่งที่ออกมา出来的是什么 | a typed answer it writesคำตอบมีชนิดที่มันเขียนขึ้น它写出来的一个带类型的答案 | a score for each choice you gave itคะแนนของแต่ละตัวเลือกที่คุณให้ไป你给的每个选项各得一个分 |
| Can it invent a new answerมันคิดคำตอบใหม่ได้ไหม它能想出新答案吗 | yesได้能 | no, neverไม่ได้เลย不能,绝对不能 |
| Work per requestงานต่อหนึ่งคำขอ每次请求的工作量 | grows with the answerมากขึ้นตามความยาวคำตอบ随答案变长而变多 | one pass, then cheap sumsรอบเดียว แล้วบวกเลขถูก ๆ跑一遍,然后是便宜的加法 |
| Same choices every timeตัวเลือกเดิมทุกครั้ง每次都是同一批选项 | no help at allไม่ช่วยอะไรเลย一点帮助都没有 | the big win, see step 5นี่คือจุดที่ได้เยอะ ดูขั้นที่ 5这才是大头,看第 5 步 |
ollama pull nomic-embed-text once. That is 274 MB. Every file here uses only Python that comes with Python.
คุณไม่จำเป็นต้องมี CLM-8B ตัวจริง มันต้องการ GPU ใหญ่และเซิร์ฟเวอร์ vLLM คุณแค่ต้องมี Ollama กับโมเดลฝังตัวเล็ก ๆ หนึ่งตัว รัน ollama pull nomic-embed-text ครั้งเดียว ขนาด 274 MB ทุกไฟล์ในหน้านี้ใช้แต่ของที่ติดมากับ Python
你不需要 CLM-8B 本体。它要一块大显卡和一个 vLLM 服务。你只需要 Ollama 和一个小的嵌入模型。跑一次 ollama pull nomic-embed-text,274 MB。这一页的每个文件只用 Python 自带的东西。
🎯 Tutorial 1 — Scoring, not writing · บทที่ 1 — ให้คะแนน ไม่ใช่เขียน · 教程 1 —— 打分,不是写字
🇬🇧 English
First, build the idea with a small model. Three actions, one customer message, three scores.
The first file gets the answer wrong. That is on purpose. The wrong answer is the reason CLM exists.
🇹🇭 ไทย
เริ่มจากสร้างแนวคิดด้วยโมเดลเล็ก ๆ สามการกระทำ ข้อความลูกค้าหนึ่งข้อความ สามคะแนน
ไฟล์แรกตอบผิด และนั่นตั้งใจ คำตอบที่ผิดนั่นแหละคือเหตุผลที่ CLM มีอยู่
🇨🇳 中文
先用一个小模型把想法搭出来。三个动作,一条客户消息,三个分数。
第一个文件会答错。这是故意的。那个错答案,正是 CLM 存在的理由。
1 Three actions, three numbers · สามการกระทำ สามตัวเลข · 三个动作,三个数字
🇬🇧 English
An embedding turns text into a row of numbers. Two texts that mean the same thing get two close rows.
So: embed the customer message. Embed each action. Measure which pair is closest. No writing anywhere.
🇹🇭 ไทย
การฝังตัวเปลี่ยนข้อความให้เป็นแถวตัวเลข ข้อความสองอันที่ความหมายเหมือนกันจะได้แถวที่ใกล้กัน
ดังนั้น ฝังข้อความลูกค้า ฝังการกระทำแต่ละอัน แล้ววัดว่าคู่ไหนใกล้ที่สุด ไม่มีการเขียนที่ไหนเลย
🇨🇳 中文
嵌入把文字变成一行数字。两段意思相同的文字,会得到两行很接近的数字。
所以:把客户消息嵌入,把每个动作嵌入,量一下哪一对最近。全程没有写字。
📄 NEW FILE · clm1.py
"""clm1.py - score every action, then take the best one."""
import json
import math
import urllib.request
OLLAMA = "http://localhost:11434/api/embed"
MODEL = "nomic-embed-text"
def embed(lines):
"""Turn a list of sentences into a list of number rows."""
payload = json.dumps({"model": MODEL, "input": lines}).encode()
request = urllib.request.Request(
OLLAMA, data=payload, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(request, timeout=300) as answer:
return json.load(answer)["embeddings"]
def closeness(one, two):
"""Cosine similarity. 1.0 is the same direction, 0.0 is unrelated."""
dot = sum(a * b for a, b in zip(one, two))
size_one = math.sqrt(sum(a * a for a in one))
size_two = math.sqrt(sum(b * b for b in two))
return dot / (size_one * size_two)
STATE = "my parcel never arrived and I want my money back"
# What the agent is allowed to do. The text after the colon is the whole
# description the model gets. It is written the way a tool list is written.
ACTIONS = [
"open_refund: start a refund for the customer",
"track_parcel: look up where the parcel is now",
"reset_password: send a password reset link",
]
# One call embeds the state and all three actions together.
rows = embed([STATE] + ACTIONS)
state_row = rows[0]
action_rows = rows[1:]
print("one row is", len(state_row), "numbers long")
print()
for action, row in zip(ACTIONS, action_rows):
print(f"{closeness(state_row, row):.4f} {action.split(':')[0]}")
scores = [closeness(state_row, row) for row in action_rows]
print()
print("picked:", ACTIONS[scores.index(max(scores))].split(":")[0])
🖥️ WHAT IT PRINTED · 27 September 2026
one row is 768 numbers long
0.5513 open_refund
0.6262 track_parcel
0.4493 reset_password
picked: track_parcel
| Line | What it does · ทำอะไร · 做什么 |
|---|---|
| embed([STATE] + ACTIONS) | One call, four texts. The list comes back in the same order.เรียกครั้งเดียว สี่ข้อความ ลิสต์กลับมาเรียงลำดับเดิม一次调用,四段文字。列表按原顺序回来。 |
| 768 | How long one row is. Every text becomes this many numbers.ความยาวของหนึ่งแถว ทุกข้อความกลายเป็นตัวเลขจำนวนเท่านี้一行有多长。每段文字都变成这么多个数字。 |
| closeness(one, two) | The angle between two rows. Length does not matter, direction does.มุมระหว่างสองแถว ความยาวไม่สำคัญ ทิศทางสำคัญ两行之间的夹角。长度不重要,方向才重要。 |
| scores.index(max(scores)) | Where the best score sits. That position is the answer.ตำแหน่งของคะแนนที่ดีที่สุด ตำแหน่งนั้นคือคำตอบ最高分在第几个。那个位置就是答案。 |
open_refund. The model chose track_parcel, at 0.6262 against 0.5513. It is not broken. It is doing its job. It measures what a text is about. The message is about a parcel, so the parcel action wins. Nothing in this model knows what an agent ought to do. That gap is the whole reason CLM-8B was trained.
ลูกค้าต้องการเงินคืน การกระทำที่ถูกคือ open_refund แต่โมเดลเลือก track_parcel ที่ 0.6262 เทียบกับ 0.5513 มันไม่ได้เสีย มันทำงานของมัน มันวัดว่าข้อความนั้นเกี่ยวกับอะไร ข้อความนี้เกี่ยวกับพัสดุ การกระทำเรื่องพัสดุจึงชนะ ไม่มีอะไรในโมเดลนี้ที่รู้ว่าเอเจนต์ควรทำอะไร ช่องว่างนั้นคือเหตุผลทั้งหมดที่ CLM-8B ถูกฝึกขึ้นมา
客户想要退钱。正确的动作是 open_refund。模型选了 track_parcel,0.6262 对 0.5513。它没坏,它在做本职工作:它量的是一段文字关于什么。这条消息关于包裹,所以包裹那个动作赢了。这个模型里没有任何东西知道一个智能体该做什么。这道缝,就是 CLM-8B 被训练出来的全部理由。
- Run it. Check you get 768 numbers too.
- Change the state to
"I want a refund please". Now it is right. - Add a fourth action of your own. Watch all four scores.
- Print the scores sorted. Which two are always close?
- รันดู ตรวจว่าคุณได้ 768 ตัวเลขเหมือนกัน
- เปลี่ยนสถานะเป็น
"I want a refund please"ทีนี้มันถูกแล้ว - เพิ่มการกระทำที่สี่ของคุณเอง แล้วดูคะแนนทั้งสี่
- พิมพ์คะแนนแบบเรียงลำดับ สองอันไหนที่ใกล้กันตลอด
- 跑一遍。看看你是不是也得到 768 个数字。
- 把状态换成
"I want a refund please"。现在它对了。 - 自己加第四个动作,看四个分数。
- 把分数排序打出来。哪两个总是挨得很近?
2 Change the words, watch the gap close · เปลี่ยนคำ แล้วดูช่องว่างแคบลง · 换个说法,看差距缩小
🇬🇧 English
The action list was written like a manual. Start a refund. That is what the tool does.
Try writing it as what the customer says instead. The customer wants their money back. Same action, different voice.
🇹🇭 ไทย
รายการการกระทำถูกเขียนแบบคู่มือ เริ่มการคืนเงิน นั่นคือสิ่งที่เครื่องมือทำ
ลองเขียนเป็นสิ่งที่ลูกค้าพูดแทน ลูกค้าต้องการเงินคืน การกระทำเดิม แต่คนละน้ำเสียง
🇨🇳 中文
这份动作清单是照说明书写的。开始退款。那是工具做的事。
试着改成写客户说的话。客户想要他们的钱回来。同一个动作,换一个口吻。
📄 NEW FILE · clm2.py
"""clm2.py - the same actions, described in the caller s own words."""
import json
import math
import urllib.request
OLLAMA = "http://localhost:11434/api/embed"
MODEL = "nomic-embed-text"
def embed(lines):
"""Turn a list of sentences into a list of number rows."""
payload = json.dumps({"model": MODEL, "input": lines}).encode()
request = urllib.request.Request(
OLLAMA, data=payload, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(request, timeout=300) as answer:
return json.load(answer)["embeddings"]
def closeness(one, two):
"""Cosine similarity. 1.0 is the same direction, 0.0 is unrelated."""
dot = sum(a * b for a, b in zip(one, two))
size_one = math.sqrt(sum(a * a for a in one))
size_two = math.sqrt(sum(b * b for b in two))
return dot / (size_one * size_two)
STATE = "my parcel never arrived and I want my money back"
# Written as a tool list: what the function does.
TOOL_WORDS = [
"open_refund: start a refund for the customer",
"track_parcel: look up where the parcel is now",
"reset_password: send a password reset link",
]
# The same three actions, written as what the customer says instead.
STATE_WORDS = [
"open_refund: the customer wants their money back",
"track_parcel: the customer asks where their parcel is",
"reset_password: the customer cannot log in",
]
state_row = embed([STATE])[0]
for label, actions in (("tool words ", TOOL_WORDS),
("state words", STATE_WORDS)):
rows = embed(actions)
scores = [closeness(state_row, row) for row in rows]
refund, parcel = scores[0], scores[1]
winner = actions[scores.index(max(scores))].split(":")[0]
print(f"{label} refund {refund:.4f} parcel {parcel:.4f} "
f"gap {parcel - refund:+.4f} picked {winner}")
🖥️ WHAT IT PRINTED
tool words refund 0.5513 parcel 0.6262 gap +0.0749 picked track_parcel
state words refund 0.6329 parcel 0.6372 gap +0.0043 picked track_parcel
- Run it. Compare the two gaps.
- Try to word the refund action so it finally wins.
- Now check your wording on
"where is my order"too. - Fixing one usually breaks the other. That is the point.
- รันดู เทียบช่องว่างทั้งสองอัน
- ลองเขียนการกระทำเรื่องคืนเงินให้มันชนะให้ได้
- แล้วเอาคำที่คุณเขียนไปลองกับ
"where is my order"ด้วย - แก้อันหนึ่งมักทำอีกอันพัง นั่นแหละประเด็น
- 跑一遍。把两个差距比一比。
- 试着把退款那个动作写到它终于能赢。
- 再把你的写法拿去试
"where is my order"。 - 修好一个,通常会弄坏另一个。这正是重点。
🧠 Tutorial 2 — Why CLM trains two heads · บทที่ 2 — ทำไม CLM ต้องฝึกสองหัว · 教程 2 —— CLM 为什么要训练两个头
🇬🇧 English
Step 2 showed the problem. Now here is the shape of the answer.
No code to run in this step. Read it, then go back to the keyboard in step 4.
🇹🇭 ไทย
ขั้นที่ 2 แสดงปัญหาให้เห็นแล้ว ทีนี้มาดูรูปร่างของคำตอบ
ขั้นนี้ไม่มีโค้ดให้รัน อ่านให้จบ แล้วกลับไปที่แป้นพิมพ์ในขั้นที่ 4
🇨🇳 中文
第 2 步把问题摆出来了。现在看看答案长什么样。
这一步没有代码要跑。读完,第 4 步再回到键盘前。
3 A state head and an action head · หัวสถานะ และหัวการกระทำ · 一个状态头,一个动作头
🇬🇧 English
CLM-8B keeps a big model frozen. It adds two tiny layers on top. One reads the state. One reads the actions.
They are trained together on real agent runs. The training pulls a state and its correct action close. It pushes the wrong ones away.
🇹🇭 ไทย
CLM-8B แช่โมเดลใหญ่ไว้ไม่ให้เปลี่ยน แล้วเติมชั้นเล็ก ๆ สองชั้นไว้ข้างบน ชั้นหนึ่งอ่านสถานะ อีกชั้นอ่านการกระทำ
ทั้งสองถูกฝึกไปด้วยกันบนการทำงานจริงของเอเจนต์ การฝึกจะดึงสถานะกับการกระทำที่ถูกต้องให้เข้ามาใกล้กัน และผลักการกระทำที่ผิดออกไป
🇨🇳 中文
CLM-8B 把一个大模型冻住,在上面加两个很小的层。一个读状态,一个读动作。
它们在真实的智能体运行记录上一起训练。训练会把状态和它正确的动作拉近,把错的动作推开。
| Training stageขั้นการฝึก训练阶段 | What it sawเห็นอะไร看了什么 | Top-1 after itTop-1 หลังจากนั้น之后的 Top-1 |
|---|---|---|
| Pre-trainingฝึกก่อน预训练 | ~60M Nemotron question and answer pairsคู่คำถามคำตอบ Nemotron ~60M คู่约 6000 万对 Nemotron 问答 | 52.1% |
| Mid-trainingฝึกกลาง中期训练 | ~30M hard wrong answers, written by Gemini 2.5 Flash-Liteคำตอบผิดแบบยาก ~30M อัน เขียนโดย Gemini 2.5 Flash-Lite约 3000 万个刁钻的错答案,由 Gemini 2.5 Flash-Lite 写 | 69.2% |
| Post-trainingฝึกหลัง后期训练 | ~1M real agent runsการทำงานจริงของเอเจนต์ ~1M ครั้ง约 100 万次真实智能体运行 | see step 7ดูขั้นที่ 7看第 7 步 |
| Partส่วน部件 | Sizeขนาด大小 | Noteหมายเหตุ说明 |
|---|---|---|
| Base modelโมเดลฐาน底座模型 | Qwen3-8B | frozen, so it is never changedแช่ไว้ จึงไม่เคยถูกเปลี่ยน冻住的,所以从不被改动 |
| The two headsสองหัว两个头 | ~20M each~20M ต่อหัว各约 2000 万 | the whole download is 75 MBไฟล์ที่ต้องโหลดทั้งหมด 75 MB整个下载只有 75 MB |
| Licenceสัญญาอนุญาต许可证 | Apache 2.0 | the heads are open, like Layaสองหัวนี้เปิด เหมือน Laya这两个头是开放的,和 Laya 一样 |
| How much it readsอ่านได้เท่าไร能读多少 | 2048 tokens2048 โทเคน2048 个词元 | short. A long state must be cut down.สั้น สถานะที่ยาวต้องถูกตัดให้สั้นลง很短。长的状态必须先裁掉。 |
⚡ Tutorial 3 — Why it is fast · บทที่ 3 — ทำไมมันถึงเร็ว · 教程 3 —— 它为什么快
🇬🇧 English
Here is the part you can measure on your own machine, today.
The speed does not come from the model being small. It comes from one old idea. Do not do the same work twice.
🇹🇭 ไทย
นี่คือส่วนที่คุณวัดเองบนเครื่องของคุณได้ วันนี้เลย
ความเร็วไม่ได้มาจากการที่โมเดลเล็ก มันมาจากแนวคิดเก่าข้อเดียว คืออย่าทำงานเดิมสองครั้ง
🇨🇳 中文
这一段你今天就能在自己机器上量出来。
快不是因为模型小。快来自一个很老的想法:同样的活别干两遍。
4 Keep the action vectors · เก็บเวกเตอร์ของการกระทำไว้ · 把动作向量留着
🇬🇧 English
Your three actions never change. So embedding them on every request is wasted work.
Embed them once. Keep the rows. After that, only the state is new.
🇹🇭 ไทย
การกระทำสามอย่างของคุณไม่เคยเปลี่ยน ดังนั้นการฝังมันทุกครั้งที่มีคำขอจึงเป็นงานที่เสียเปล่า
ฝังมันครั้งเดียว เก็บแถวไว้ หลังจากนั้นมีแค่สถานะที่ใหม่
🇨🇳 中文
你那三个动作从来不变。所以每次请求都把它们嵌入一遍,是白干。
嵌入一次,把那几行留着。之后只有状态是新的。
📄 NEW FILE · clm3.py
"""clm3.py - embed the actions once, then keep them."""
import json
import math
import time
import urllib.request
OLLAMA = "http://localhost:11434/api/embed"
MODEL = "nomic-embed-text"
def embed(lines):
"""Turn a list of sentences into a list of number rows."""
payload = json.dumps({"model": MODEL, "input": lines}).encode()
request = urllib.request.Request(
OLLAMA, data=payload, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(request, timeout=300) as answer:
return json.load(answer)["embeddings"]
def closeness(one, two):
"""Cosine similarity. 1.0 is the same direction, 0.0 is unrelated."""
dot = sum(a * b for a, b in zip(one, two))
size_one = math.sqrt(sum(a * a for a in one))
size_two = math.sqrt(sum(b * b for b in two))
return dot / (size_one * size_two)
ACTIONS = [
"open_refund: start a refund for the customer",
"track_parcel: look up where the parcel is now",
"reset_password: send a password reset link",
]
STATES = [
"my parcel never arrived and I want my money back",
"where is my order right now",
"I cannot log in any more",
]
ROUNDS = 10
# The plain way. Every request embeds the state and all three actions.
start = time.time()
for turn in range(ROUNDS):
rows = embed([STATES[turn % len(STATES)]] + ACTIONS)
scores = [closeness(rows[0], row) for row in rows[1:]]
plain = (time.time() - start) / ROUNDS
# The CLM way. The actions never change, so embed them once and keep them.
cached = embed(ACTIONS)
start = time.time()
for turn in range(ROUNDS):
state_row = embed([STATES[turn % len(STATES)]])[0]
scores = [closeness(state_row, row) for row in cached]
quick = (time.time() - start) / ROUNDS
print(f"embed the lot every time : {plain * 1000:7.1f} ms a request")
print(f"actions kept, state only : {quick * 1000:7.1f} ms a request")
print(f"times faster : {plain / quick:7.2f}")
🖥️ WHAT IT PRINTED · on a DGX Spark, three actions
embed the lot every time : 33.5 ms a request
actions kept, state only : 16.7 ms a request
times faster : 2.01
| Line | What it does · ทำอะไร · 做什么 |
|---|---|
| cached = embed(ACTIONS) | Runs once, before the loop. This one line is the whole trick.ทำงานครั้งเดียว ก่อนเข้าลูป บรรทัดเดียวนี้คือทริกทั้งหมด在循环之前跑一次。就这一行,是全部的招数。 |
| embed([STATES[...]])[0] | One text now, not four. That is where the time goes.ทีนี้ข้อความเดียว ไม่ใช่สี่ เวลาหายไปตรงนั้นแหละ现在只有一段文字,不是四段。时间就省在这。 |
| closeness(state_row, row) | Plain arithmetic. No model, no network, almost no time.เลขคณิตธรรมดา ไม่มีโมเดล ไม่มีเครือข่าย แทบไม่ใช้เวลา纯算术。没有模型,没有网络,几乎不花时间。 |
| ROUNDS = 10 | Ten runs, then divide. One run alone tells you nothing.รันสิบรอบ แล้วหาร รอบเดียวไม่บอกอะไรคุณเลย跑十次再除。只跑一次什么也说明不了。 |
- Run it. Your machine will give different numbers.
- Run it twice. The second run is usually faster. Why?
- Set
ROUNDS = 1. Now the number jumps around. - Set it back to 10. Averages are how you measure.
- รันดู เครื่องของคุณจะให้ตัวเลขคนละแบบ
- รันสองครั้ง ครั้งที่สองมักเร็วกว่า ทำไม
- ตั้ง
ROUNDS = 1ทีนี้ตัวเลขจะเด้งไปมา - ตั้งกลับเป็น 10 การวัดต้องใช้ค่าเฉลี่ย
- 跑一遍。你的机器会给出不一样的数。
- 跑两次。第二次通常更快。为什么?
- 把
ROUNDS设成 1。现在数字开始乱跳。 - 再改回 10。要量东西,就得用平均值。
5 Thirty actions, three hundred actions · สามสิบ และสามร้อยการกระทำ · 三十个动作,三百个动作
🇬🇧 English
Two times faster is not much. But three actions is a toy. Real agents have dozens or hundreds.
Run the same test at three sizes. Watch what happens to the gap.
🇹🇭 ไทย
เร็วขึ้นสองเท่าไม่เยอะ แต่สามการกระทำคือของเล่น เอเจนต์จริงมีเป็นสิบหรือเป็นร้อย
รันการทดสอบเดิมที่สามขนาด แล้วดูว่าช่องว่างเป็นยังไง
🇨🇳 中文
快两倍不算多。但三个动作是玩具。真正的智能体有几十个甚至几百个。
在三个规模上跑同一个测试,看差距会怎样。
📄 NEW FILE · clm4.py
"""clm4.py - the more actions there are, the more the cache is worth."""
import json
import math
import time
import urllib.request
OLLAMA = "http://localhost:11434/api/embed"
MODEL = "nomic-embed-text"
def embed(lines):
"""Turn a list of sentences into a list of number rows."""
payload = json.dumps({"model": MODEL, "input": lines}).encode()
request = urllib.request.Request(
OLLAMA, data=payload, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(request, timeout=300) as answer:
return json.load(answer)["embeddings"]
def closeness(one, two):
"""Cosine similarity. 1.0 is the same direction, 0.0 is unrelated."""
dot = sum(a * b for a, b in zip(one, two))
size_one = math.sqrt(sum(a * a for a in one))
size_two = math.sqrt(sum(b * b for b in two))
return dot / (size_one * size_two)
STATE = "my parcel never arrived and I want my money back"
ROUNDS = 5
print(f"{'actions':>8} {'no cache':>10} {'cached':>9} {'faster':>7}")
for count in (3, 30, 300):
actions = [f"tool_{n}: handle support job number {n}" for n in range(count)]
start = time.time()
for turn in range(ROUNDS):
rows = embed([STATE] + actions)
scores = [closeness(rows[0], row) for row in rows[1:]]
plain = (time.time() - start) / ROUNDS
cached = embed(actions)
start = time.time()
for turn in range(ROUNDS):
state_row = embed([STATE])[0]
scores = [closeness(state_row, row) for row in cached]
quick = (time.time() - start) / ROUNDS
print(f"{count:8d} {plain * 1000:9.1f}ms {quick * 1000:8.1f}ms "
f"{plain / quick:6.2f}x")
🖥️ WHAT IT PRINTED · on a DGX Spark
actions no cache cached faster
3 32.1ms 13.7ms 2.34x
30 202.4ms 21.1ms 9.60x
300 1560.8ms 29.4ms 53.02x
- Run it. Three sizes, one table.
- Add 1000 to the list of sizes. Be patient.
- Time only the
closenesssums. They are tiny. - So the cost is the embedding, not the comparing.
- รันดู สามขนาด ตารางเดียว
- เพิ่ม 1000 เข้าไปในรายการขนาด ใจเย็น ๆ
- จับเวลาเฉพาะการบวกใน
closenessมันเล็กมาก - แปลว่าต้นทุนอยู่ที่การฝังตัว ไม่ใช่การเปรียบเทียบ
- 跑一遍。三个规模,一张表。
- 在规模列表里加上 1000。耐心点。
- 只给
closeness的加法计时。它非常小。 - 所以代价在嵌入,不在比较。
🚧 Tutorial 4 — Where it breaks · บทที่ 4 — มันพังตรงไหน · 教程 4 —— 它在哪里失灵
🇬🇧 English
A picker always picks. Ask it about nothing on its list. It still hands you something.
That is the danger, and the fix is short.
🇹🇭 ไทย
ตัวเลือกย่อมเลือกเสมอ ถ้าให้คำถามที่ไม่เกี่ยวกับอะไรในรายการเลย มันก็ยังยื่นอะไรสักอย่างมาให้
นั่นคืออันตราย และวิธีแก้ก็สั้น
🇨🇳 中文
会挑的东西总是会挑。给它一个和清单上任何东西都无关的问题,它照样递给你一个。
这就是危险所在,而解法很短。
6 A floor under the best score · ตั้งพื้นไว้ใต้คะแนนที่ดีที่สุด · 给最高分设一条底线
🇬🇧 English
Ask it about the capital of Peru. None of the three actions fit at all.
It still answers. But the score is low, and a low score is information. Set a floor and use it.
🇹🇭 ไทย
ลองถามมันเรื่องเมืองหลวงของเปรู ไม่มีการกระทำไหนในสามอันเข้าเค้าเลย
มันก็ยังตอบ แต่คะแนนต่ำ และคะแนนต่ำคือข้อมูล ตั้งพื้นไว้ แล้วใช้มัน
🇨🇳 中文
问它秘鲁的首都。三个动作没有一个沾边。
它照样答。但分数很低,而低分本身就是信息。设一条底线,然后用它。
📄 NEW FILE · clm5.py
"""clm5.py - a floor, because the best of three may still be wrong."""
import json
import math
import urllib.request
OLLAMA = "http://localhost:11434/api/embed"
MODEL = "nomic-embed-text"
def embed(lines):
"""Turn a list of sentences into a list of number rows."""
payload = json.dumps({"model": MODEL, "input": lines}).encode()
request = urllib.request.Request(
OLLAMA, data=payload, headers={"Content-Type": "application/json"})
with urllib.request.urlopen(request, timeout=300) as answer:
return json.load(answer)["embeddings"]
def closeness(one, two):
"""Cosine similarity. 1.0 is the same direction, 0.0 is unrelated."""
dot = sum(a * b for a, b in zip(one, two))
size_one = math.sqrt(sum(a * a for a in one))
size_two = math.sqrt(sum(b * b for b in two))
return dot / (size_one * size_two)
ACTIONS = [
"open_refund: start a refund for the customer",
"track_parcel: look up where the parcel is now",
"reset_password: send a password reset link",
]
STATES = [
"my parcel never arrived and I want my money back",
"what is the capital of Peru",
]
# Below this, no action is close enough to trust.
FLOOR = 0.60
cached = embed(ACTIONS)
for state in STATES:
state_row = embed([state])[0]
scores = [closeness(state_row, row) for row in cached]
best = max(scores)
name = ACTIONS[scores.index(best)].split(":")[0]
print(f"state : {state}")
print(f"best : {name} at {best:.4f}")
if best < FLOOR:
print(" under the floor, so hand it to a person")
print()
🖥️ WHAT IT PRINTED
state : my parcel never arrived and I want my money back
best : track_parcel at 0.6262
state : what is the capital of Peru
best : track_parcel at 0.4077
under the floor, so hand it to a person
track_parcel for a geography question. With no floor, the agent would have run it. The number 0.4077 was there the whole time, saying "none of these". A picker cannot say no on its own. You have to give it a way to. One if does it.
อ่านคำตอบที่สอง มันเลือก track_parcel ให้คำถามภูมิศาสตร์ ถ้าไม่มีพื้น เอเจนต์ก็จะรันมันไปแล้ว เลข 0.4077 อยู่ตรงนั้นมาตลอด และมันกำลังบอกว่า "ไม่มีอันไหนเลย" ตัวเลือกพูดว่าไม่เองไม่ได้ คุณต้องให้ทางกับมัน if เดียวก็พอ
读第二个答案。它给一道地理题选了 track_parcel。没有底线的话,智能体就真去跑了。0.4077 这个数一直都在,它在说"一个都不合适"。一个只会挑的东西自己说不了"不"。你得给它一条路。一个 if 就够了。
- Run it. Note the two scores.
- Add three questions of your own that fit nothing.
- Write down the highest score any of them got.
- Set
FLOORjust above it. That is how you choose one.
- รันดู จดคะแนนทั้งสองไว้
- เพิ่มคำถามของคุณเองสามข้อที่ไม่เข้ากับอะไรเลย
- จดคะแนนสูงสุดที่ข้อไหนสักข้อทำได้
- ตั้ง
FLOORให้สูงกว่านั้นนิดเดียว นั่นคือวิธีเลือกค่า
- 跑一遍。把两个分数记下来。
- 自己加三个和清单毫无关系的问题。
- 记下它们当中最高的那个分。
- 把
FLOOR设得比它高一点点。底线就是这样定的。
📊 Tutorial 5 — What CLM-8B really scored · บทที่ 5 — CLM-8B ทำคะแนนได้เท่าไรจริง ๆ · 教程 5 —— CLM-8B 到底考了多少
🇬🇧 English
Now the real model. Everything below is published by the authors. We did not run CLM-8B.
The headline says nine times faster. The table says something more useful.
🇹🇭 ไทย
ทีนี้มาดูโมเดลจริง ทุกอย่างข้างล่างนี้ผู้สร้างเป็นคนเผยแพร่ เราไม่ได้รัน CLM-8B เอง
พาดหัวบอกว่าเร็วกว่าเก้าเท่า แต่ตารางบอกอะไรที่มีประโยชน์กว่านั้น
🇨🇳 中文
现在看真正的模型。下面所有内容都是作者发布的。我们没有跑过 CLM-8B。
标题说快九倍。表格说的东西更有用。
7 Where it wins, and where it loses · ชนะตรงไหน และแพ้ตรงไหน · 哪里赢,哪里输
🇬🇧 English
Four tasks, no fine-tuning. Green is the winner in that row. Read the last two columns, not the first two.
🇹🇭 ไทย
สี่งาน ไม่มีการปรับจูน สีเขียวคือผู้ชนะในแถวนั้น อ่านสองคอลัมน์สุดท้าย ไม่ใช่สองคอลัมน์แรก
🇨🇳 中文
四个任务,没有微调。绿色是那一行的赢家。看最后两列,别看前两列。
| Taskงาน任务 | CLM timeเวลา CLMCLM 用时 | Jev timeเวลา JevJev 用时 | CLM got rightCLM ทำถูกCLM 做对 | Jev got rightJev ทำถูกJev 做对 |
|---|---|---|---|---|
| T-Rex gameเกม T-RexT-Rex 小游戏 | 16.5 ms | 149.8 ms | 5/5 | 5/5 |
| Super Marioซูเปอร์มาริโอ超级马力欧 | 33.5 ms | 132.6 ms | 5/5 | 5/5 |
| Tool callingการเรียกเครื่องมือ工具调用 | 76.8 ms | 125.5 ms | 95.2% | 99.2% |
| WikiRacingWikiRacingWikiRacing | 79.8 ms | 225 ms | 26/30 | 30/30 |
🇬🇧 English
Trained for one job, it does better. Here it grades other models' work instead of acting.
🇹🇭 ไทย
ถ้าฝึกมาเพื่องานเดียว มันทำได้ดีกว่า ตรงนี้มันตรวจงานของโมเดลอื่น แทนที่จะลงมือทำเอง
🇨🇳 中文
只为一件事训练过,它表现更好。这里它是在给别的模型的作业打分,不是自己动手。
| Benchmarkชุดทดสอบ测试集 | No graderไม่มีผู้ตรวจ没有评分者 | CLM grades itCLM ตรวจCLM 来评 | Jev grades itJev ตรวจJev 来评 | Time eachเวลาต่ออัน各自用时 |
|---|---|---|---|---|
| DeepSWE | 73.7% | 81.6% | 71.1% | 79 ms vs 449 ms79 ms เทียบ 449 ms79 毫秒 对 449 毫秒 |
| Terminal-Bench 2.1 | 84.0% | 87.6% | 83.1% | 32 ms vs 131 ms32 ms เทียบ 131 ms32 毫秒 对 131 毫秒 |
- Count your own agent's tools. Three, or three hundred?
- Count how often the same tool runs twice in a row.
- Those two numbers tell you if CLM would help you.
- Ask one more: can you live with 95 instead of 99?
- นับเครื่องมือของเอเจนต์คุณเอง สามอัน หรือสามร้อยอัน
- นับว่าเครื่องมือเดิมถูกเรียกซ้ำติดกันบ่อยแค่ไหน
- สองตัวเลขนั้นจะบอกคุณว่า CLM จะช่วยคุณได้ไหม
- ถามอีกข้อ คุณรับ 95 แทน 99 ได้หรือเปล่า
- 数数你自己智能体的工具。三个,还是三百个?
- 数数同一个工具连着跑两次的频率有多高。
- 这两个数字会告诉你 CLM 对你有没有用。
- 再问一个:95 换 99,你受得了吗?
🏆 Tutorial 6 — Five challenges · บทที่ 6 — โจทย์ห้าข้อ · 教程 6 —— 五道题
🇬🇧 English
Each one starts from a file you already have. Try it before you open the answer.
🇹🇭 ไทย
ทุกข้อเริ่มจากไฟล์ที่คุณมีอยู่แล้ว ลองทำเองก่อนเปิดเฉลย
🇨🇳 中文
每一题都从你已有的文件出发。先自己试,再打开答案。
8 Try first. The answers are folded away. · ลองเองก่อน เฉลยพับซ่อนไว้ · 先自己试。答案折起来放着。
1Show the second choice too · แสดงตัวเลือกที่สองด้วย · 把第二名也显示出来
🇬🇧 English
Print all three actions in order, best first, with their scores.
A near tie is worth seeing. Step 2 was one.
🇹🇭 ไทย
พิมพ์การกระทำทั้งสามเรียงตามลำดับ ดีที่สุดก่อน พร้อมคะแนน
ผลที่เกือบเสมอกันควรได้เห็น ขั้นที่ 2 ก็เป็นแบบนั้น
🇨🇳 中文
把三个动作按顺序打出来,最好的在前,带上分数。
接近平手的情况值得看见。第 2 步就是一个。
Show the answerดูเฉลย看答案
# Add this to the end of clm1.py, in place of the single print.
# zip pairs each action with its score. sorted puts the best first,
# because reverse=True counts down instead of up.
pairs = sorted(zip(scores, ACTIONS), reverse=True)
for place, (score, action) in enumerate(pairs, start=1):
print(f"{place}. {score:.4f} {action.split(':')[0]}")
# The distance between first and second is how sure it is.
print(f"\nlead over second place: {pairs[0][0] - pairs[1][0]:.4f}")
🖥️ WHAT IT PRINTED
1. 0.6262 track_parcel
2. 0.5513 open_refund
3. 0.4493 reset_password
lead over second place: 0.0749
2Refuse a close call · ปฏิเสธเมื่อคะแนนสูสี · 太接近就别答
🇬🇧 English
Step 6 refused when the best score was low. Now refuse for a second reason.
If first and second are very close, the model is guessing. Say so.
🇹🇭 ไทย
ขั้นที่ 6 ปฏิเสธเมื่อคะแนนดีที่สุดยังต่ำ ทีนี้ปฏิเสธด้วยเหตุผลที่สอง
ถ้าที่หนึ่งกับที่สองใกล้กันมาก แปลว่าโมเดลกำลังเดา ให้บอกออกมา
🇨🇳 中文
第 6 步在最高分太低时拒绝。现在为第二个理由拒绝。
如果第一和第二非常接近,模型是在猜。把这件事说出来。
Show the answerดูเฉลย看答案
# Two ways to be unsure, so two tests. Put this in place of the
# single check in clm5.py.
FLOOR = 0.60 # nothing is close enough
LEAD = 0.05 # the top two are too near each other
ranked = sorted(zip(scores, ACTIONS), reverse=True)
best, second = ranked[0][0], ranked[1][0]
if best < FLOOR:
print("nothing fits, hand it to a person")
elif best - second < LEAD:
print(f"too close to call: {best - second:.4f} apart, ask the customer")
else:
print("picked:", ranked[0][1].split(":")[0])
🖥️ WHAT IT PRINTED · first with the step 1 wording, then the step 2 wording
picked: track_parcel
too close to call: 0.0043 apart, ask the customer
3Save the cache to a file · บันทึกแคชลงไฟล์ · 把缓存存进文件
🇬🇧 English
Right now the cache dies when the program ends. Write it to a file instead.
Load it if it is there. Build it if it is not.
🇹🇭 ไทย
ตอนนี้แคชตายเมื่อโปรแกรมจบ เขียนมันลงไฟล์แทน
ถ้ามีไฟล์อยู่ก็โหลด ถ้าไม่มีก็สร้างใหม่
🇨🇳 中文
现在程序一结束缓存就没了。把它写进文件吧。
有文件就读,没有就建。
Show the answerดูเฉลย看答案
# A list of number rows is plain JSON, so no extra library is needed.
import os
CACHE = "actions.json"
if os.path.exists(CACHE):
with open(CACHE) as saved:
cached = json.load(saved)
print("loaded", len(cached), "action rows from", CACHE)
else:
cached = embed(ACTIONS)
with open(CACHE, "w") as saved:
json.dump(cached, saved)
print("built and saved", len(cached), "action rows")
🖥️ WHAT IT PRINTED · run it twice
built and saved 3 action rows
loaded 3 action rows from actions.json
4Find the two actions that confuse each other · หาสองการกระทำที่สับสนกันเอง · 找出互相混淆的那两个动作
🇬🇧 English
Two actions that sit close together will be mixed up often. Find them before your users do.
Score every action against every other action.
🇹🇭 ไทย
การกระทำสองอันที่อยู่ใกล้กันจะถูกสลับกันบ่อย หาให้เจอก่อนผู้ใช้ของคุณจะเจอ
ให้คะแนนการกระทำทุกอันเทียบกับทุกอันที่เหลือ
🇨🇳 中文
两个挨得很近的动作会经常被搞混。在你的用户发现之前先找出来。
把每个动作和其他每个动作都打一次分。
Show the answerดูเฉลย看答案
# Compare each action with the ones after it, so no pair is done twice.
rows = embed(ACTIONS)
pairs = []
for first in range(len(ACTIONS)):
for second in range(first + 1, len(ACTIONS)):
score = closeness(rows[first], rows[second])
pairs.append((score, ACTIONS[first].split(":")[0],
ACTIONS[second].split(":")[0]))
for score, one, two in sorted(pairs, reverse=True):
warning = " <- too close" if score > 0.75 else ""
print(f"{score:.4f} {one} vs {two}{warning}")
🖥️ WHAT IT PRINTED · with the three actions from clm1.py
0.6258 open_refund vs reset_password
0.6013 open_refund vs track_parcel
0.5414 track_parcel vs reset_password
open_refund and reset_password, at 0.6258. But step 1 went wrong a different way: track_parcel beat open_refund. That pair is only second here. So this test missed it. Comparing two actions asks one thing. Do these two sound alike? Comparing an action with a state asks another. Does this action suit this customer? A pair can be safe on one test. And dangerous on the other. Run both. Neither one covers for the other.
อ่านบรรทัดบนสุด คู่ที่ใกล้กันที่สุดคือ open_refund กับ reset_password ที่ 0.6258 แต่ขั้นที่ 1 ผิดคนละแบบ คือ track_parcel ชนะ open_refund คู่นั้นอยู่อันดับสองตรงนี้เท่านั้น การทดสอบนี้จึงพลาดมันไป การเทียบการกระทำสองอันคือถามว่า "สองอันนี้ฟังดูเหมือนกันไหม" ส่วนการเทียบการกระทำกับสถานะคือถามว่า "การกระทำนี้เหมาะกับลูกค้าคนนี้ไหม" นั่นคือคนละคำถาม และคู่หนึ่งอาจปลอดภัยในคำถามหนึ่ง แต่อันตรายในอีกคำถาม ให้รันทั้งสองอย่าง ไม่มีอันไหนแทนอีกอันได้
看最上面一行。最接近的一对是 open_refund 和 reset_password,0.6258。但第 1 步错的是另一种:track_parcel 赢了 open_refund。那一对在这里只排第二。所以这个检查漏掉了它。比较两个动作,问的是"这两个听起来像吗";比较动作和状态,问的是"这个动作适合这位客户吗"。这是两个不同的问题,一对动作可能在一个问题上安全,在另一个问题上危险。两个都跑。谁也替不了谁。
5Make the cache pay for itself · ทำให้แคชคุ้มค่าตัวเอง · 让缓存把自己赚回来
🇬🇧 English
Building the cache costs time up front. How many requests before it has paid that back?
Measure it. Do not guess.
🇹🇭 ไทย
การสร้างแคชมีต้นทุนเวลาตอนเริ่ม ต้องใช้กี่คำขอมันถึงจะคืนทุน
วัดเอา อย่าเดา
🇨🇳 中文
建缓存一开始要花时间。要多少次请求才把这笔时间赚回来?
量出来,别猜。
Show the answerดูเฉลย看答案
# Time the one-off cost of building the cache.
start = time.time()
cached = embed(ACTIONS)
build = time.time() - start
# plain and quick are the two per-request times from clm3.py.
saved_each = plain - quick
requests = build / saved_each
print(f"building the cache took : {build * 1000:.1f} ms")
print(f"saved on each request : {saved_each * 1000:.1f} ms")
print(f"pays for itself after : {requests:.1f} requests")
🖥️ WHAT IT PRINTED · three actions, on a DGX Spark
building the cache took : 19.2 ms
saved on each request : 23.0 ms
pays for itself after : 0.8 requests
🇬🇧 English
You built a picker in five files. It was wrong on the first try, and you found out why.
CLM-8B is the same shape, trained properly. Two small heads on a frozen model. A million real runs taught them.
It is faster because it does not write. It also remembers the choices. It is less accurate for the same reason. Now you can tell whether that trade suits you.
🇹🇭 ไทย
คุณสร้างตัวเลือกขึ้นมาในห้าไฟล์ มันตอบผิดตั้งแต่ครั้งแรก และคุณได้รู้ว่าทำไม
CLM-8B คือรูปร่างเดียวกัน แต่ฝึกมาอย่างถูกต้อง หัวเล็ก ๆ สองหัวบนโมเดลที่แช่ไว้ สอนด้วยการทำงานจริงนับล้านครั้ง
มันเร็วกว่าเพราะมันไม่เขียน และเพราะมันจำตัวเลือกไว้ มันแม่นน้อยกว่าด้วยเหตุผลเดียวกัน ทีนี้คุณบอกเองได้แล้วว่าการแลกแบบนี้เหมาะกับคุณไหม
🇨🇳 中文
你用五个文件做出了一个挑选器。它第一次就答错了,而你知道了为什么。
CLM-8B 是同样的形状,只是训练得当。冻住的模型上两个小头,由上百万次真实运行教出来。
它快,因为它不写字,也因为它记住了选项。它准确率低,也是同样的原因。现在你能自己判断这笔交易适不适合你了。
| You wantคุณอยากได้你想要 | Look upไปหาคำว่า去查 |
|---|---|
| The model itselfตัวโมเดลเอง模型本体 | pip install contrastive-lm |
| The weightsค่าน้ำหนัก权重 | Contrastive-LM/CLM-v0.1-8B |
| Millions of vectors, searched fastเวกเตอร์เป็นล้าน ค้นหาเร็ว上百万向量,快速搜索 | faiss |
| Better embeddings than nomicการฝังตัวที่ดีกว่า nomic比 nomic 更好的嵌入 | mxbai-embed-large |
| To train heads like these yourselfฝึกหัวแบบนี้ด้วยตัวเอง自己训练这样的头 | InfoNCE loss |
🇬🇧 English
Next: Jev, an answer not a sentence, Laya, the open answer to Jev, or meaning as a row of numbers.
🇹🇭 ไทย
ต่อไป: Jev คำตอบ ไม่ใช่ประโยค, Laya คำตอบแบบเปิดของ Jev หรือ ความหมายในรูปแถวตัวเลข