Skip to content

Conditional Routing and Confidence-Scored Classification

Before you start

Prerequisite: Lesson 01 (the current agent API, including the conditional-edges graph you already built) and lesson 02 (state as a typed contract). After this lesson, you can: build a classifier node that scores its own confidence, route on that score with a conditional edge, and explain why this isn't a new LangGraph feature so much as the same mechanism you already know, pointed at a different kind of output.

The mechanism you already have

Lesson 01's conversational graph already used a conditional edge:

python
graph.add_conditional_edges(
    "conversation",
    should_end,
    {True: "END", False: "conversation"}
)

should_end is a plain Python function. It looks at state and returns a value that decides which node runs next. That's the entire mechanism. This course's own source material introduces "classification-based routing" and "confidence-driven decisions" later, in unit 1.3, in a way that reads like a new capability. It isn't one. A classifier node is a node like any other, one that happens to call a model and write a category and a confidence score into state. The conditional edge that reads those fields is the exact same should_end-shaped function, just branching on more than a boolean.

two-architectures lesson 02 makes the underlying point directly, with a worked example: when a model classifies something, what actually happens is {"tool": "classify_ticket", "input": {"ticket_id": "T-4829"}}, a text string your code parses and acts on. A classification is a tool call. The routing decision that follows it is exactly as mechanical as routing on any other tool's output.

Building a classifier node

python
from typing import Annotated, TypedDict, Literal
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages

class State(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]
    category: str
    confidence: float

def classify_message(state: State) -> State:
    """Ask the model to categorize the latest message and score its own confidence."""
    last_message = state["messages"][-1].content
    result = classifier_model.invoke(
        f"Classify this message as 'question', 'complaint', or 'other'. "
        f"Return JSON with 'category' and 'confidence' (0-1): {last_message}"
    )
    parsed = json.loads(result.content)
    return {"category": parsed["category"], "confidence": parsed["confidence"]}

Notice category and confidence are plain fields, not Annotated with a reducer. That's a direct application of lesson 02's own framework: a classifier's output for the current message is a new fact each time, not something that needs merging across two nodes updating the same field concurrently, so a plain overwrite-on-write field is the correct choice here, unlike messages. The source material's own exercise 2 tracks these exact two fields with no reducer discussion at all; it happens to land on the right answer without ever explaining why it's right.

Step 1: Score confidence, don't just classify

A bare category ("question", "complaint", "other") throws away information the model already has: how sure it is. Ask for both in one call. It costs nothing extra and gives the routing function something better than a coin flip to work with.

Step 2: Route on a threshold, not just the category
python
def route_by_confidence(state: State) -> Literal["handle_confidently", "ask_for_clarification"]:
    if state["confidence"] >= 0.75:
        return "handle_confidently"
    return "ask_for_clarification"

A low-confidence classification routing to the same handler as a high-confidence one is how a misrouted complaint ends up answered like a question. The threshold is the actual design decision; the classification itself is just the input to it.

Step 3: Keep the classifier's own context narrow

two-architectures lesson 02's context-window budget breaks a typical turn down to roughly 18% tool definitions, 20% retrieved documents, 28% history, and only 12% the actual user message, with three typical MCP servers alone capable of consuming 70%+ of a 200K window in schema before anything else happens. A classifier node doing one narrow job needs almost none of that. Give it a small, focused prompt rather than the same tool-and-context-loaded prompt your main agent node uses; a classification is cheap by nature, and a bloated prompt is the only thing that would make it expensive.

Classification is a tool call; routing is the same conditional edge you already built
Quick check — Your classify_message node writes category and confidence to state on every call. Should these fields use a reducer like add_messages?

What this buys you before lesson 05

You can now build a node that classifies with a confidence score and route on that score using the exact conditional-edge mechanism from lesson 01. Lesson 05 turns to a different kind of model output: not a classification the model reports on itself, but a tool call the model emits to take an action in the world.

Continue to Lesson 05

Tools, the basic loop: binding a real tool to your agent, and why the source material's own hand-parsed keyword matching is the wrong way to decide when to call one.

Have a question about this lesson?

Reply here and it goes straight to Rod. Same as replying to one of his emails.