Skip to main content

Using Tools and Agents in LangChain: Autonomous LLM Agents

Agents are LLM-powered systems that decide which tools to use and iterate until they solve a problem. Unlike chains, which execute a fixed sequence, agents reason about which action to take next based on the problem. Tools are functions the agent can invoke—APIs, search, calculators, databases. Together, they enable autonomous reasoning loops where the LLM is the decision-maker.

I was manually routing queries to different functions until I built an LangChain agent. The agent learned to route itself—ask it anything and it decides whether to search the web, run code, or query a database. Zero hardcoded rules; it just reasons its way through.

Defining Tools

Tools are functions wrapped with metadata so the agent understands their purpose. Define them with the @tool decorator:

from langchain_core.tools import tool
import math

@tool
def calculate_area(radius: float) -> float:
"""Calculate the area of a circle given the radius."""
return math.pi * radius ** 2

@tool
def get_weather(location: str) -> str:
"""Get the current weather for a location."""
# In production, call a real weather API
return f"Sunny, 72°F in {location}"

# Tools are now callable by the agent
tools = [calculate_area, get_weather]

The docstring is crucial—the agent reads it to understand what the tool does. Docstrings should be concise and describe parameters and return values.

For more control, use the Tool class directly:

from langchain_core.tools import Tool

def multiply(a: float, b: float) -> float:
return a * b

multiply_tool = Tool(
name="multiply",
func=multiply,
description="Multiply two numbers together. Returns the product."
)

Creating an Agent with LangChain

Use create_react_agent() to build a ReAct (Reasoning and Acting) agent—the most common pattern:

from langchain_openai import ChatOpenAI
from langchain.agents import create_react_agent, AgentExecutor
from langchain import hub

# Create the model and tools
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
tools = [calculate_area, get_weather]

# Load a prompt template designed for ReAct agents
prompt = hub.pull("hwchase17/react")

# Create the agent
agent = create_react_agent(model, tools, prompt)

# Wrap it in an executor
agent_executor = AgentExecutor.from_agent_and_tools(
agent=agent,
tools=tools,
verbose=True, # Print reasoning steps
max_iterations=10 # Prevent infinite loops
)

# Run the agent
result = agent_executor.invoke({
"input": "What's the area of a circle with radius 5?"
})

print(result["output"])

The agent reasons through the problem:

  1. Thought: "I need to calculate the area of a circle. I have a calculate_area tool."
  2. Action: Calls calculate_area(radius=5)
  3. Observation: Receives 78.54
  4. Thought: "The area is about 78.54 square units."
  5. Final Answer: Returns the result

Multi-Step Agent Reasoning

Agents shine when problems require multiple steps:

@tool
def search_web(query: str) -> str:
"""Search the web for information."""
return f"Search results for '{query}': ... [mock result]"

@tool
def summarize_text(text: str) -> str:
"""Summarize a block of text."""
return f"Summary of {len(text)} chars: [mock summary]"

tools = [search_web, summarize_text]

agent_executor = AgentExecutor.from_agent_and_tools(
agent=agent,
tools=tools,
verbose=True
)

# Agent will search, then summarize without explicit orchestration
result = agent_executor.invoke({
"input": "Find information about Python async/await and summarize it."
})

The agent iterates:

  1. Search the web for "Python async/await"
  2. Receive search results
  3. Summarize the results
  4. Return the summary

No hardcoded orchestration—pure reasoning.

Handling Tool Errors and Retries

Agents can fail if tools error or they misuse them. Handle gracefully:

from langchain.agents import Tool
from langchain_core.tools import ToolException

@tool
def divide(a: float, b: float) -> float:
"""Divide two numbers. Raises an error if b is 0."""
if b == 0:
raise ToolException("Division by zero is undefined.")
return a / b

# Pass handle_tool_error=True to let the agent retry on errors
tool = Tool(
name="divide",
func=divide,
description="Divide a by b.",
handle_tool_error=True
)

When the agent triggers an error, it sees the error message and tries a different approach.

Tool Input Validation

Validate inputs before the agent calls a tool:

from pydantic import BaseModel, Field

class CalculateAreaInput(BaseModel):
radius: float = Field(gt=0, description="The radius, must be positive")

@tool(args_schema=CalculateAreaInput)
def calculate_area(radius: float) -> float:
"""Calculate circle area."""
return math.pi * radius ** 2

The agent sees the schema and knows not to pass negative radii.

Streaming Agent Output

For long-running agents, stream reasoning steps:

# Use stream() instead of invoke()
for event in agent_executor.stream({"input": "..."}):
print(event)
# Prints each Thought, Action, Observation as it happens

Great for user-facing applications where showing work-in-progress builds confidence.

Comparing Agent Types

TypeReasoningFlexibilityComplexity
ReActThought → Action → ObservationVery highMedium
OpenAI FunctionsDirect tool selectionModerateLow
Plan-and-ExecuteCreate plan, then executeHighHigh
Conversational AgentMulti-turn with memoryModerateMedium

Agent Debugging

When agents misbehave, enable full debugging:

import langchain
langchain.debug = True

agent_executor.invoke({"input": "..."})
# Prints LLM calls, tool invocations, and reasoning

Look for:

  • Wrong tool choice: Agent is misunderstanding the task
  • Tool errors: Tools are throwing exceptions
  • Max iterations hit: Agent is stuck in a loop

Key Takeaways

  • Tools are functions wrapped with metadata so agents understand their purpose
  • ReAct agents reason (Thought), act (call tool), and observe (receive result) iteratively
  • Agents decide which tool to call; no hardcoded routing
  • Multi-step problems are solved without explicit orchestration
  • Use verbose=True and langchain.debug = True to debug agent reasoning
  • Stream agent outputs for user-facing applications
  • Validate tool inputs with Pydantic schemas to prevent misuse

Frequently Asked Questions

How does the agent decide which tool to use?

The LLM reads the tool descriptions and reasons which is most relevant. It's not magic—it depends on clear descriptions and the model's capability.

Can agents use tools that aren't predefined?

No, but you can define a generic "run_python_code" tool and let the agent write Python to solve problems dynamically. This is powerful but requires careful sandboxing.

What if the agent gets stuck in a loop?

Set max_iterations to a reasonable number (default 10). If the agent hits the limit, it returns whatever progress it made.

Do agents work with local models?

Yes, with ChatOllama or similar. Local models reason less effectively than GPT-4, but they still work for simple tool-calling tasks.

Can I make the agent remember previous interactions?

Yes, add memory to the agent: AgentExecutor(..., memory=ConversationBufferMemory()). The agent will reference past conversations.

Further Reading