Conditional Routing and Confidence-Scored Classification
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:
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
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.
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.
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.
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.
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.
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.
Reply here and it goes straight to Rod. Same as replying to one of his emails.