Tools, the Basic Loop, and Why Keyword Matching Fails
Prerequisite: Lesson 01 (the current agent-construction API) and lesson 04 (conditional routing) — this lesson assumes you can build a StateGraph and route on a condition function.
After this lesson, you can: bind a real tool to a LangGraph agent, let the model's own structured tool-calling output decide when to use it, and explain why a hand-parsed keyword check is the wrong way to make that decision even though it's what this course's own source material does.
The pattern the source material actually teaches, and the gap in it
Unit 2.1 of the original tutorial series walks through five exercises: mock tool configuration, a "tool calling node," tool execution with error handling, processing tool results, and a complete conversational agent. Read the second exercise's actual decision logic closely and a real problem shows up. The function that decides whether to call a tool looks like this:
def llm_node(state: State) -> State:
if not state.get("messages"):
return {"messages": [HumanMessage(content="What is the capital of France?")], ...}
last_message = state["messages"][-1].content
if "capital of France" in last_message:
return {"tool_calls": [{"tool_name": "TavilySearchResults", "args": {"query": "capital of France"}}]}
return stateThat's a substring check against the user's raw text, hardcoded to one example query. It isn't a bug in the tutorial so much as a snapshot of a pattern that predates native function calling as a first-class LLM capability: before June 2023, deciding "should I call a tool" meant parsing free text for a match, because there was no structured alternative. tools-memory-and-multi-agent-systems lesson 09 names this exact history: prompt-hack tools (2022-2023) parsed Action: text before function calling replaced that with a JSON Schema the model fills in and your code validates. This lesson's source material is still doing the pre-2023 version of that decision, dated 2025.
What a tool actually needs, reused from a different framework's own checklist
Before building the current version, borrow the discipline tools-memory-and-multi-agent-systems lesson 09 already worked out for exactly this problem, credited by name rather than re-derived from scratch: six things every tool-calling setup needs regardless of framework.
| Concern | What it means for a LangGraph tool |
|---|---|
| Auth | API keys from environment variables, never hardcoded (the source's own mock key is fine for a lesson, never for real code) |
| Timeout | Set on every external call the tool makes |
| Retry | Exponential backoff on rate limits and server errors |
| Normalise | Strip the tool's raw response down to what the model needs |
| Error map | A structured failure shape, not a raw exception reaching the model |
| Size limit | Cap what gets injected back into context |
That same lesson's M x N math applies here too: three agents each hand-parsing keywords for ten different tools is thirty brittle string checks to maintain, and every new tool means touching every agent's decision function. Native tool-calling collapses that the same way MCP collapsed the API-integration version of the same problem: the model sees each tool's schema and decides for itself, in one place, using structure instead of substring matching.
Binding a tool the current way
from langchain_core.tools import tool
from langchain.agents import create_agent
@tool
def search_capital(query: str) -> str:
"""Look up factual information like a country's capital city.
Use this when the user asks a factual question you're not certain about.
Do NOT use this for anything conversational or opinion-based."""
# A real implementation calls a search API here, with the six disciplines
# above: auth from env, timeout, retry, normalise, error map, size cap.
return f"Result for: {query}"
agent = create_agent(
model="anthropic:claude-opus-5",
tools=[search_capital],
system_prompt="Answer factual questions using the search tool when needed.",
)Nothing here inspects message text for a keyword. The @tool decorator turns the function's docstring into the schema the model reads, and the model's own output, not your code, decides when search_capital gets called. That decision arrives on the message object as a structured tool_calls list, already parsed, already typed. Compare that to the source material's if "capital of France" in last_message and the difference is the whole point of this lesson: one is a keyword match your code owns, the other is a schema the model reasons over.
The docstring is the tool's schema description doing double duty as documentation. tools-memory-and-multi-agent-systems lesson 07 calls this the second design principle: what it does, when to use it, what it returns, when NOT to use it. Skip the "when NOT to use it" line and the model has no disambiguation signal when two tools could plausibly apply.
The source material's five exercises build a hand-rolled State with a tool_calls field the node populates manually. The current pattern skips that entirely: pass tools=[...] once, at create_agent, and the framework handles routing tool calls to execution and results back into the message history.
After agent.invoke(...), inspect the response message's own tool_calls attribute if you need to branch on whether a tool fired. That attribute is populated by the model's structured output. It is never something your code should derive by scanning the user's text for a keyword.
What this buys you before lesson 06
You can now bind one tool the current way and explain exactly what the source material's own hand-parsed decision logic gets wrong. Lesson 06 builds on this directly: the same create_agent call with multiple tools bound at once, and the routing questions that only show up once a model has more than one tool to choose between.
Multi-tool agent systems: what changes when an agent chooses between more than one tool, and the routing patterns unit 2.2's ten exercises build toward.
Reply here and it goes straight to Rod. Same as replying to one of his emails.