Chapter 06 · Section II · 26 min read
Connecting tools and data
Function calling and tool use — how you let an LLM do things, not just say things. A worked example that lets an assistant check exchange rates, look up NEPSE prices, and query a real database.
RAG lets an LLM answer from documents you’ve indexed. Tool use lets an LLM take actions — call an API, query a database, run a calculation — and use the results in its answer. It’s the difference between “answer from what I read yesterday” and “answer from what’s true right now.” This section teaches the pattern with a worked Nepali example: an assistant that can check the NPR-USD exchange rate, look up a NEPSE price, and query a small database, all in the middle of a chat.
The mental model
Tool use works like this:
- You declare a list of tools the model can call, each with a schema (name, description, input parameters).
- You send a user message.
- The model decides whether to call a tool. If yes, it emits a structured tool call (which tool, what arguments).
- Your code runs the tool and returns the result.
- The model reads the result and either calls another tool or answers the user.
This is what “function calling” means. The LLM doesn’t run functions; it asks your code to run functions and then reads the results.
The Anthropic tool-use API
Claude’s tool-use API takes a tools argument on messages.create:
tools = [
{
"name": "get_exchange_rate",
"description": "Get the current exchange rate from NPR to another currency.",
"input_schema": {
"type": "object",
"properties": {
"target_currency": {
"type": "string",
"description": "3-letter ISO code, e.g. 'USD', 'INR', 'EUR'",
},
},
"required": ["target_currency"],
},
},
]
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=800,
tools=tools,
messages=[
{"role": "user", "content": "How many US dollars is 100 NPR right now?"},
],
)
If the model decides to call the tool, response.stop_reason will be "tool_use" and one of the content blocks will describe the call:
for block in response.content:
if block.type == "tool_use":
print(f"Tool: {block.name}")
print(f"Args: {block.input}")
# → Tool: get_exchange_rate
# → Args: {'target_currency': 'USD'}
Now your code runs the tool and sends the result back as another message:
tool_result = fetch_exchange_rate_from_real_api("USD") # your implementation
# Send the result back to the model
followup = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=800,
tools=tools,
messages=[
{"role": "user", "content": "How many US dollars is 100 NPR right now?"},
{"role": "assistant", "content": response.content}, # the tool call
{"role": "user", "content": [
{
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(tool_result),
}
]},
],
)
print(followup.content[0].text)
# → "100 NPR is approximately 0.75 USD at today's exchange rate."
The shape is: model calls tool → your code runs tool → you send result → model reads and responds. For multi-step reasoning, the model may call several tools in sequence.
A worked assistant with three tools
Let’s build nepse-mitra — a small Nepali financial assistant with three tools:
get_exchange_rate(target_currency)— current NPR-to-X rate.get_nepse_price(symbol)— current NEPSE stock price.get_holdings(user_id)— user’s own portfolio from a small local database.
import json
import requests
import anthropic
client = anthropic.Anthropic()
# -- Tool implementations -----------------------------------
def get_exchange_rate(target_currency: str) -> dict:
try:
r = requests.get(
f"https://open.er-api.com/v6/latest/NPR", timeout=5,
)
r.raise_for_status()
rates = r.json()["rates"]
code = target_currency.upper()
if code not in rates:
return {"error": f"Unknown currency: {code}"}
return {"npr_to_target": rates[code], "target": code}
except Exception as e:
return {"error": str(e)}
def get_nepse_price(symbol: str) -> dict:
# Illustrative stub — a real implementation would hit a public NEPSE feed
fake_prices = {
"NABIL": 1245.5,
"NICA": 850.0,
"NHPC": 380.2,
}
price = fake_prices.get(symbol.upper())
if price is None:
return {"error": f"No price for symbol {symbol}"}
return {"symbol": symbol.upper(), "price_npr": price}
HOLDINGS_DB = {
"user_001": [
{"symbol": "NABIL", "shares": 20},
{"symbol": "NHPC", "shares": 100},
],
"user_002": [
{"symbol": "NICA", "shares": 50},
],
}
def get_holdings(user_id: str) -> dict:
holdings = HOLDINGS_DB.get(user_id)
if not holdings:
return {"error": f"No holdings for user {user_id}"}
return {"user_id": user_id, "holdings": holdings}
# Map name → callable
TOOL_IMPLEMENTATIONS = {
"get_exchange_rate": get_exchange_rate,
"get_nepse_price": get_nepse_price,
"get_holdings": get_holdings,
}
Declaring the tools to the model
TOOLS = [
{
"name": "get_exchange_rate",
"description": "Get today's exchange rate from NPR to another currency.",
"input_schema": {
"type": "object",
"properties": {
"target_currency": {
"type": "string",
"description": "3-letter ISO currency code (USD, INR, EUR, ...).",
},
},
"required": ["target_currency"],
},
},
{
"name": "get_nepse_price",
"description": "Get the current price of a NEPSE-listed stock by symbol.",
"input_schema": {
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "NEPSE ticker symbol (NABIL, NICA, NHPC, ...).",
},
},
"required": ["symbol"],
},
},
{
"name": "get_holdings",
"description": "Get the current user's portfolio (shares held per NEPSE symbol).",
"input_schema": {
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "The user identifier.",
},
},
"required": ["user_id"],
},
},
]
Every tool has a name, a plain-English description (the model uses this to decide whether to call it), and a JSON schema for the arguments (the model produces arguments conforming to this).
The conversation loop
Tool use turns a single call into a small state machine:
def chat_once(user_message: str, user_id: str = "user_001") -> str:
messages = [{"role": "user", "content": user_message}]
system = f"""You are nepse-mitra, a Nepali financial assistant.
The current user_id is {user_id}. Use it when calling get_holdings.
Respond in the same language the user writes in.
When you have the information you need, provide a helpful, concise answer.
"""
while True:
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=800,
temperature=0.0,
system=system,
tools=TOOLS,
messages=messages,
)
if response.stop_reason == "end_turn":
# Model has finished
return response.content[0].text
if response.stop_reason == "tool_use":
# Model wants to call a tool
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for block in response.content:
if block.type == "tool_use":
fn = TOOL_IMPLEMENTATIONS[block.name]
result = fn(**block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
})
messages.append({"role": "user", "content": tool_results})
continue
# Other stop reasons (max_tokens, etc.) — return what we have
return response.content[0].text
The loop: call the model → if it wants to call tools, execute them and append results → call the model again → repeat until it finishes with an answer. For most questions, this loops once or twice.
Trying it
print(chat_once("What's the current NPR-USD rate?"))
# → Tool call: get_exchange_rate({"target_currency": "USD"}) → 0.0075
# → "1 NPR is approximately 0.0075 USD today."
print(chat_once("How much is my Nabil position worth?", user_id="user_001"))
# → Tool call: get_holdings({"user_id": "user_001"})
# → [{'symbol': 'NABIL', 'shares': 20}, ...]
# → Tool call: get_nepse_price({"symbol": "NABIL"})
# → 1245.5
# → "Your 20 shares of Nabil Bank are worth NPR 24,910 at today's price of NPR 1,245.50."
print(chat_once("के मेरो पोर्टफोलियोको कुल मूल्य कति छ?"))
# → Tool calls: get_holdings + get_nepse_price for each symbol
# → "तपाईंको पोर्टफोलियोको कुल मूल्य लगभग NPR ६२,९३० छ..."
Three examples. Three different tool-use patterns:
- Single tool call.
- Two tool calls (needed both holdings and price).
- Multiple sequential calls (one per stock in portfolio).
The model decides how many calls to make based on the question. This is the emergent power of tool use.
The four safety habits
Tool use is powerful, which means it needs care. Four habits worth internalising:
1. Never let the LLM run arbitrary code. Do not expose tools like run_shell_command(cmd) or execute_sql(query) to a public-facing LLM. Every tool should have a narrow, safe API.
2. Validate every tool argument. The model may generate a plausible-looking but invalid argument (target_currency="US Dollar" instead of "USD"). Validate at the tool boundary and return a helpful error the model can respond to.
3. Rate-limit expensive tools. If a tool calls an external API, limit how often it can fire per session — a runaway loop can burn through API budgets quickly.
4. Log every tool call. In production, you want to know exactly which tools were called with which arguments and what they returned. This is essential for debugging and often for compliance.
Tool use vs. RAG
Two related patterns, different purposes:
| RAG | Tool use | |
|---|---|---|
| Data source | Static documents you indexed | Live systems (APIs, DBs) |
| Best for | Q&A over policies, docs, articles | Actions and real-time queries |
| Latency | Fast (index lookup + one LLM call) | Slower (each tool call adds latency) |
| Cost per query | Predictable | Depends on how many tool calls |
| Safety | Read-only, low risk | Depends on what tools do |
Most real production LLM applications use both. A customer support bot might use RAG to answer policy questions and use tools to look up the user’s actual account status. A financial assistant might use RAG for market education and tools for real-time prices.
Check your understanding
Quick check
—A developer wants to give their internal AI the ability to run arbitrary SQL queries against the production database, arguing 'the LLM knows SQL.' What's the professional response?
Quick check
—A user's question triggers a tool call that returns an error ('symbol not found'). What should the assistant do, if the system is well-designed?
What comes next
You can build assistants that talk and act. The final section of Chapter 6 covers the pre-launch discipline that turns an impressive demo into a safe product: testing for safety, bias, and Nepali-specific correctness. This is where the real professional work happens.