LangChain Tools and Agents: The Classic Agent Loop
The Gap Between "Saying" and "Doing": Why LLMs Need Tools
Consider this prompt to a raw language model: "What's the current weather in Tokyo, and how does today's high compare to yesterday's?" The model answers confidently.
🎯 System Scenario & Requirements
System Goal
Consider this prompt to a raw language model: "What's the current weather in Tokyo, and how does today's high compare to yesterday's?" The model answers confidently.
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- Consider this prompt to a raw language model: "What's the current weather in Tokyo, and how does today's high compare to yesterday's?" The model answers confidently.
- It might even produce a plausible-sounding temperature.
- But that answer is fabricated — the model's weights were frozen at training cutoff and it has no mechanism to observe the world at inference time.
Constraints & Assumptions
- AgentExecutor is a pragmatic tool with well-understood limitations.
- Being explicit about them up front saves a lot of debugging time in production.
⚡ Execution Stages & Decision Trace
Stage 1 — What a Tool Actually Is: A Python Function the LLM Can Request
A tool, in LangChain's model, is nothing exotic.
It is a regular Python function that: 1.
Deep Dive
Scenario context
Consider this prompt to a raw language model:
"What's the current weather in Tokyo, and how does today's high compare to yesterday's?"
The model answers confidently. It might even produce a plausible-sounding temperature. But that answer is fabricated — the model's weights were frozen at training cutoff and it has no mechanism to observe the world at inference time. It cannot make an HTTP request. It cannot re…
This walkthrough focuses on The Gap Between "Saying" and "Doing": Why LLMs Need Tools.
Before making a design choice, clarify:
- Consider this prompt to a raw language model: "What's the current weather in Tokyo, and how does today's high compare to yesterday's?" The model answers confidently.
- It might even produce a plausible-sounding temperature.
- But that answer is fabricated — the model's weights were frozen at training cutoff and it has no mechanism to observe the world at inference time.
Concepts in play: llm-agents-and-langgraph, low-level-design-systems, llm-prompt-engineering, event-driven-architecture
Stage 1 — What a Tool Actually Is: A Python Function the LLM Can Request
- A tool, in LangChain's model, is nothing exotic.
- It is a regular Python function that: 1.
- Has a name the LLM uses to refer to it.
- Has a description (from the docstring) that the LLM reads to decide when to call it.
Observable outcome: Has a description (from the docstring) that the LLM reads to decide when to call it.
Stage 2 — Turning Python Functions into LLM Tools with the @tool Decorator
- The Decorator Approach The @tool decorator from langchain core.tools is the simplest way to define a tool.
- Decorate a function, write a meaningful docstring, and annotate the parameters — that's it.
- The docstring is not documentation for human readers — it is the tool's entire identity to the LLM.
- The model reads it to decide whether to call the tool at all.
# NOTE: External API calls below are mocked for local development.
# Replace the mock return values with real API calls in production.
from langchain_core.tools import tool
@tool
def get_current_weather(city: str) -> str:
"""
Fetch the current weather for a given city.
Returns a plain-text description including temperature and conditions.
Use this when the user asks about current or today's weather anywhere.
"""
# Mock: in production, call OpenWeatherMap or similar
mock_data = {
"tokyo": "Tokyo: 18°C, partly cloudy, humidity 65%",
"london": "London: 12°C, overcast, humidity 80%",
"new york": "New York: 22°C, sunny, humidity 45%",
}
return mock_data.get(city.lower(), f"Weather data unavailable for {city}")
@tool
def get_yesterday_weather(city: str) -> str:
"""
Fetch yesterday's high temperature for a given city.
Returns a plain-text description of yesterday's weather summary.
Use this when the user wants to compare today's weather to yesterday's.
"""
# Mock: in production, query a historical weather API
mock_data = {
"tokyo": "Tokyo yesterday: high of 15°C, light rain",
"london": "London yesterday: high of 10°C, cloudy",
"new york": "New York yesterday: high of 20°C, partly cloudy",
}
return mock_data.get(city.lower(), f"Historical weather unavailable for {city}")
from pydantic import BaseModel, Field
from langchain_core.tools import tool
class StockLookupInput(BaseModel):
ticker: str = Field(description="Stock ticker symbol, e.g. AAPL, TSLA, NVDA")
currency: str = Field(default="USD", description="Currency for the price output (USD or EUR)")
@tool("get_stock_price", args_schema=StockLookupInput)
def get_stock_price(ticker: str, currency: str = "USD") -> str:
"""
Return the current stock price for a given ticker symbol.
Use this when the user asks about stock prices or market data.
Do NOT use this for cryptocurrency prices.
"""
# Mock: in production, call Yahoo Finance, Alpha Vantage, or Polygon.io
mock_prices = {"AAPL": 213.07, "TSLA": 172.30, "NVDA": 118.42, "MSFT": 415.20}
price = mock_prices.get(ticker.upper())
if price is None:
return f"No price data found for {ticker}"
if currency == "EUR":
price = round(price * 0.92, 2)
return f"{ticker.upper()} is trading at {currency} {price:.2f}"
Observable outcome: The model reads it to decide whether to call the tool at all.
Stage 3 — Deep Dive: How AgentExecutor Orchestrates the ReAct Loop
- The Internals: AgentExecutor as a Configurable Runnable Loop AgentExecutor is LangChain's agent runner.
- Internally, it is a while-loop wrapped around a runnable chain: on each iteration, it calls the LLM, inspects the output, decides whether to invoke a tool, runs the tool if needed, appends the observation to the message…
- It exits when the LLM produces a final answer (no more tool calls) or when a configured maximum step count is reached.
- The loop follows the ReAct (Reasoning + Acting) pattern, published by Yao et al.
from langchain import hub
from langchain.agents import AgentExecutor, create_react_agent
from langchain_openai import ChatOpenAI
# Pull the standard ReAct prompt from LangChain Hub
# It instructs the LLM to emit Thought/Action/Observation cycles
react_prompt = hub.pull("hwchase17/react")
llm = ChatOpenAI(model="gpt-4o", temperature=0)
tools = [get_current_weather, get_yesterday_weather, get_stock_price]
agent = create_react_agent(llm=llm, tools=tools, prompt=react_prompt)
executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True, # print the full reasoning trace
max_iterations=6, # hard stop to prevent runaway loops
handle_parsing_errors=True, # recover from malformed tool calls
)
result = executor.invoke({"input": "What's the current weather in Tokyo and how does it compare to yesterday?"})
print(result["output"])
> Entering new AgentExecutor chain...
Thought: I need to fetch the current weather in Tokyo and yesterday's weather to compare them.
Action: get_current_weather
Action Input: {"city": "tokyo"}
Observation: Tokyo: 18°C, partly cloudy, humidity 65%
Thought: Now I need yesterday's weather to compare.
Action: get_yesterday_weather
Action Input: {"city": "tokyo"}
Observation: Tokyo yesterday: high of 15°C, light rain
Thought: I have both data points. Today is 18°C and partly cloudy; yesterday peaked at 15°C with rain.
Final Answer: Tokyo is currently 18°C and partly cloudy — 3 degrees warmer than yesterday's high of 15°C, and the rain has cleared.
Observable outcome: The loop follows the ReAct (Reasoning + Acting) pattern, published by Yao et al.
Stage 4 — Visualizing the ReAct Loop: Thought → Action → Observation → Answer
- The flowchart below shows the complete execution path for a single agent invocation: Each loop back from Observation to Agent is one complete ReAct cycle.
- The LLM decides at every step whether to call another tool or produce a final answer.
- The sequence below zooms into the message-passing protocol between components during one tool call cycle: Two tool calls, three LLM calls, one final answer.
- Every message is appended to the conversation before the next LLM call — this is what "context grows with each step" means in practice.
flowchart TD
A([User Query]) --> B[Agent: LLM + Prompt]
B --> C{Tool call in response?}
C -- Yes --> D[AgentExecutor: Dispatch Tool]
D --> E[Tool Function Executes]
E --> F[Observation appended to history]
F --> G{Max iterations reached?}
G -- No --> B
G -- Yes --> H([Force-stop: return partial answer])
C -- No --> I([Final Answer returned to user])
sequenceDiagram
participant U as User
participant AE as AgentExecutor
participant LLM as LLM (gpt-4o)
participant T as Tool Function
U->>AE: invoke({"input": "Weather in Tokyo vs yesterday?"})
AE->>LLM: [SystemPrompt + UserMessage]
LLM-->>AE: AIMessage(tool_calls=[get_current_weather])
AE->>T: get_current_weather(city="tokyo")
T-->>AE: "Tokyo: 18C, partly cloudy"
AE->>LLM: ["History + ToolMessage(observation)"]
LLM-->>AE: AIMessage(tool_calls=[get_yesterday_weather])
AE->>T: get_yesterday_weather(city="tokyo")
T-->>AE: "Tokyo yesterday: high of 15C, light rain"
AE->>LLM: ["History + ToolMessage(observation)"]
LLM-->>AE: AIMessage(content="Tokyo is 3C warmer today...")
AE-->>U: {"output": "Tokyo is 3C warmer today..."}
Observable outcome: Every message is appended to the conversation before the next LLM call — this is what "context grows with each step" means in practice.
Stage 5 — Real-World Applications of Built-in LangChain Tools
- LangChain ships several ready-to-use tools that cover the most common "reach the internet" use cases.
- Knowing when to use each one saves you from writing boilerplate integrations.
- DuckDuckGo Search is the no-API-key option for web search.
- It returns a handful of snippets and URLs.
from langchain_community.tools import DuckDuckGoSearchRun
search = DuckDuckGoSearchRun()
print(search.run("LangChain AgentExecutor latest release"))
from langchain_community.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
wikipedia = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper(top_k_results=1, doc_content_chars_max=1000))
print(wikipedia.run("ReAct prompting technique"))
Observable outcome: It returns a handful of snippets and URLs.
Stage 6 — Worked Example: A Research Assistant with Web Search, Calculator, and Stock Price
- This example wires three tools into a single AgentExecutor — a DuckDuckGo search, a custom calculator, and a custom stock price lookup.
- The goal is a "Research Assistant" agent that can answer compound questions.
- Execution trace (abbreviated): The agent correctly sequences three distinct tool types — a mock API, a calculation, and a live web search — and synthesizes them into a coherent answer.
- No user code controlled the sequencing; the LLM decided the order based on its reasoning.
# NOTE: Install dependencies:
# pip install langchain langchain-openai langchain-community duckduckgo-search
from langchain_core.tools import tool
from langchain_community.tools import DuckDuckGoSearchRun
from langchain_openai import ChatOpenAI
from langchain import hub
from langchain.agents import AgentExecutor, create_react_agent
# --- Tool 1: Web search (built-in, no API key) ---
web_search = DuckDuckGoSearchRun(name="web_search")
# --- Tool 2: Calculator ---
@tool
def calculator(expression: str) -> str:
"""
Evaluate a mathematical expression and return the numeric result.
Input must be a valid Python arithmetic expression, e.g. "1500 * 1.07 ** 10".
Use this for any calculation that requires precise arithmetic.
"""
try:
result = eval(expression, {"__builtins__": {}}, {}) # restricted eval
return f"Result: {result}"
except Exception as exc:
return f"Error evaluating expression: {exc}"
# --- Tool 3: Stock price lookup ---
@tool
def get_stock_price(ticker: str) -> str:
"""
Return the current stock price for a given ticker symbol (e.g. AAPL, MSFT, NVDA).
Use this when the user asks about stock prices or market valuations.
NOTE: Returns mock data — replace with a real market data API in production.
"""
mock_prices = {
"AAPL": 213.07, "TSLA": 172.30, "NVDA": 118.42,
"MSFT": 415.20, "GOOGL": 175.50, "AMZN": 198.90,
}
price = mock_prices.get(ticker.upper())
if price is None:
return f"No price data found for ticker: {ticker}"
return f"{ticker.upper()} current price: ${price:.2f} USD"
# --- Assemble the agent ---
tools = [web_search, calculator, get_stock_price]
llm = ChatOpenAI(model="gpt-4o", temperature=0)
react_prompt = hub.pull("hwchase17/react")
agent = create_react_agent(llm=llm, tools=tools, prompt=react_prompt)
executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
max_iterations=8,
handle_parsing_errors=True,
)
# --- Run a compound research query ---
response = executor.invoke({
"input": (
"What is NVIDIA's current stock price? "
"If I invested $5,000 at that price, how many shares would I own? "
"Also search for the latest news about NVIDIA's AI chips."
)
})
print("\n=== FINAL ANSWER ===")
print(response["output"])
> Entering new AgentExecutor chain...
Thought: I need three pieces of information: NVIDIA stock price, share count calculation, and news.
Action: get_stock_price
Action Input: {"ticker": "NVDA"}
Observation: NVDA current price: $118.42 USD
Thought: Now I'll calculate shares.
Action: calculator
Action Input: {"expression": "5000 / 118.42"}
Observation: Result: 42.22...
Thought: Now I'll search for NVIDIA AI chip news.
Action: web_search
Action Input: NVIDIA AI chips latest news 2026
Observation: [DuckDuckGo snippets about Blackwell GPU architecture, data center demand...]
Thought: I have all three pieces of data. I can compose the final answer.
Final Answer: NVIDIA is currently trading at $118.42. With $5,000 you would own approximately
42.2 shares. Recent news highlights strong demand for Blackwell GPUs in AI data centers...
Observable outcome: No user code controlled the sequencing; the LLM decided the order based on its reasoning.
End-to-end outcome
After working through the stages, you should be able to explain:
- Write docstrings for the model, not for humans.
- Every word in a tool docstring is LLM prompt text.
- LLMs only predict tokens — they cannot fetch data, run calculations, or execute code.
- Tools are the mechanism that bridges language to action.
Alternatives considered
AgentExecutor Under the Hood: Runnable Sequences and the Tool-Calling vs. ReAct Distinction
AgentExecutor is built on LangChain's LCEL (LangChain Expression Language) runnable interface.
Decision Guide: Classic AgentExecutor or LangGraph?
The right tool depends on the complexity of the agent's decision-making surface.
Failure and degraded modes
AgentExecutor Trade-offs and Failure Modes
- Detection: AgentExecutor is a pragmatic tool with well-understood limitations.
- User or operator impact: AgentExecutor is a pragmatic tool with well-understood limitations.
