Langgraph Decision Models
LangChainOfficialPublished by LangChain
- Skill
- Tool integration
- Data and AI
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a framework for replacing generative LLM calls with specialized decision models (Jev, SemIf) within LangGraph agents. By using typed primitives like Noul, Choice, and Score, developers can perform semantic classification tasks—such as triage, filtering, and routing—with higher efficiency and lower latency than traditional LLM-based parsing. The approach emphasizes batching independent questions into single requests and using standard Python logic for routing based on probability distributions.
What it does
- Replaces LLM-based text parsing with typed, probabilistic decision models.
- Provides primitives for binary (Noul), categorical (Choice), and rubric-based (Score) classification.
- Supports integration with LangSmith Gateway for model hosting and management.
- Enables efficient routing logic using probability-weighted outputs.
- Facilitates auditing existing agents to identify opportunities for replacing generative calls.
- Supports high-throughput workflows through LangGraph's Send pattern.
When to use it
- When an LLM node is used solely to generate structured output for routing or filtering.
- When you need to perform semantic judgments on a large volume of items where LLM costs are prohibitive.
- When you require consistent, testable decision logic that does not rely on multi-step reasoning.
- When you need to replace brittle regex or keyword-based heuristics with semantic classification.
Advantages
- Significantly lower latency and cost compared to generative LLM calls.
- Provides clear, probabilistic confidence scores for better decision-making.
- Decouples classification logic from generative tasks, improving agent maintainability.
- Enables cost-effective processing of entire datasets instead of sampling.
Usage examples
- “How do I replace an LLM-based routing node with a decision model?”
- Define a set of questions using Noul or Score primitives, invoke the TypeSafeClassifier within your LangGraph node, and implement a standard Python function to route based on the returned probabilities.
- “How can I improve the accuracy of my classification?”
- Refine your question wording by using specific criteria to define outcomes, and ensure you are comparing scores against calibrated bands rather than absolute values.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/langchain-ai/langgraph-decision-models/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip langgraph-decision-models.zip -d .agents/skills
Documentation by LangChain
<overview> A **decision model** answers typed questions about state and returns probabilities instead of prose. It replaces the common pattern of prompting an LLM, parsing its text, and branching on the result.
Noul(instructions=...)— binary question, returns a probability of yesChoice(instructions=..., criteria={...})— picks one label, returns the full distribution plus confidenceScore(instructions=..., criteria=[...])— grades against an ordered rubric, returns an expected value plus confidence
TypeSafeClassifier is a LangChain Runnable[ClassifierRequest, ClassifierResponse], so it drops into a node like any other runnable. Up to 32 questions share one request and are answered independently — your code combines them.
Reach for one when a node generates text only so you can parse a decision out of it: routing, triage, filtering, guardrails, or per-item classification over a batch.
Do not reach for one when the node's output is the product (summaries, drafts, code) or when the judgment needs multi-step reasoning. A decision model classifies; it does not think. </overview>
Install and wire
langchain-typesafe is alpha (0.0.1a3) and TypeSafeClassifier is marked @beta — pin it and expect churn.
uv add langchain-typesafeThree ways to reach a model. The classifier POSTs to {base_url}/v1/systemone with Authorization: Bearer {api_key}, so switching providers is constructor arguments only:
<ex-wiring> <python> ```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>
---
## The conversion pattern
Ask every question about a page/item in **one** request, put the typed response in state, and let a plain function route on it. The router is ordinary Python — testable without touching a network.
<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"</python> </ex-classify-and-route>
Reading answers: response.nouls[id].noul, response.choices[id].choice, response.scores[id].score. Each view is keyed by your question id; response.answers holds them all.
Three traps when reading answers
These cause silent misrouting, not exceptions.
1. Score.score is an expected value, not a level. It is a probability-weighted average over the rubric and is routinely fractional. score == 0 almost never fires — a "not responsive" item lands at 0.07, not 0. Always compare against a band.
if a.scores["relevant"].score < 0.5: # correct
if a.scores["relevant"].score == 0: # WRONG -- nearly never true2. Confidence measures distribution shape, not correctness. On a Score, confidence reports how concentrated the rubric distribution is. An item sitting cleanly between two levels scores low confidence even when the model is entirely clear about it. A blanket confidence < X -> escalate rule therefore escalates items the model already decided. Gate on confidence only inside the ambiguous middle:
if score < NOT_RELEVANT: # decisive -- trust it
return "close"
if score < RELEVANT or confidence < MIN_CONF: # ambiguous -- escalate
return "human_review"
return "handle"3. Thresholds do not transfer between models. Calibration is part of the model. The same policy over the same items routes differently on Jev vs SemIf vs an LLM adapter. Re-tune thresholds whenever you change models, and pin the model id.
Question wording dominates accuracy
A loose question produces confident wrong answers, and no threshold fixes it. Use criteria to say what each outcome means, including what should not count.
In a measured case, "Is this a confidential communication with a lawyer?" scored a routine finance memo at 0.798. Rewriting it to name the actual test — written by or to a lawyer, with an explicit carve-out for finance and accounting content — moved the same page to 0.005 while a genuinely privileged page held at 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.",
),
)Before blaming the model, rewrite the question and re-measure.
Auditing an existing agent
To find where a decision model fits, look for these in the codebase — see references/conversion-playbook.md for the full walkthrough.
| Signal | What to look for |
|---|---|
| Generate-then-parse | An LLM call whose output is immediately regex'd, json.loads'd, or string-matched into a branch |
| Prompted classifiers | Prompts containing "respond with one of", "answer yes or no", "rate from 1 to 5" |
| Sampling for cost | Comments or configs that check only the first N items because checking all is too expensive |
| Brittle rules | Keyword lists or regexes standing in for semantic judgment |
| Re-reading context | The same document re-sent to a model for each separate question |
The last two matter most: cheap semantic judgments change what you can build, not just the bill. If evaluating every item became affordable, what would you stop sampling?
Expectations
Measured on a 24-item batch, identical LangGraph graph and routing policy, only the classifier swapped:
| per item | tokens (6 items) | notes | |
|---|---|---|---|
| Jev 1.13.0 | 0.27s | 3,648 in / 318 out | reports usage |
| SemIf 4B | 0.49s | not reported | hosted on the Gateway |
| Claude Sonnet 5 | 2.87s | 7,930 in / 864 out | via structured output |
Routing agreed on 4–5 of 6 items across engines; disagreements clustered on genuinely borderline items. Treat these as shape, not benchmarks — measure on your own workload.
If you compare against an LLM baseline, use method="json_schema" so the comparison is fair. LangChain's with_structured_output defaults to method="function_calling", which injects a tool schema into every request — 556/35 tokens versus 228/12 for the native output_config.format path on the same one-field probe.
Batching with Send
Classification is per-item and independent, so fan out with Send and let each item route on its own.
<ex-fan-out> <python> ```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>
Fan-out hides latency, so it flatters slow classifiers most: in the run above, Sonnet gained 8x from concurrency and Jev only 1.4x — yet Jev still finished first. Compare throughput, not the speedup multiple.
---
## Related skills
- **langgraph-fundamentals** — StateGraph, `Send`, `Command`, conditional edges
- **langgraph-human-in-the-loop** — `interrupt()` for the escalation branch above
- **langchain-middleware** — structured output when you need an LLM, not a classifierFiles
- ATTRIBUTION.mdAdded by AutoGineer912 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md9.2 KiB
- references/conversion-playbook.md6.1 KiB
references/conversion-playbook.md
Converting an existing agent to route on a decision model
A six-step audit for finding where typed classification replaces a generative call, and for proving it was worth doing. The framing of steps 1–2 is adapted from jevify, a prompt for investigating what a decision model makes possible in an existing project.
Keep exploration separate from production changes. Do not edit the agent until step 6.
1. Understand the project as it is
Read the real code, not the README. For each model call, record: what it costs, what it returns, and what the caller actually uses from the return.
Tie every finding to a specific file and line. Look for:
Generate-then-parse. A completion immediately fed to
json.loads, a regex, or.startswith(...). The prose was never the product.Prompted classifiers. Prompts containing "respond with one of", "answer yes or no", "rate 1–5", "reply ONLY with".
Budget-driven sampling. Code that checks the first N items, or runs a check only on weekends, because doing it always is too expensive.
Brittle proxies. Keyword lists, regexes, or heuristics standing in for semantic judgment because a model call was too slow or costly.
Re-reading. The same document sent to a model repeatedly, once per question.
2. Reconsider from first principles
The pivot question: if many semantic judgments were affordable inside your latency budget, what would you design differently?
Sort ideas into three buckets, and give the third real weight:
Direct savings — same behavior, lower cost or latency.
Better outcomes — same feature, better decisions (judging every item instead of sampling).
New capabilities — things currently impossible: reacting mid-interaction, continuously reassessing state, filtering a firehose.
Bucket 3 is where the leverage is. Bucket 1 is the easiest to justify and the least interesting.
3. Make each opportunity concrete
For every candidate, write down:
The integration point — which node, which file.
The input state — exactly what gets sent.
Which primitives apply —
Noul,Choice, orScore, and why.Which questions share one request versus genuinely depend on a prior answer. Questions in one request are answered independently; if B depends on A's answer, they are two requests and two nodes.
How plain code consumes the result — the routing function, in full.
What still needs generation or retrieval. Most workflows keep an LLM for the part that produces text; the decision model only picks which items get there.
Do not bury hard reasoning in a fuzzy question. "Is this code correct?" is not a classification. If a question needs multi-step reasoning to answer, it needs a reasoning model.
4. Test the economics honestly
Estimate the whole workflow, not one call: retries, fallbacks, the escalation path, and the generative work that remains.
Separate per-request latency from end-to-end. Fan-out changes the second and not the first.
Compare against the cheaper alternatives you skipped: deterministic code, caching, embeddings, a smaller model. Sometimes a regex really is the right answer.
If you have no measurements yet, state the assumption and the break-even point rather than a number you cannot defend.
When benchmarking against an LLM baseline, make the baseline fair — native structured output (method="json_schema"), no tool-schema injection, caching enabled on any stable prompt prefix. An unfair baseline makes the result useless for deciding anything.
5. Design an evaluation that can falsify the idea
A decision model returns probabilities. Probabilities are signals whose calibration must be tested on your workload; vendor benchmarks do not transfer.
Baselines — current behavior, and the cheapest non-model alternative.
Asymmetric costs — a false negative on a privilege check is not a false positive on a spam check. Weight them.
Latency distribution — p50 and p99, not the mean.
Adversarial and ambiguous inputs — items that should land mid-rubric. These are where thresholds get decided.
Threshold and fallback validation — sweep thresholds against labeled data. Do not hand-pick them from a handful of examples.
Go/no-go criteria, written before you run it.
langsmith-skills covers dataset construction and evaluators for this step.
6. Deliver a recommendation
A ranked table of opportunities with expected impact and confidence.
Detailed designs for the top three.
A sketch of how the system would look if designed this way from scratch — often different from the incremental patch.
The smallest decisive experiment: the one change that would settle it.
Rejected ideas, with reasons. These are as valuable as the accepted ones.
Migration shapes
Prompted classifier → one classifier node. The direct swap. The prompt's enumerated options become Choice.criteria; its rating scale becomes Score.criteria; its yes/no becomes a Noul. Delete the parsing code — that is the point.
Chain of LLM guards → one request, many questions. Sequential guardrail calls that each ask one yes/no collapse into a single request with several Nouls, then a routing function. Only do this where the guards are genuinely independent.
Sampling → full coverage. Where cost forced you to check 1 in 100, check all of them and route the uncertain ones to the expensive path. This usually improves the product, not just the bill.
Keyword rules → semantic questions. Replace regex allowlists with a Noul plus a threshold. Keep the regex as a fast path if it is precise; use the model for the tail it cannot cover.
Rollout
Run the decision model in shadow beside existing logic. Log both decisions; change nothing.
Compare on real traffic. Disagreements are your evaluation set — label them.
Tune thresholds against those labels, not against intuition.
Cut over the decisive bands first; keep humans or the LLM on the ambiguous middle.
Pin the model id and re-validate thresholds on every model change — calibration shifts between versions.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs an API key, Needs Python, Needs Node.js
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- LangChain
- Publisher
- LangChain (Official)
- Source
- langchain-ai/langchain-skills · config/skills/langgraph-decision-models · commit 126c384
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by LangChain or LangChain.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download langgraph-decision-models.zip