LangGraphLangGraph 1.2 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
38 small wins to finish your pathNext lesson →

Agent loop

The agent loop is the pattern that makes an agent: call the model, and if it asked for a tool, run the tool and loop back to the model, otherwise stop.

Last updated: 29 Sep, 2026 · LangGraph 1.2

Everything so far comes together here. The one new idea is the edge from the tools node back to the model, so the model can read the tool's result and decide again.

The ReAct agent: sending the tool result back to the LLM · from the Complete Agentic AI Course In 10 Hours · 235:47 to 240:39

From one tool call to a loop

In the graph from ToolNode and tools_condition, a question with two parts got only one part done, because the tool's result went to END. The video's fix is one edge: "tools" goes back to tool_calling_llm instead of to END. The model reads each tool result and decides again, to call another tool or to answer. The video calls this the ReAct architecture, after its three steps: act (call a tool), observe (read what came back) and reason (decide what to do next).

The ReAct loop: tool_calling_llm calls tools, tools goes back to tool_calling_llm, and the run ends when the model calls no tool.

The video's two-part question, "Give me the recent ai news and then multiply 5 by 10", runs below on the looping graph with both of its tools: tool, the TavilySearch from ToolNode and tools_condition, which needs langchain-tavily and TAVILY_API_KEY, and multiply. It is a complete script: save it as its own file, such as agent_loop.py, and set GROQ_API_KEY and TAVILY_API_KEY first (add the load_dotenv() lines at the top if they are in .env). The code is the video's looping graph; only the model string changes:

ExampleAPI keyFrom the video, run on Groq
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph import StateGraph,START,END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from langchain.chat_models import init_chat_model
from langchain_tavily import TavilySearch

class State(TypedDict):
    messages:Annotated[list,add_messages]

llm=init_chat_model("groq:openai/gpt-oss-120b")

tool=TavilySearch(max_results=2)

## Custom function
def multiply(a:int,b:int)->int:
    """Multiply a and b

    Args:
        a (int): first int
        b (int): second int

    Returns:
        int: output int
    """
    return a*b

tools=[tool,multiply]
llm_with_tool=llm.bind_tools(tools)

## Node definition
def tool_calling_llm(state:State):
    return {"messages":[llm_with_tool.invoke(state["messages"])]}

## Grpah
builder=StateGraph(State)
builder.add_node("tool_calling_llm",tool_calling_llm)
builder.add_node("tools",ToolNode(tools))

## Add Edges
builder.add_edge(START, "tool_calling_llm")
builder.add_conditional_edges("tool_calling_llm",tools_condition)
builder.add_edge("tools","tool_calling_llm")   # the one change: back to the model

## compile the graph
graph=builder.compile()

response=graph.invoke({"messages":"Give me the recent ai news and then multiply 5 by 10"})
for m in response['messages']:
    m.pretty_print()

The model asked for tavily_search first, with its own query and an advanced search over the past month. Because "tools" now leads back to the model, it read the two results and then asked for multiply with 5 and 10, which returned 50. Its last message has no tool call: a table of the news, each row naming the result it came from, and 5 × 10 = 50. The video's run on the tools-to-END graph stopped after the search; here both parts are done. Search results change from day to day, so your run will find other pages and word its answer differently. Below, the support agent builds the same loop around an order lookup.

The agent-loop wiring

python
from langgraph.graph import MessagesState, StateGraph, START, END
from langgraph.prebuilt import ToolNode, tools_condition

def call_model(state: MessagesState):
    return {"messages": [model_with_tools.invoke(state["messages"])]}

builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "call_model")
builder.add_conditional_edges("call_model", tools_condition)  # -> "tools" or END
builder.add_edge("tools", "call_model")                       # tools back to the model
agent = builder.compile()

The model here is the real one from the chat-models lesson, given the tool with bind_tools so it can ask for it. Build the graph one piece at a time.

The imports

Start with the imports: the graph pieces, the tools node and its router, the model builder, the tool decorator, and the message types.

python
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition   # tools node + ready-made router
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain.messages import HumanMessage, SystemMessage

The tool

Define the tool the agent can call. It takes an order id and returns a short line about the order.

python
@tool
def lookup_order(order_id: str) -> str:
    """Look up an order by id."""
    return f"Order {order_id}: shipped on 3 March."

