CLM は、対照学習(contrastive learning) で「状態(state)」と「行動(action)」を結びつけるSystem One model。
Stanford の Jacky Kwok らが Hazy Research などと共同で開発し、2026年9月23日に CLM-8B(CLM-v0.1-8B) として発表した。Jevと同程度の性能で 最大9倍速い 推論を実現したと報告されている。
GitHub: Contrastive-LM/CLM
Hugging Face: Contrastive
-LM /CLM -v0 .1 -8B 発表ポスト: Jacky Kwok on X
アイデア¶
System One modelが行う「判断」を、stateに対して最も合うaction(選択肢)を検索する問題 とみなす。
CLIPが画像とテキストを同じ埋め込み空間に写したように、CLMは stateとactionを別々のエンコーダーで同じ埋め込み空間に写し、その類似度で選択肢のスコアを決める。
state:判断の前提となる情報(会話・画面の状態・エージェントの履歴など)
action:選択肢(ツール名・次の操作・回答候補など)
アーキテクチャ¶
バックボーン:凍結した Qwen3-8B(エンコーダーとして使う)
その上に、学習可能な小さな 射影ヘッド(約20Mパラメータ)が2つ
state head
action head
stateとactionは 分離(disaggregated) されてエンコードされ、埋め込みの内積(類似度)でスコアを出す
stateの埋め込みを 、actionの埋め込みを とすると、選択肢の確率は
のように候補間のsoftmaxで得られる( は温度)。
学習¶
損失関数:双方向 InfoNCE¶
バッチ内の正例ペア について、state→action と action→state の 両方向 で検索がうまくいくように最適化する。
(CLIPと同じ形の対称な損失)
3段階の学習¶
| 段階 | データ | 目的 |
|---|---|---|
| 事前学習(pre-training) | Nemotronの質問・回答ペア 約6000万組 | 広い意味理解 |
| 中間学習(mid-training) | Gemini 2.5で合成した ハードネガティブ(誤答)約3000万件 | 細かい違いの識別 |
| 事後学習(post-training) | エージェントの作業履歴 約100万件(ADP、Endless-Terminals、LiteCoderなど) | エージェント的なタスクへの適応 |
事後学習では、破滅的忘却を防ぐために Nemotron のデータを40%リプレイし、残り60%をエージェントのトレースにしている。
バックボーンは凍結し、射影ヘッドだけを更新するので学習が軽い。
なぜ速いのか:埋め込みのキャッシュ¶
stateとactionを分離してエンコードするため、それぞれの埋め込みを独立にキャッシュして再利用できる。
ツールの一覧やゲームの操作など、同じ選択肢が何度も出てくる 環境では、actionの埋め込みを1回計算すれば使い回せる
同じstateを再訪するときはstateの埋め込みも再利用できる(RTX 4090で3択の場合 1.7ms → 0.6ms、2.8倍速)
Jevの場合、再利用できるのはstate側の計算結果だけとされており、選択肢が多いほどCLMが有利になる(候補が多いと最大13倍という報告もある)。
これは情報検索でいう bi-encoder(CLM)と cross-encoder(stateと選択肢を一緒に入れるモデル)の関係と同じで、bi-encoderは速いがstateと選択肢の細かい相互作用は捉えにくい、というトレードオフがある。
性能¶
| タスク | 結果 |
|---|---|
| コンピューター操作・ゲーム・ツール呼び出し(ゼロショット) | Jevと同等、最大9倍速い |
| DeepSWE(verifierとして。軽いfine-tune後) | 81.6%(SOTA) |
| Terminal-Bench 2.1(verifierとして。軽いfine-tune後) | 87.6%(SOTA) |
コーディング課題では、複数の解答候補から正解を選ぶ verifier として使われている。gihyoの記事によれば、この設定でJevは無作為選択を下回った一方、CLMは正解を選別できたとのこと。
使い方¶
TypeSafe互換のAPI(POST /v1/systemone)でCLM-8Bをサーブできる。埋め込みの計算にはvLLMを使う。
pip install contrastive-lm
# Qwen3-8B エンコーダーを GPU で起動(pooling runner)
vllm serve Qwen/Qwen3-8B --served-model-name qwen3-8b --runner pooling --port 8090 &
# CLM API を :8700 で起動
clm-servefrom clm import CLMClient, Choice, Noul, Score
client = CLMClient()
r = client.system_one(
state="Customer: my invoice was charged twice!",
questions={
"urgency": Noul(instructions="Is this urgent?"),
"department": Choice(
instructions="Which team?",
criteria={
"billing": "Charges, invoices, refunds",
"technical": "Bugs and outages",
},
),
"frustration": Score(
instructions="Customer frustration level?",
criteria=["Calm", "Frustrated", "Very angry"],
),
},
)
print(r.answers["department"].choice) # "billing"
print(r.answers["frustration"].score) # 0..2 のレベルの期待値サーバーを立てずに、プロセス内で候補をランキングすることもできる。
from clm import Engine
engine = Engine(emb_url="http://127.0.0.1:8090/v1/embeddings")
engine.rank(
"What causes tides?",
["The Moon's gravitational pull.", "Photosynthesis.", "Earth is round."],
)POST /v1/rank:文脈+質問+候補を渡すと確率順に並べる(基本の操作)GET /:WebのプレイグラウンドGET /v1/models:利用できるヘッドの一覧
限界¶
文章生成はできない(System One modelなので当然)
返す確率は 候補の中での相対値 であり、候補の組み合わせを変えると確率も変わる(Jevのような絶対的なキャリブレーションとは性質が違う)
stateは2048トークンで切り捨てられる(設定で延ばせるが、GPUメモリが必要)
外部のvLLM埋め込みサーバーが必要
キャッシュはヘッドの重みに紐づくので、重みを差し替えるとキャッシュは無効になる
画像・動画は未対応(マルチモーダル版の CLM-35B を予定)
重みは Apache 2.0 ライセンスで公開されている。