Course Content
Tool Use: Giving LLMs Capabilities
How to define and connect tools — web search, code execution, database access
What Is Tool Use?
A base language model is fundamentally limited: it can only work with information that was in its training data or that you put in the prompt. It cannot look up today’s stock price, run your code, query your database, or send an email. It lives entirely inside the prompt.
Tool use — also called function calling — breaks this limitation. Instead of just generating text, the model can output a structured request to call an external function. Your code executes that function, gets the result, and passes it back to the model. The model then continues with real, current, external data.
The model doesn’t actually execute anything. It says “please call this function with these parameters” in a structured format, and your application does the actual work. Think of it as the model filling out a work order that a program then carries out.
This simple mechanism unlocks enormous capability. With the right tools, a language model can operate as a financial analyst, a system administrator, a research assistant, or a software engineer.
The Call-Execute-Return Cycle
Here’s exactly what happens during a tool call:
1. You send: user message + list of available tools
2. Model responds: "I want to call search_stock(ticker='NVDA')"
3. Your code: calls the actual search_stock function
4. Your code: sends the result back to the model
5. Model responds: final answer using the real dataIn code, this looks like:
# Step 1: Send message + tools
response = client.messages.create(
model="claude-opus-4-5",
tools=tools, # list of tool definitions
messages=[{"role": "user", "content": "What's Nvidia's stock price?"}]
)
# Step 2: Model decides to call a tool
if response.stop_reason == "tool_use":
tool_call = response.content[0] # e.g., search_stock(ticker='NVDA')
# Step 3: Your code executes the function
result = search_stock(ticker=tool_call.input["ticker"])
# Step 4: Return result to model
response = client.messages.create(
model="claude-opus-4-5",
tools=tools,
messages=[
{"role": "user", "content": "What's Nvidia's stock price?"},
{"role": "assistant", "content": response.content},
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": tool_call.id, "content": str(result)}
]}
]
)
# Step 5: Model gives final answer
print(response.content[0].text)Defining a Tool Schema
A tool schema has three parts: name, description, and parameter schema. The description is the most important — it’s what the model reads to decide when and how to use the tool.
tool_schema = {
"name": "get_stock_price",
"description": """Look up the current stock price and basic metrics for a publicly traded company.
Use this when the user asks about stock prices, valuations, or market cap.
Returns current price, percent change today, market cap, and P/E ratio.""",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "Stock ticker symbol, e.g. 'NVDA' for Nvidia or 'AAPL' for Apple"
},
"include_history": {
"type": "boolean",
"description": "If true, also return 30-day price history. Default false."
}
},
"required": ["ticker"]
}
}A few principles for writing good tool descriptions:
- Be explicit about when to use it — “Use this when the user asks about stock prices” removes ambiguity
- Describe what it returns — the model needs to know what format to expect
- Clarify parameter formats — “e.g. ‘NVDA’ for Nvidia” prevents ticker hallucination
- Mark optional parameters — tell the model which parameters are optional and what they do
A Financial Analyst Agent
Let’s build a realistic financial analysis agent with three tools: stock price lookup, news search, and portfolio calculation.
import anthropic
import json
from typing import Any
client = anthropic.Anthropic()
# Tool definitions
TOOLS = [
{
"name": "get_stock_price",
"description": """Get current stock price, market metrics, and basic financials for a ticker.
Returns: price, day_change_pct, market_cap_billions, pe_ratio, 52w_high, 52w_low.""",
"input_schema": {
"type": "object",
"properties": {
"ticker": {"type": "string", "description": "Stock ticker symbol (e.g. 'NVDA')"}
},
"required": ["ticker"]
}
},
{
"name": "search_financial_news",
"description": """Search recent financial news about a company or market topic.
Use for earnings reports, analyst upgrades/downgrades, and major company events.
Returns: list of recent articles with title, date, and summary.""",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query, e.g. 'Nvidia Q3 earnings'"},
"days_back": {"type": "integer", "description": "How many days of news to search (default 7)"}
},
"required": ["query"]
}
},
{
"name": "calculate_portfolio_metrics",
"description": """Calculate portfolio-level metrics given a list of holdings.
Computes total value, weighted average P/E, sector allocation, and daily gain/loss.""",
"input_schema": {
"type": "object",
"properties": {
"holdings": {
"type": "array",
"description": "List of holdings",
"items": {
"type": "object",
"properties": {
"ticker": {"type": "string"},
"shares": {"type": "number"},
"cost_basis": {"type": "number"}
}
}
}
},
"required": ["holdings"]
}
}
]
# Tool implementations (simplified — use real APIs in production)
def get_stock_price(ticker: str) -> dict:
"""Mock stock price lookup. Replace with real API call."""
mock_data = {
"NVDA": {"price": 875.50, "day_change_pct": 2.3, "market_cap_billions": 2150, "pe_ratio": 65},
"AAPL": {"price": 189.30, "day_change_pct": -0.4, "market_cap_billions": 2920, "pe_ratio": 29},
"MSFT": {"price": 415.20, "day_change_pct": 0.8, "market_cap_billions": 3090, "pe_ratio": 35},
}
return mock_data.get(ticker.upper(), {"error": f"Ticker {ticker} not found"})
def search_financial_news(query: str, days_back: int = 7) -> list[dict]:
"""Mock news search. Replace with Serper/Tavily/Bloomberg API."""
return [
{
"title": f"Analysts Bullish on {query.split()[0]} After Strong Quarterly Results",
"date": "2026-06-04",
"summary": "Several Wall Street analysts raised price targets following better-than-expected earnings."
},
{
"title": f"{query.split()[0]} Announces New Product Line",
"date": "2026-06-02",
"summary": "Company revealed next-generation products at annual conference, driving shares higher."
}
]
def calculate_portfolio_metrics(holdings: list[dict]) -> dict:
"""Calculate portfolio metrics from holdings list."""
total_value = 0
total_cost = 0
for holding in holdings:
price_data = get_stock_price(holding["ticker"])
if "error" not in price_data:
current_value = price_data["price"] * holding["shares"]
total_value += current_value
total_cost += holding["cost_basis"] * holding["shares"]
return {
"total_value": round(total_value, 2),
"total_cost": round(total_cost, 2),
"total_gain_loss": round(total_value - total_cost, 2),
"total_return_pct": round((total_value - total_cost) / total_cost * 100, 2)
}
def execute_tool(tool_name: str, tool_input: dict) -> Any:
"""Route tool calls to implementations."""
if tool_name == "get_stock_price":
return get_stock_price(**tool_input)
elif tool_name == "search_financial_news":
return search_financial_news(**tool_input)
elif tool_name == "calculate_portfolio_metrics":
return calculate_portfolio_metrics(**tool_input)
return {"error": f"Unknown tool: {tool_name}"}
def run_financial_agent(user_query: str) -> str:
"""Run the financial analyst agent with tool use loop."""
messages = [{"role": "user", "content": user_query}]
system = """You are a financial analyst assistant. Use the available tools to
look up accurate, current information before making any analysis.
Always use get_stock_price before discussing valuations, and
search_financial_news for context about recent developments."""
for _ in range(10): # max iterations
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=2048,
system=system,
tools=TOOLS,
messages=messages
)
if response.stop_reason == "end_turn":
return next(b.text for b in response.content if hasattr(b, "text"))
if response.stop_reason == "tool_use":
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
print(f"Called {block.name}({block.input}) -> {result}")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result)
})
messages.append({"role": "user", "content": tool_results})
return "Analysis incomplete — max iterations reached."
# Example usage
result = run_financial_agent(
"Analyze Nvidia's current position. Include price, recent news, "
"and whether it seems overvalued or undervalued at current levels."
)
print(result)When the Model Decides to Use a Tool vs. Answer Directly
The model doesn’t always call a tool. It will answer directly when:
- The question is general knowledge that doesn’t require real-time data
- The question asks for explanation or reasoning, not current facts
- No tool in the schema is relevant to the question
It will call a tool when:
- The question requires data it can’t know (prices, current events)
- A tool’s description explicitly matches the question type
- The answer needs external computation (e.g., “calculate my portfolio return”)
This is an important behavior to understand: the model reads your tool descriptions and makes a judgment call. If your tool descriptions are vague or overlap with things the model already knows, it may skip them. Write descriptions that clearly indicate when to use each tool.
Handling Tool Failures
Real tools fail. The API times out, the ticker doesn’t exist, the database query returns no results. Good agent design handles this gracefully:
def execute_tool_safely(tool_name: str, tool_input: dict) -> str:
"""Execute a tool with error handling."""
try:
result = execute_tool(tool_name, tool_input)
if isinstance(result, dict) and "error" in result:
return f"Tool returned an error: {result['error']}. Try a different approach."
return json.dumps(result)
except TimeoutError:
return f"Tool {tool_name} timed out. The service may be unavailable."
except ValueError as e:
return f"Invalid parameters for {tool_name}: {str(e)}"
except Exception as e:
return f"Unexpected error calling {tool_name}: {str(e)}. Consider alternative tools."Critically: return the error as a tool result, not as a Python exception that crashes the loop. The model can then reason about the error and try a different approach. If the stock ticker wasn’t found, the model might search for the company name to find the correct ticker.
Tool Design Principles
After the description quality, there are four more things that make tools reliable:
1. Single responsibility — each tool does one thing. Don’t combine “search and summarize” into one tool; keep them separate so the model can use each independently.
2. Predictable output format — always return the same structure. A tool that sometimes returns a dict and sometimes a string creates confusion for the model.
3. Idempotent when possible — read operations should return the same result if called multiple times. Write operations should be clearly labeled as having side effects.
4. Include error states in the schema description — “Returns null if the ticker is not found” helps the model know what to check for.
Summary
- Tool use lets the model output structured function call requests that your code executes
- The schema has three parts: name, description, and parameter schema — descriptions are the most important
- The call-execute-return cycle runs in a loop until the model gives a final answer
- The financial analyst agent shows three complementary tools working together
- The model decides when to use tools based on your descriptions — write them carefully
- Always handle tool failures gracefully and return errors as tool results, not exceptions
Next: Memory Systems — how agents remember context, past interactions, and long-term preferences.
