Skip to content
Rearway logorearwayLet's talk ↗

AI engineering

Tool selection in LangGraph agents: patterns that hold up in production

Rearway team · Nov 2026 ·

An agent is a model in a loop with tools and memory. The failure that matters in production is not a clumsy sentence. It is a tool call that should not have happened, or a tool call that never happened because the model wandered. LangGraph is useful here because the loop is a graph you can draw: nodes, conditional edges, and a state object that survives each step. Tool selection is the design of that graph, not a hope that the model will 'figure out the API'.

State is the contract

Every node should read and write an explicit state. Messages, the current intent, the documents already retrieved, the tools already called, and whether a person has approved a side effect. If that state is a bag of strings, the next node cannot make a reliable decision. We keep state small and typed. A field that is not in the state is not available to the next step. That sounds strict. It is how you stop a tool from seeing a secret that belonged to a different turn.

One router, then specialists

A single node that may call any tool in a large catalogue will eventually call the wrong one, because the descriptions overlap. A pattern that holds up is a router node with a small menu: answer from context, look up, take an action, or hand off. Each branch is its own node with its own tool subset. The router does not get the payment tool. The action node does not get the whole corpus. Narrow tools are easier to describe, easier to test, and easier to disable.

class State(TypedDict):
    messages: list
    intent: str
    approved: bool

def route(state: State) -> str:
    if state["intent"] == "action" and not state["approved"]:
        return "human"
    if state["intent"] == "lookup":
        return "retrieve"
    if state["intent"] == "action":
        return "tools"
    return "answer"

graph.add_conditional_edges("router", route)

Schemas beat prose

A tool described only in a paragraph will be called with missing arguments. Give each tool a schema: required fields, enums where the set is closed, and a description of the side effect in one sentence. If the model emits arguments that do not validate, the node returns the validation error into state and stops. It does not 'try something close'. Close is how a refund becomes a cancellation.

Human-in-the-loop is an edge, not a comment

Some steps must wait. Sending a message to a customer, changing a record, spending money, deleting data. In the graph that is an interrupt: the run pauses, state is stored, and a person resumes or rejects. The model does not get a second chance to call the tool while it waits. When the person resumes, the graph continues from the node you named, with the approval bit set. If you only prompt the model to 'ask before doing anything important', you do not have a control. You have a suggestion.

Guardrails around the loop

  • An allowlist of tools for each node. Anything else is a bug, not a creative choice.
  • A maximum number of steps so a loop cannot chatter.
  • Output checks for the final answer: required fields, refusal when context is empty, no claim that a tool ran if it did not.
  • Traces that record the route, the tool name, and the validation result. You cannot fix a router you cannot see.

What we test

We test the router on a set of utterances that should land on each branch, including ones that look similar and must not. We test that a forbidden tool cannot be reached from the answer node. We test that a side effect does not run when approval is missing. These are ordinary software tests. The model is one component. The graph is the product.

Tool selection holds up when the graph is boring: a small router, specialist nodes, schemas, an interrupt for side effects, and traces. The model chooses among the options you actually built. It does not invent the options. If you cannot draw the path from a message to a tool, you do not have a selection policy yet. You have a prompt, and a prompt is not a control. Draw the graph before you tune the wording.

How a turn actually moves

A message arrives on a channel. The adapter writes it into state and does nothing else. The router classifies intent against the small menu. Lookup goes to retrieval and then to an answer node that may only quote what retrieval returned. Action goes to the interrupt if the tool has a side effect, and only then to the tool node, which may call the one schema it owns. If validation fails, the error is written to state and the graph stops on that branch instead of improvising a second tool. The final node checks that the reply does not claim a tool ran unless the trace says it ran. That sequence is the pattern. The model fills in classification and argument values. The edges decide what is possible. When we review a graph we read the edges first. If a dangerous tool is reachable from a casual intent, the graph is wrong even if the prompt says to be careful. Production behaviour is the graph you can draw, plus the traces that show which edge fired. We also name the failure we will not tolerate: a tool that writes, sends, or deletes without the interrupt having fired. If a trace can show that path, the graph is not ready, regardless of how fluent the reply looks.

All notes ↗