Conditional routing

Conditional edges in LangGraph are just functions. They read the current state and return the string name of the next node. Our router is short, but every branch is deliberate.

retriever.py
python
MAX_DEPTH = 5

def _route(state: RetrievalState) -> str:
    if state["confidence"] < 0.3:
        logger.info("Low confidence, stopping traversal")
        return "end"
    if state["depth"] >= MAX_DEPTH:
        logger.info("Max depth reached, retrieving current node")
        return "retrieve"
    if state["should_descend"] and state["current_node"].children:
        return "descend"
    return "retrieve"

Four guard rails in order: low confidence stops the agent, max depth forces retrieval, the descend flag plus available children triggers descent, otherwise retrieve what we have.

Max depth is the circuit breaker. Without it, a buggy tree or a confused LLM could keep descending forever. Five is generous for most technical documents: chapter, section, subsection, sub-subsection, and one extra level of slack. Past that, we just retrieve what we have.

Ordering exercise: Put the routing guards in the correct order

Loading practice…

retriever.py
python
def _make_descend():
    def descend(state: RetrievalState) -> dict:
        current: TreeNode = state["current_node"]
        target_id = state.get("target_child_id")

        # Find the chosen child by id, or fall back to the first
        target = next(
            (c for c in current.children if c.id == target_id),
            current.children[0],
        )
        return {"current_node": target}
    return descend

The descend node is a pure state update. It swaps current_node for the chosen child, then the graph routes back to analyze. The closure factory pattern lets us share client and model across nodes cleanly.

Quiz: Quiz

Loading practice…