mcpField
L
langgraph-decision-models

langgraph-decision-models

github.com/langchain-aiエージェントワークフロー1.3k1.8k 回インストール3 日前に更新
リポジトリ

langgraph-decision-models について

langgraph-decision-modelsは、LangGraphで生成テキストを解析する代わりに、型付きの確率分類を使うLangChainスキルです。ルーティング、トリアージ、フィルタリング、ガードレール、バッチ分類のノードを作る開発者向けです。

主な機能

  1. 01二択判定の確率出力
  2. 02ラベル選択と分布の返却
  3. 03ルーブリックに基づくスコアリング
  4. 041リクエストで最大32問を処理
  5. 05LangGraphのルーティングとバッチ処理の解説

活用シーン

  1. 01請求関連度によるチケット振り分け
  2. 02人手確認が必要な項目の選別
  3. 03意味基準によるコンテンツのフィルタリング
  4. 04バッチ内の各項目の分類

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/langchain-ai/langchain-skills --skill langgraph-decision-models

インストール後、Claude Code で/langgraph-decision-modelsを入力して読み込まれたか確認します。

同梱ファイルreferences/conversion-playbook.md

詳細

**決定モデル**は、状態について型付きの質問に回答し、文章ではなく確率を返します。LLM にプロンプトを送信してテキストを解析し、その結果に応じて分岐する一般的なパターンを置き換えます。
  • 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 するため、プロバイダーの切り替えはコンストラクターの引数だけで行えます。

```python import os from langchain_typesafe import TypeSafeClassifier

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.00.27 秒入力 3,648 / 出力 318使用量を報告
SemIf 4B0.49 秒報告なしGateway 上でホスト
Claude Sonnet 52.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 で並列に処理し、各項目を個別にルーティングできます。

```python from langgraph.types import 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 が必要な場合の構造化出力

コメント