LangChain Agents & Tool Use
A chain runs a fixed path (L2). An agent lets the model choose the path — which tools to call, in what order, when to stop. This chapter defines tools the LangChain way, runs a tool-calling agent, and shows why the modern answer to "build an agent" points at LangGraph's prebuilt ReAct agent.
- an Anthropic API key (
ANTHROPIC_API_KEY) +pip install anthropic
Learning objectives
- Define tools with the
@tooldecorator and clear schemas/descriptions. - Bind tools to a model and read back tool-call requests.
- Run a tool-calling agent and trace its reason→act→observe loop.
- Understand why LangGraph's
create_react_agentis the current recommended path. - Add safety: approval gates, error handling, and step limits.
Defining tools intermediate
A LangChain tool is a function plus a schema and a description. The @tool decorator builds all three from a typed function with a docstring.
tools.pyfrom langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""Get the current weather for a city. Use when the user asks about weather."""
return f"18°C and clear in {city}"
@tool
def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
print(get_weather.name) # "get_weather"
print(get_weather.description) # from the docstring — drives tool selection
print(get_weather.args) # {"city": {"type": "string"}} — from the type hints
A tool is just a normal Python function that you let the model call. The @tool decorator sits on top of the function and, from the function itself, builds the three things the model needs to use it: a name, a description, and a list of arguments with their types. You write one plain function; LangChain turns it into something an agent can reach for.
@toolis a decorator — a label placed on the line above a function. It wrapsget_weatherso that instead of a plain function you get a LangChain tool object, without changing the code inside.- The function body is ordinary Python:
get_weathertakes acitystring and returns a weather sentence;addtakes two integers and returns their sum. This is the real work the tool does when called. - The triple-quoted line under each
defis the docstring. LangChain uses it as the tool's description — the text the model reads to decide when to call the tool. Notice it says when to use it ("Use when the user asks about weather"), not just what it does. - The three
printlines read back what the decorator built:.nameis the function name,.descriptioncomes from the docstring, and.argsis inferred from thecity: strtype hint — you didn't write the schema by hand, the type hints did it for you.
What the output means: Three lines print: get_weather (the name), the docstring text (the description), and {"city": {"type": "string"}} (the argument schema). Together these are exactly what the model sees when deciding whether to use this tool.
Try this: Change the docstring to something vague like "Gets weather." and imagine you are the model choosing a tool — the clear "Use when…" version is much easier to pick correctly. A vague description is the #1 reason an agent ignores or misuses a tool.
Binding tools to a model intermediate
Before an agent, understand the primitive under it: bind_tools. It tells the model which tools exist; the model responds with tool-call requests you then execute — exactly the C2 pattern, one layer down.
Requires: pip install langchain-anthropic
bind.pyfrom langchain_anthropic import ChatAnthropic
model = ChatAnthropic(model="claude-opus-4-8", max_tokens=512)
model_with_tools = model.bind_tools([get_weather, add])
msg = model_with_tools.invoke("What's the weather in Paris?")
print(msg.tool_calls) # [{'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': '...'}]
bind_tools gets you a request — name + args + id. Running the function and returning the result is still your job. An agent is the thing that automates that request→run→feed-back loop for you.This is the primitive underneath an agent: bind_tools. It hands the model the list of tools that exist, then the model replies with a request to call one — it does not run the tool itself. Running it and feeding the result back is still your job. An agent (next lab) is just the thing that automates that request→run→feed-back loop.
ChatAnthropic(model="claude-opus-4-8", max_tokens=512)creates the model client.modelpicks which Claude to use;max_tokenscaps how long the reply can be.model.bind_tools([get_weather, add])makes a new model handle that knows about those two tools. It doesn't call them — it just makes them available for the model to request..invoke("What's the weather in Paris?")sends the question. Because the question is about weather, the model responds by asking to callget_weatherwithcity="Paris".msg.tool_callsis that request: a list with the tool name, the args the model filled in, and an id. This is a plan to call a tool, not the tool's answer.
What the output means: msg.tool_calls prints something like [{'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': '...'}] — the model chose the right tool and extracted Paris from the sentence on its own. There is no weather text yet because nothing has actually run the function.
Try this: Ask "What is 12 plus 30?" instead and look at tool_calls — the model should now request add with {'a': 12, 'b': 30}. Same mechanism, different tool, chosen from the descriptions.
Lab L3.2 · A tool-calling agent intermediate
The agent runtime closes the loop: it calls the model, executes any requested tools, feeds results back, and repeats until the model answers. The modern, recommended way to get one is LangGraph's create_react_agent — a prebuilt ReAct agent (L1) that LangChain now points to as the default.
while loop. Model emits tool calls → runtime executes them → results go back as messages → model decides again. It exits when the model returns a plain answer with no tool calls. You supply model + tools; the runtime supplies the loop.
This picture is the agent loop — what create_react_agent does for you in the next lab. It's a cycle between the model and your tools that repeats until the model has a final answer.
- Start at the Model box on the left. The model looks at the conversation and decides: do I need a tool? The label
tool_calls?on the arrow is that decision point. - If it wants a tool (top arrow), control moves right to Run tools — the runtime actually executes the requested function (the step
bind_toolsleft up to you). - The curved arrow back to Model is labelled results fed back as messages: the tool's output is added to the conversation and handed back to the model, which can now decide again. That back-and-forth is the loop.
- If it wants no tool (the
no tool_callsarrow on the far right), the loop exits and the model returns a plain answer. That's how the agent knows it's done.
In short: You supply the model and the tools; the runtime supplies the loop. It is the exact "ask for a tool → run it → feed the result back → ask again" cycle, done automatically until the model stops asking for tools.
shellpip install langgraph langchain-anthropic
Requires: pip install langchain-anthropic langgraph
agent.pyfrom langgraph.prebuilt import create_react_agent
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(model="claude-opus-4-8", max_tokens=1024)
agent = create_react_agent(model, tools=[get_weather, add]) # prebuilt ReAct loop
result = agent.invoke({"messages": [("user", "Weather in Paris, and what's 12 + 30?")]})
for m in result["messages"]:
m.pretty_print() # see the full reason→act→observe trace
Here the loop from the diagram becomes one line of code. create_react_agent gives you a ready-made agent that wraps a model and a set of tools and runs the whole call-model → run-tools → repeat cycle for you until the model produces a final answer.
- The two
importlines bring increate_react_agent(the prebuilt agent from LangGraph) andChatAnthropic(the model, same as the previous lab). create_react_agent(model, tools=[get_weather, add])builds the agent. You pass it the model and the tools; it supplies the loop. Nothing has run yet — this just assembles the agent.agent.invoke({"messages": [("user", "...")]})starts it with a question that needs two tools (weather and arithmetic). The agent will call each tool as needed and keep going until it can answer.- The
for m in result["messages"]loop withm.pretty_print()prints every step of the conversation — the model's tool requests, the tool results, and the final reply — so you can watch the agent reason, act, and observe.
What the output means: You see the full trace: the agent asking to call get_weather('Paris'), the tool result coming back, a call to add(12, 30), its result, and finally a plain-language answer combining both. That sequence of messages is the loop from the diagram, printed out.
Try this: Ask a question that needs only one tool ("What's the weather in Tokyo?") and read the trace — you'll see just one tool call, then the answer. The agent uses only the tools the question actually requires.
AgentExecutor tutorials are legacyYou'll find lots of examples using initialize_agent / AgentExecutor and "agent types" like ZERO_SHOT_REACT_DESCRIPTION. Those are the older LangChain agent API. The current guidance is the LangGraph-based create_react_agent shown here — it's more controllable and it's the bridge into L4/L5. Learn this one.Why the agent lives in LangGraph now advanced
Notice the import: the recommended agent comes from langgraph, not langchain. That's deliberate. A real agent needs things a plain chain can't express:
| Need | Why a chain can't & a graph can |
|---|---|
| Cycles | The loop runs an unknown number of times — chains are acyclic; graphs allow cycles (L4/L5) |
| State | Messages, scratchpad, counters persist across steps — graphs carry explicit state (L4) |
| Branching | "If tool call → run tools, else → end" is conditional routing (L4) |
| Human-in-the-loop | Pause for approval before a risky tool, then resume — needs interrupts + persistence (L5) |
create_react_agent is a prebuilt graph — convenient, but a black box. The next two chapters open it up: L4 builds the state + routing from scratch, L5 adds cycles, human approval, and durable persistence. When the prebuilt agent isn't enough, you'll build your own.Lab L3.3 · Making the agent safe advanced
An agent that can call tools can cause effects. The same discipline from Chapter 4 and Claude Code (C3) applies here.
Requires: pip install langchain-core
safe_tools.pyfrom langchain_core.tools import tool
@tool
def delete_record(record_id: str) -> str:
"""Delete a record by id. Destructive — requires confirmation."""
if not _approved(record_id): # human-in-the-loop gate
return "BLOCKED: awaiting human approval."
try:
_do_delete(record_id)
return f"Deleted {record_id}."
except Exception as e:
return f"ERROR: {e}" # return errors so the agent can adapt
A tool that can change things (delete, send, pay) can cause real damage if the model calls it wrongly. This lab shows the safety pattern for such tools: check for approval before acting, and return errors as text instead of crashing. Both keep the agent controllable.
delete_recordis a normal@tool, but its docstring flags it as destructive — a signal to both the reader and the model that it needs care.if not _approved(record_id):is the approval gate. Before doing anything, the tool checks whether a human has approved this action; if not, it returns"BLOCKED: awaiting human approval."and stops. Nothing is deleted.- Only after approval does the
try:block run the real delete via_do_delete. Wrapping it intrymeans a failure won't crash the whole agent. except Exception as e:catches any failure and returns it as a string ("ERROR: {e}") instead of raising. The agent reads that error like any other tool result and can adapt — an uncaught exception would kill the run instead.
What the output means: If the record isn't approved, the tool returns BLOCKED and nothing happens. If it is approved and the delete works, it returns Deleted {record_id}.. If the delete throws, the agent receives an ERROR: string it can react to — the run keeps going either way.
Try this: Trace what happens when _approved returns False: the very first if returns the BLOCKED message, so _do_delete is never reached. That single guard line is what stands between the model and an irreversible action.
| Guardrail | How |
|---|---|
| Approval gate | Destructive tools check for confirmation before acting (real gate: L5 interrupts) |
| Least privilege | Only give the agent tools it needs; prefer read/notify over write/delete |
| Return errors, don't raise | A tool that returns an error string lets the agent recover; an uncaught exception kills the run |
| Step limit | Cap iterations (a recursion/step limit) so a confused agent can't loop forever |
Common pitfalls expert
| Pitfall | Fix |
|---|---|
Following AgentExecutor/initialize_agent tutorials | Use LangGraph's create_react_agent (current path) |
| Vague tool docstrings | State when to call the tool, not just what it does |
| Tools that raise on error | Return an error string so the agent can adapt |
| No step limit → infinite loops | Set a recursion/step cap |
| Ungated destructive tools | Add an approval gate (real one in L5) |
| Reaching for a prebuilt agent when a chain fits | If the path is fixed, use a chain (L2) |
Exercises expert
Exercise L3.1 — Three-tool agent
Context: Reading a trace to confirm which tools fired is how you verify an agent actually reasoned rather than guessed.
Your task: Give create_react_agent three tools — get_weather, add, and a search_docs tool backed by your L2 retriever — then ask a question that needs two of them.
Requirements:
- Wire three distinct tools, including one backed by your earlier retriever
- Ask a question that requires two of the three
- Read the trace to confirm both were actually called
- Confirm the agent chose them itself
💡 Hint: Craft the question so a single tool can't answer it — that forces the multi-tool path you want to observe.
Exercise L3.2 — Same agent, raw SDK
Context: When the framework is worth it — versus a clearer raw loop — depends on how many tools and how much custom gating you need.
Your task: Rebuild a framework agent using only the raw Chapter-2 manual tool loop, then compare line count and control.
Requirements:
- Reproduce the agent with a hand-written tool loop and the raw SDK
- Compare line count against the framework version
- Compare how much control each gives you
- State when the framework is worth it and when the raw loop is clearer
💡 Hint: The raw loop stays clear for one or two tools with no gating; the framework pulls ahead as tools and custom control multiply.
Exercise L3.3 — Gate a destructive tool
Context: A tool-level flag can refuse an action, but it cannot pause the whole run for a human and resume — which is exactly what L5's interrupts provide.
Your task: Add a send_email tool that returns "BLOCKED" unless an approved=True flag is set, prompt the agent to send an email, and confirm it's blocked.
Requirements:
- The tool refuses unless an approval flag is set
- Prompt the agent to use it and confirm the refusal
- Explain why a real gate must pause the run for a human
- Connect that to LangGraph interrupts + a checkpointer (L5)
💡 Hint: A flag can say no, but only a checkpointed interrupt can stop, surface the pending action, and resume on approval — the production-grade gate.
Show the connection
A tool-level flag can refuse, but it can't pause and wait for a human then resume where it left off. LangGraph interrupts + a checkpointer (L5) stop the graph, surface the pending action, and resume on approval — the production-grade version of this gate.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: LangChain's @tool builds a name, a description, and an argument schema from a typed function with a docstring — because the model never sees your code, only that packaged interface.
Your task: Model @tool: a decorator that captures the name, description, and arg schema from any typed function.
Requirements:
- Capture the function name as the tool name
- Take the description from the docstring
- Derive the arg schema from the type hints (via
inspect) - Attach all three to the function and show them
💡 Hint: inspect.signature gives you the parameters and their annotations; the docstring drives selection, the hints drive the schema.
Show solution
The docstring drives selection; the type hints drive the schema. Model @tool in stdlib:
import inspect
def tool(fn):
fn.tool_name = fn.__name__
fn.description = (fn.__doc__ or "").strip()
sig = inspect.signature(fn)
fn.args = {p.name: (p.annotation.__name__ if p.annotation != inspect._empty else "any")
for p in sig.parameters.values()}
return fn
@tool
def get_weather(city: str) -> str:
"Get the current weather for a city."
return f"18C and clear in {city}"
print(get_weather.tool_name) # get_weather
print(get_weather.description) # from the docstring
print(get_weather.args) # {'city': 'str'}
The model never sees your code — only the name, description, and arg schema. That is what @tool packages.
Context: Binding tools tells the model the schemas so it can emit a structured call. The model requests a call; you still run the tool and feed the result back.
Your task: Model a tool-bound model: given a user message, a stub model returns a tool-call request, which you then dispatch to the real tool.
Requirements:
- The stub model decides whether to request a tool and with what arguments
- Return either a tool-call request or direct text
- A dispatcher runs the requested tool with its args, or returns the text
- Show one message that triggers a tool and one that doesn't
💡 Hint: Separate "the model asks for a call" from "you execute it" — binding only communicates the schema; execution is still your job.
Show solution
The model requests a call; you dispatch it. Runnable:
TOOLS = {"get_weather": lambda city: f"18C in {city}",
"add": lambda a, b: a + b}
def model_with_tools(msg):
# stub: decide which tool the model would ask for
if "weather" in msg.lower():
return {"tool_call": {"name": "get_weather", "args": {"city": "Paris"}}}
return {"text": "I can answer directly."}
def dispatch(resp):
if "tool_call" in resp:
tc = resp["tool_call"]
return TOOLS[tc["name"]](**tc["args"])
return resp["text"]
print(dispatch(model_with_tools("what's the weather?"))) # 18C in Paris
print(dispatch(model_with_tools("hi"))) # I can answer directly.
Binding tools = telling the model the schemas so it can emit a structured call. You still run the tool and feed the result back.
Context: The agent runtime is the reason→act→observe loop: the model requests tools, you execute and append observations, repeat until a final answer — exactly what create_react_agent compiles.
Your task: Model a tool-calling agent loop offline on a multi-step task.
Requirements:
- The model reads the message history to decide the next tool call or a final answer
- Execute each requested tool and append the result as a tool message
- Loop until the model returns a final answer
- Bound the loop with a max-steps cap
- Show it completing a task that needs more than one tool call
💡 Hint: Append tool results as messages the model reads next round; a multi-step task (e.g. add then multiply) exercises the loop properly.
Show solution
The agent runtime is this loop. Runnable stdlib:
TOOLS = {"add": lambda a, b: a + b, "mul": lambda a, b: a * b}
def model(msgs):
# stub reasoner: (2+3)*4 in two tool calls, then answer
done = [m for m in msgs if m["role"] == "tool"]
if len(done) == 0:
return {"call": ("add", {"a": 2, "b": 3})}
if len(done) == 1:
return {"call": ("mul", {"a": done[0]["content"], "b": 4})}
return {"final": done[-1]["content"]}
def run(task, max_steps=6):
msgs = [{"role": "user", "content": task}]
for _ in range(max_steps):
resp = model(msgs)
if "final" in resp:
return resp["final"]
name, args = resp["call"]
result = TOOLS[name](**args)
msgs.append({"role": "tool", "content": result})
return "step limit"
print(run("compute (2+3)*4")) # 20
This reason->act->observe loop is exactly what LangGraph's create_react_agent compiles for you.
Context: Production agents need brakes: a step limit, error isolation around each tool, and an approval gate before a dangerous tool — the L5 human-in-the-loop pattern applied per tool.
Your task: Wrap the tool-call step with a step limit, a try/except around each tool, and an approval gate that blocks dangerous tools.
Requirements:
- A step limit stops runaway loops
- A try/except keeps one failing tool from crashing the run
- An approval gate blocks tools on a dangerous list unless approved
- Return a structured result distinguishing result / blocked / error
- Show an allowed call, a blocked call, and a caught error
💡 Hint: Route every tool call through one guarded function that checks the gate, then runs inside try/except; a denied dangerous tool returns a block, not a result.
Show solution
Three brakes around the loop. Runnable:
DANGEROUS = {"delete_file"}
TOOLS = {"read_file": lambda p: "contents", "delete_file": lambda p: "deleted"}
def guarded_call(name, args, approve):
if name in DANGEROUS and not approve(name, args):
return {"blocked": f"approval denied for {name}"}
try:
return {"result": TOOLS[name](**args)}
except Exception as e:
return {"error": repr(e)} # errors don't crash the loop
def auto_deny(name, args):
return False # a human would decide here
print(guarded_call("read_file", {"p": "a.txt"}, auto_deny)) # result
print(guarded_call("delete_file", {"p": "a.txt"}, auto_deny)) # blocked
print(guarded_call("read_file", {"nope": 1}, auto_deny)) # error, caught
Step limits stop runaways, try/except keeps one bad tool from killing the run, and the approval gate is the L5 human-in-the-loop pattern applied per tool.
Context: The modern answer to "build an agent" points at LangGraph's prebuilt create_react_agent — itself a compiled graph — and you drop to raw LangGraph only for the control it hides.
Your task: Write a decision helper for when to use the prebuilt ReAct agent versus raw LangGraph.
Requirements:
- Default to
create_react_agentfor a standard ReAct loop - Drop to raw LangGraph for custom routing, extra state, human pauses, or multi-agent
- Take those needs as parameters
- Show a case choosing the prebuilt and a case choosing raw LangGraph
💡 Hint: If any of custom-routing / extra-state / HITL / multi-agent is true, you've outgrown the prebuilt; otherwise don't hand-write the loop.
Show solution
Use the prebuilt until you need control it hides. Runnable:
def agent_choice(needs_custom_routing, needs_extra_state,
needs_human_pause, needs_multi_agent):
if any([needs_custom_routing, needs_extra_state,
needs_human_pause, needs_multi_agent]):
return "raw LangGraph -- you need control create_react_agent hides"
return "create_react_agent -- prebuilt ReAct loop, don't hand-write it"
print(agent_choice(False, False, False, False)) # prebuilt
print(agent_choice(True, False, True, False)) # raw LangGraph
create_react_agent is itself a compiled LangGraph graph — start there, and drop to raw nodes/edges only for custom routing, extra state, HITL, or multi-agent (L4/L5).
Context: The production tool-calling agent is the prebuilt create_react_agent driving real @tool functions against a Claude model — the loop you built by hand, now compiled.
Your task: Write the real prebuilt agent with a Claude model and at least one @tool function, using documented APIs. (Needs the libraries installed.)
Requirements:
- Define a tool with the real
@tooldecorator (typed + docstring) - Create a
ChatAnthropicmodel - Build the agent with
create_react_agent(model, tools=[...]) - Invoke it with a messages input and read the final answer
- Note that
interrupt_beforeadds the L5 approval gate; label it as needinglanggraph/langchain-anthropic+ a key
💡 Hint: You supply the tools; the prebuilt agent drives the reason→act→observe loop you modeled by hand — verify imports against your installed version.
Show solution
Correct prebuilt agent. Needs pip install langgraph langchain-anthropic + ANTHROPIC_API_KEY:
from langchain_core.tools import tool
from langchain_anthropic import ChatAnthropic
from langgraph.prebuilt import create_react_agent
@tool
def get_weather(city: str) -> str:
"Get the current weather for a city."
return f"18C and clear in {city}"
model = ChatAnthropic(model="claude-opus-4-8", max_tokens=512)
agent = create_react_agent(model, tools=[get_weather])
result = agent.invoke(
{"messages": [{"role": "user", "content": "weather in Paris?"}]}
)
print(result["messages"][-1].content) # model answers using the tool
You write the tools; the prebuilt agent drives the reason->act->observe loop you modeled by hand above. Add interrupt_before for the L5 approval gate. Verify imports against your installed version.
✓ Checkpoint — you can move on when you can…
- Define a tool with
@tooland explain why the docstring matters. - Use
bind_toolsand read backtool_calls. - Run
create_react_agentand read its message trace. - Explain why the recommended agent lives in LangGraph.
- Add approval gates, error handling, and a step limit.
terraform_plan, kubectl_get, describe_pod — are exactly these LangChain tools, each with a description that tells the model when to reach for it and a gate on the destructive ones. Whether you drive them with create_react_agent or a custom LangGraph graph (L4/L5) is the choice the next two chapters equip you to make. See the tools + agent-loop build →Knowledge check check yourself
With bind_tools, what does the model actually return, and whose job is it to run the tool?
Show answer
bind_tools only tells the model which tools exist; the model responds with a tool-call request (name + args + id) in msg.tool_calls — it never executes anything. Running the function and feeding the result back is still your job; an agent runtime is what automates that request→run→feed-back loop.Why does the recommended create_react_agent live in langgraph rather than langchain?