The model node

Give the model the tool with bind_tools, and a system message that keeps its answers short and grounded. The node sends the whole conversation and appends the model's reply.

python
model = init_chat_model("groq:openai/gpt-oss-120b", temperature=0)  # uses your GROQ_API_KEY
model_with_tools = model.bind_tools([lookup_order])
SYSTEM = SystemMessage("You are the support assistant for a small online shop. "
                       "Answer in one short sentence, using only what the tools returned.")

def call_model(state: MessagesState):
    return {"messages": [model_with_tools.invoke([SYSTEM] + state["messages"])]}
  • bind_tools tells the model which tools it may ask for.
  • Each call gets the system message plus the conversation so far, and the reply is either a tool call or a final answer.

Wiring the loop

Wire the two nodes into a graph. The edges are what make it loop.

python
builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_node("tools", ToolNode([lookup_order]))
builder.add_edge(START, "call_model")
builder.add_conditional_edges("call_model", tools_condition)  # tool call? -> tools, else END
builder.add_edge("tools", "call_model")                       # the loop: back to the model
agent = builder.compile()
  • tools_condition sends a tool call to the tools node, and anything else to END.
  • The edge from tools back to call_model is the loop: run the tool, then let the model read the result.

Running the agent

Run one question through the agent and print every message with its type, so you can watch the loop.

python
out = agent.invoke({"messages": [HumanMessage("Where is order A17?")]})
for m in out["messages"]:
    calls = [(c["name"], c["args"]) for c in getattr(m, "tool_calls", [])]
    print(f"{m.type:<5}", calls or m.content)

The agent loop end to end

The same pieces in one file.

ExampleAPI key
from langgraph.graph import StateGraph, START, MessagesState
from langgraph.prebuilt import ToolNode, tools_condition
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain.messages import HumanMessage, SystemMessage

@tool
def lookup_order(order_id: str) -> str:
    """Look up an order by id."""
    return f"Order {order_id}: shipped on 3 March."

model = init_chat_model("groq:openai/gpt-oss-120b", temperature=0)  # uses your GROQ_API_KEY
model_with_tools = model.bind_tools([lookup_order])
SYSTEM = SystemMessage("You are the support assistant for a small online shop. "
                       "Answer in one short sentence, using only what the tools returned.")

def call_model(state: MessagesState):
    return {"messages": [model_with_tools.invoke([SYSTEM] + state["messages"])]}

builder = StateGraph(MessagesState)
builder.add_node("call_model", call_model)
builder.add_node("tools", ToolNode([lookup_order]))
builder.add_edge(START, "call_model")
builder.add_conditional_edges("call_model", tools_condition)
builder.add_edge("tools", "call_model")
agent = builder.compile()

out = agent.invoke({"messages": [HumanMessage("Where is order A17?")]})
for m in out["messages"]:
    calls = [(c["name"], c["args"]) for c in getattr(m, "tool_calls", [])]
    print(f"{m.type:<5}", calls or m.content)

How the run reached its answer

Read it top to bottom. The model's first reply was a tool call, so tools_condition sent the run to the tools node. ToolNode ran lookup_order and appended its result, the edge sent it back to the model, and this time the model answered in plain text from that result, so the run reached END.

How a run flows

  • START → call_model: the model reads the conversation.
  • call_model → tools (via tools_condition) if it asked for a tool; the tool runs and returns a result.
  • tools → call_model: the model reads the result and answers, or asks for another tool.
  • call_model → END when the model replies with no tool call.

The load-bearing edge

The edge tools → call_model is what makes this an agent rather than a one-shot call. Without it the model could ask for a tool but never see the answer. With it, the model can look something up, read the result, and use it in its reply.

Where the agent loop fits

  • Any assistant that looks things up before answering.
  • The base shape every tool-using agent is built on, by hand here and with create_agent in the next lesson.
Watch out. A model that keeps asking for tools keeps the loop going, and the default step cap is large. Pass a recursion_limit, as in Loops and recursion limit, so a runaway run stops with GraphRecursionError.
Try it yourself
  • Draw the four transitions above on paper before running anything.
  • Remove the tools → call_model edge and describe what breaks.

Every expert started right here.