langgraph-decision-models について
langgraph-decision-modelsは、LangGraphで生成テキストを解析する代わりに、型付きの確率分類を使うLangChainスキルです。ルーティング、トリアージ、フィルタリング、ガードレール、バッチ分類のノードを作る開発者向けです。
主な機能
- 01二択判定の確率出力
- 02ラベル選択と分布の返却
- 03ルーブリックに基づくスコアリング
- 041リクエストで最大32問を処理
- 05LangGraphのルーティングとバッチ処理の解説
活用シーン
- 01請求関連度によるチケット振り分け
- 02人手確認が必要な項目の選別
- 03意味基準によるコンテンツのフィルタリング
- 04バッチ内の各項目の分類
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ langchain-ai/ langchain-skills --skill langgraph-decision-modelsインストール後、Claude Code で/langgraph-decision-modelsを入力して読み込まれたか確認します。
同梱ファイルreferences/conversion-playbook.md
詳細
Noul(instructions=...)— 二択の質問で、「はい」の確率を返しますChoice(instructions=..., criteria={...})— ラベルを1つ選び、分布全体と信頼度を返しますScore(instructions=..., criteria=[...])— 順序付きルーブリックに基づいて評価し、期待値と信頼度を返します
TypeSafeClassifier は LangChain の Runnable[ClassifierRequest, ClassifierResponse] なので、他の Runnable と同じようにノードに組み込めます。最大32個の質問を1つのリクエストで共有し、それぞれ独立して回答します。質問の結果を組み合わせるのは、あなたのコードです。
ノードがテキストを生成するのは、そこから判断を解析するためだけという場合に使います。ルーティング、トリアージ、フィルタリング、ガードレール、バッチ内の項目ごとの分類などが該当します。
ノードの出力自体が成果物である場合(要約、下書き、コードなど)、または判断に複数段階の推論が必要な場合は、使わないでください。決定モデルは分類しますが、思考はしません。
インストールと接続
langchain-typesafe はアルファ版(0.0.1a3)で、TypeSafeClassifier は @beta とマークされています。バージョンを固定し、頻繁な変更を想定してください。
uv add langchain-typesafe
モデルに接続する方法は3つあります。分類器は Authorization: Bearer {api_key} を指定して {base_url}/v1/systemone に POST するため、プロバイダーの切り替えはコンストラクターの引数だけで行えます。
1. TypeSafe directly (Jev). Reads TYPESAFE_API_KEY when api_key is omitted.
classifier = TypeSafeClassifier(model="jev-latest")
2. SemIf, hosted on the LangSmith Gateway. Note: LangSmith key, not a TypeSafe key.
classifier = TypeSafeClassifier( model="semif-qwen3.5-4b", api_key=os.environ["LANGSMITH_API_KEY"], base_url="https://gateway.smith.langchain.com", )
3. Jev through the Gateway (BYOK). The typesafe/ prefix routes to a
TYPESAFE_API_KEY stored in LangSmith workspace secrets. Without that secret
every typesafe/* id returns 424 Failed Dependency -- before the model name is
even validated, so a 424 does not confirm the id is real.
classifier = TypeSafeClassifier( model="typesafe/jev-1.13.0", api_key=os.environ["LANGSMITH_API_KEY"], base_url="https://gateway.smith.langchain.com", )
</python>
</ex-wiring>
---
## 変換パターン
ページまたは項目についてのすべての質問を**1つの**リクエストで行い、型付きの応答を状態に格納して、通常の関数でルーティングします。ルーターは通常の Python なので、ネットワークに接続せずにテストできます。
<ex-classify-and-route>
<python>
```python
from typing import TypedDict
from langchain_typesafe import ClassifierResponse, Noul, Score, TypeSafeClassifier
from langgraph.graph import StateGraph, START, END
QUESTIONS = {
"relevant": Score(
instructions="How relevant is this ticket to a billing problem?",
criteria=["Unrelated.", "Possibly related.", "Directly about billing."],
),
"angry": Noul(instructions="Is the customer expressing anger?"),
}
class State(TypedDict):
text: str
answers: ClassifierResponse
route: str
classifier = TypeSafeClassifier(model="jev-latest")
def classify(state: State) -> dict:
# One request, every question. They are answered independently.
return {"answers": classifier.invoke(
{"state": state["text"], "questions": QUESTIONS}
)}
def route(state: State) -> str:
a = state["answers"]
if a.nouls["angry"].noul > 0.7:
return "escalate"
if a.scores["relevant"].score < 0.5:
return "close"
return "handle"
回答を読み取るには、response.nouls[id].noul、response.choices[id].choice、response.scores[id].score を使います。各ビューは質問 ID をキーとしており、response.answers にすべての回答が格納されています。
回答を読むときの3つの落とし穴
これらは例外ではなく、気付かないうちに誤ったルーティングを引き起こします。
1. Score.score はレベルではなく期待値です。 ルーブリックの確率加重平均であり、通常は小数になります。score == 0 になることはほとんどありません。「回答性なし」の項目は 0 ではなく 0.07 になります。必ず範囲と比較してください。
if a.scores["relevant"].score < 0.5: # correct
if a.scores["relevant"].score == 0: # WRONG -- nearly never true
2. 信頼度が示すのは分布の形であり、正しさではありません。 Score では、信頼度はルーブリックの分布がどの程度集中しているかを示します。2つのレベルの中間に明確に位置する項目は、モデルの判断が明確でも信頼度が低くなります。そのため、confidence < X -> escalate のような一律のルールを使うと、モデルがすでに判断した項目までエスカレーションされます。信頼度による判定は、曖昧な中間領域に限って行ってください。
if score < NOT_RELEVANT: # decisive -- trust it
return "close"
if score < RELEVANT or confidence < MIN_CONF: # ambiguous -- escalate
return "human_review"
return "handle"
3. しきい値はモデル間で引き継げません。 キャリブレーションはモデルの一部です。同じ項目に同じポリシーを適用しても、Jev、SemIf、LLM アダプターではルーティング結果が異なります。モデルを変更するたびにしきい値を再調整し、モデル ID を固定してください。
質問の表現が精度を大きく左右します
曖昧な質問は自信満々の誤答を招き、しきい値では修正できません。criteria を使って、各結果の意味と、何を該当とみなすべきでないかを明記してください。
ある測定例では、「これは弁護士との機密通信ですか?」という質問に対し、一般的な財務メモが 0.798 というスコアになりました。実際の判定基準、つまり弁護士が作成した、または弁護士宛ての文書であることを明示し、財務および会計の内容を明確に除外するよう書き換えたところ、同じページのスコアは 0.005 に下がり、実際に秘匿特権の対象となるページは 0.991 を維持しました。
Noul(
instructions=(
"Was this written by or to a lawyer, or does it convey a lawyer's legal "
"advice? Answer no for ordinary business or accounting discussion, even "
"when the subject is litigation-sensitive."
),
criteria=NoulCriteria(
true="A named attorney is author or recipient, or it relays legal advice.",
false="Business or accounting content with no attorney involved.",
),
)
モデルを疑う前に、質問を書き換えて再測定してください。
既存のエージェントを監査する
意思決定モデルをどこに組み込めるかを見つけるには、コードベースで次の項目を探してください。詳しい手順については、references/conversion-playbook.md を参照してください。
| シグナル | 確認する内容 |
|---|---|
| 生成してから解析 | 出力を直ちに正規表現で処理したり、json.loads で読み込んだり、文字列と照合して分岐したりする LLM 呼び出し |
| プロンプトによる分類器 | 「次のいずれかで回答してください」「はいかいいえで答えてください」「1 から 5 で評価してください」などを含むプロンプト |
| コストを抑えるためのサンプリング | すべてを確認するにはコストがかかりすぎるため、最初の N 個だけを確認するコメントや設定 |
| 脆弱なルール | 意味に基づく判断の代わりに使われているキーワードリストや正規表現 |
| コンテキストの再読み込み | 別々の質問ごとに同じドキュメントをモデルへ再送する処理 |
特に重要なのは最後の 2 項目です。低コストな意味判断によって変わるのは、請求額だけではなく、何を構築できるかです。すべての項目を評価できるようになったら、何のサンプリングをやめますか?
期待値
24 項目のバッチで測定しました。同一の LangGraph グラフとルーティングポリシーを使用し、分類器のみを入れ替えています。
| 項目あたり | トークン数(6 項目) | 備考 | |
|---|---|---|---|
| Jev 1.13.0 | 0.27 秒 | 入力 3,648 / 出力 318 | 使用量を報告 |
| SemIf 4B | 0.49 秒 | 報告なし | Gateway 上でホスト |
| Claude Sonnet 5 | 2.87 秒 | 入力 7,930 / 出力 864 | 構造化出力を使用 |
各エンジン間でルーティングが一致したのは 6 項目中 4~5 項目でした。不一致は、実際に境界が曖昧な項目に集中しました。これらはベンチマークではなく傾向として捉え、ご自身のワークロードで測定してください。
LLM のベースラインと比較する場合は、公平な比較になるよう method="json_schema" を使用してください。LangChain の with_structured_output は、デフォルトで method="function_calling" を使用し、各リクエストにツールスキーマを挿入します。同じ 1 フィールドのプローブでは、ネイティブの output_config.format 経由の 228/12 トークンに対し、556/35 トークンを消費します。
Send によるバッチ処理
分類は項目ごとに独立しているため、Send で並列に処理し、各項目を個別にルーティングできます。
def fan_out(state): return [Send("classify_item", {"text": t}) for t in state["items"]]
builder.add_conditional_edges(START, fan_out, ["classify_item"])
</python>
</ex-fan-out>
並列処理でレイテンシーが隠れるため、遅い分類器ほど大きく向上して見えます。上記の実行では、Sonnet は並行処理によって 8 倍向上し、Jev は 1.4 倍向上しました。それでも Jev のほうが先に完了しました。速度向上率ではなく、スループットを比較してください。
---
## 関連スキル
- **langgraph-fundamentals** — StateGraph、`Send`、`Command`、条件付きエッジ
- **langgraph-human-in-the-loop** — 上記のエスカレーション分岐で使う `interrupt()`
- **langchain-middleware** — 分類器ではなく LLM が必要な場合の構造化出力