How agents actually call functions — what happens under the hood
Two Terms — One Concept
You will hear both "tool calling" and "function calling" — they mean the same thing with a slight difference:
Function Calling = OpenAI's original name for this feature (2023)
The LLM outputs a JSON with function name + arguments
You run the function, send result back to LLM
Tool Calling = LangChain's abstraction on top of function calling
Works with OpenAI, Anthropic, Gemini, all providers
Same code — any LLM provider underneath
In 2025: "tool calling" is the standard term
"function calling" = same thing, older name
What Actually Happens When a Tool is Called
Most developers think the LLM "runs" the tool. It does not.
Here is exactly what happens step by step:
Step 1 — You define a tool
A JavaScript function + name + description + schema
Step 2 — Tool schema sent to LLM
LangChain converts your tool definition into JSON schema
Sends it to OpenAI alongside the user's message
LLM now "knows" what tools are available
Step 3 — LLM decides to use a tool
LLM does NOT call the function
LLM outputs a special JSON saying:
"I want to call this tool with these arguments"
{
"tool": "calculator",
"args": { "operation": "multiply", "a": 1847, "b": 23 }
}
Step 4 — LangChain intercepts this
LangChain reads the LLM's JSON output
LangChain actually runs your JavaScript function
With the arguments the LLM provided
Step 5 — Result sent back to LLM
Tool result is added to conversation as a ToolMessage
LLM reads the result and continues reasoning
Step 6 — LLM generates final answer
Now with real data from the tool result
The LLM never touches your function code. It only outputs JSON describing what it wants to call. LangChain does the actual execution.
Project Setup
mkdir agent-tools-demo cd agent-tools-demo npm init -y
Update package.json:
{ "name": "agent-tools-demo", "version": "1.0.0", "main": "index.js", "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "description": "", "dependencies": { "@langchain/langgraph": "^1.4.8", "@langchain/openai": "^1.5.5", "dotenv": "^17.4.2", "langchain": "^1.5.4", "zod": "^4.4.3" } }
npm install langchain @langchain/openai @langchain/langgraph zod dotenv
Create .env:
OPENAI_API_KEY=sk-proj-your-key-here
Part 1 — Anatomy of a Tool
Create src/01_tool_anatomy.js:
import { tool } from "langchain"; import { z } from "zod"; import { ChatOpenAI } from "@langchain/openai"; import * as dotenv from "dotenv"; dotenv.config();
// ───────────────────────────────────────── // A TOOL HAS THREE PARTS: // 1. The function — actual code that runs // 2. The name — how LLM refers to this tool // 3. The schema — what inputs the LLM must provide // ─────────────────────────────────────────
const calculatorTool = tool(
// PART 1 — The actual function async ({ operation, a, b }) => { // This is plain JavaScript — you can do anything here // Call an API, query a database, run a calculation // The LLM never sees this code — only the result
switch (operation) { case "add": return `${a} + ${b} = ${a + b}`; case "subtract": return `${a} - ${b} = ${a - b}`; case "multiply": return `${a} × ${b} = ${a * b}`; case "divide": if (b === 0) return "Cannot divide by zero"; return `${a} ÷ ${b} = ${(a / b).toFixed(4)}`; default: return `Unknown operation: ${operation}`; } // Return value = string that goes back to the LLM as tool result // LLM reads this and uses it to compose the final answer },
{ // PART 2 — Name name: "calculator", // LLM uses this name when it wants to call this tool // Must be unique — no spaces — use underscores // Bad name: "math tool" (has space) // Good name: "calculator" (clear, no space) // Good name: "math_calculator" (also fine)
// PART 3 — Description description: `Performs basic math: add, subtract, multiply, divide. Use this for ANY calculation. Never calculate in your head. Always use this tool when numbers need to be computed.`, // This is the MOST IMPORTANT part of a tool // The LLM reads this description to decide: // → Should I use this tool right now? // → What is this tool good for? // // Good description = LLM uses tool at the right time // Bad description = LLM ignores tool or uses it wrongly // // Rules for good descriptions: // 1. Say WHAT it does ("performs math") // 2. Say WHEN to use it ("for ANY calculation") // 3. Say ALWAYS/NEVER if needed ("Never calculate in your head")
schema: z.object({ // schema defines the exact inputs the LLM must provide // LangChain converts this to JSON schema for OpenAI
operation: z.enum(["add", "subtract", "multiply", "divide"]) .describe("which math operation to perform"), // z.enum = only these exact values are allowed // LLM cannot pass "addition" — must be "add" // .describe() = tells LLM what this field means
a: z.number().describe("first number"), // z.number() = must be a number (not a string) // LLM cannot pass "five" — must be 5
b: z.number().describe("second number"), }), } );
// ───────────────────────────────────────── // INSPECT THE TOOL — see what gets sent to OpenAI // ─────────────────────────────────────────
console.log("Tool name:", calculatorTool.name); // Output: "calculator"
console.log("\nTool description:", calculatorTool.description); // Output: "Performs basic math: add, subtract, multiply, divide..."
console.log("\nTool schema (what OpenAI receives):"); console.log(JSON.stringify(calculatorTool.schema, null, 2)); // Output: the JSON schema that OpenAI uses to validate LLM's tool calls // { // "type": "object", // "properties": { // "operation": { "type": "string", "enum": ["add", "subtract", "multiply", "divide"] }, // "a": { "type": "number" }, // "b": { "type": "number" } // }, // "required": ["operation", "a", "b"] // }
// ───────────────────────────────────────── // BIND TOOL TO LLM — make LLM aware of the tool // ─────────────────────────────────────────
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 });
const llmWithTools = llm.bindTools([calculatorTool]); // .bindTools() = attach tool definitions to this LLM instance // Every call to llmWithTools will include the tool schema // LLM can now decide to call this tool in its responses
console.log("\n✅ Tool bound to LLM — LLM now knows about calculator tool");
// ───────────────────────────────────────── // SEE WHAT LLM OUTPUTS WHEN IT WANTS A TOOL // This shows the raw tool call before execution // ─────────────────────────────────────────
async function showRawToolCall() { console.log("\n" + "─".repeat(55)); console.log("RAW TOOL CALL — what LLM outputs before execution"); console.log("─".repeat(55));
const response = await llmWithTools.invoke( "What is 1847 multiplied by 23?" ); // LLM receives the question + tool schema // LLM decides: "I need the calculator" // LLM outputs a special message — NOT a text answer
console.log("\nResponse type:", response.constructor.name); // AIMessage — but with special tool_calls field
console.log("\nResponse content:", response.content); // "" (empty string) — LLM didn't write text // Instead it decided to call a tool
console.log("\nTool calls (raw LLM output):"); console.log(JSON.stringify(response.tool_calls, null, 2)); // This is what the LLM actually output: // [ // { // "name": "calculator", // "args": { // "operation": "multiply", // "a": 1847, // "b": 23 // }, // "id": "call_abc123" // } // ] // // LLM said: "I want to call calculator with these args" // It did NOT run the function — LangChain will do that }
showRawToolCall().catch(console.error);
Run:
node src/01_tool_anatomy.js
Part 2 — Manual Tool Execution Loop
This shows exactly what happens inside an agent — without any magic:
Create src/02_manual_tool_loop.js:
import { tool } from "langchain"; import { z } from "zod"; import { ChatOpenAI } from "@langchain/openai"; import { HumanMessage, ToolMessage } from "@langchain/core/messages"; import * as dotenv from "dotenv"; dotenv.config();
const llm = new ChatOpenAI({ model: "gpt-4o", temperature: 0 });
// ───────────────────────────────────────── // DEFINE TOOLS // ─────────────────────────────────────────
const calculatorTool = tool( async ({ operation, a, b }) => { if (operation === "add") return String(a + b); if (operation === "subtract") return String(a - b); if (operation === "multiply") return String(a * b); if (operation === "divide") return b === 0 ? "error: divide by zero" : String(a / b); return "unknown operation"; }, { name: "calculator", description: "Performs math. Use for any calculation.", schema: z.object({ operation: z.enum(["add", "subtract", "multiply", "divide"]), a: z.number(), b: z.number(), }), } );
const weatherTool = tool( async ({ city }) => { // Simulated weather data const data = { "Delhi": "32°C, sunny, humidity 45%", "Mumbai": "28°C, cloudy, humidity 85%", "Bangalore": "22°C, pleasant, humidity 60%", }; return data[city] || `No weather data for ${city}`; }, { name: "get_weather", description: "Gets weather for a city. Use when asked about weather.", schema: z.object({ city: z.string().describe("city name"), }), } );
const tools = [calculatorTool, weatherTool]; // array of all available tools
const toolMap = { calculator: calculatorTool, get_weather: weatherTool, }; // toolMap = lookup dictionary to find tool by name // used when LLM says "call calculator" → find the calculator function
// ───────────────────────────────────────── // BIND TOOLS TO LLM // ─────────────────────────────────────────
const llmWithTools = llm.bindTools(tools);
// ───────────────────────────────────────── // MANUAL REACT LOOP // This is exactly what createAgent() does internally // We write it manually to understand every step // ─────────────────────────────────────────
async function manualReActLoop(userQuestion) { console.log("\n" + "=".repeat(55)); console.log("Question:", userQuestion); console.log("=".repeat(55));
const messages = [new HumanMessage(userQuestion)]; // messages = conversation history // starts with just the user's question // grows as we add tool calls and results
let iteration = 0; const MAX_ITERATIONS = 10; // safety limit — prevents infinite loops // if agent hasn't answered after 10 steps → stop
while (iteration < MAX_ITERATIONS) { iteration++; console.log(`\n--- Iteration ${iteration} ---`);
// ── STEP 1: LLM THINKS ────────────────────────────────── const response = await llmWithTools.invoke(messages); // send full conversation history to LLM // LLM reads everything and decides what to do next // // response is always an AIMessage // Two possible responses: // A) response.tool_calls has items → LLM wants to call a tool // B) response.tool_calls is empty → LLM is giving final answer
messages.push(response); // add LLM's response to conversation history // important: next iteration includes this response
console.log("LLM response type:", response.tool_calls?.length > 0 ? "TOOL CALL" : "FINAL ANSWER" );
// ── STEP 2: CHECK IF DONE ─────────────────────────────── if (!response.tool_calls || response.tool_calls.length === 0) { // No tool calls = LLM is giving final text answer // This is the exit condition for the loop
console.log("\n✅ FINAL ANSWER:"); console.log(response.content); // response.content = the actual answer text // example: "1847 multiplied by 23 equals 42,481" return response.content; }
// ── STEP 3: EXECUTE TOOL CALLS ────────────────────────── console.log(`LLM wants to call ${response.tool_calls.length} tool(s):`);
for (const toolCall of response.tool_calls) { // toolCall = { name: "calculator", args: { operation: "multiply", a: 1847, b: 23 }, id: "call_abc" } // loop through each tool call (LLM can request multiple at once)
console.log(` → Calling: ${toolCall.name}(${JSON.stringify(toolCall.args)})`); // example: "→ Calling: calculator({"operation":"multiply","a":1847,"b":23})"
const toolFunction = toolMap[toolCall.name]; // find the actual JavaScript function for this tool name // example: toolMap["calculator"] = calculatorTool function
if (!toolFunction) { console.log(` → Tool "${toolCall.name}" not found!`); continue; }
const toolResult = await toolFunction.invoke(toolCall.args); // actually run the JavaScript function with the LLM's arguments // example: calculatorTool.invoke({ operation: "multiply", a: 1847, b: 23 }) // example result: "42481"
console.log(` → Result: ${toolResult}`);
messages.push(new ToolMessage({ content: String(toolResult), // the tool's return value as a string // example: "42481"
tool_call_id: toolCall.id, // links this result to the specific tool call that requested it // LLM uses this ID to match results with requests // example: "call_abc123" })); // add tool result to conversation history // next iteration: LLM reads this result and continues reasoning }
// Loop back to Step 1 — LLM thinks again with tool results }
return "Max iterations reached — agent stopped"; }
// ───────────────────────────────────────── // RUN EXAMPLES // ─────────────────────────────────────────
async function main() { console.log("🔄 MANUAL REACT LOOP DEMO\n");
// Example 1 — single tool call await manualReActLoop("What is 1847 multiplied by 23?");
// Example 2 — needs to decide NO tool is needed await manualReActLoop("What is the capital of France?"); // LLM knows this → no tool needed → answers directly
// Example 3 — multi-step: two tool calls await manualReActLoop( "What is the weather in Delhi? Also calculate 15% of 8500." ); // LLM calls get_weather AND calculator // gets both results, then composes one answer }
main().catch(console.error);
Run:
node src/02_manual_tool_loop.js
Expected output:
🔄 MANUAL REACT LOOP DEMO
=======================================================
Question: What is 1847 multiplied by 23?
=======================================================
--- Iteration 1 ---
LLM response type: TOOL CALL
LLM wants to call 1 tool(s):
→ Calling: calculator({"operation":"multiply","a":1847,"b":23})
→ Result: 42481
--- Iteration 2 ---
LLM response type: FINAL ANSWER
✅ FINAL ANSWER:
1847 multiplied by 23 equals 42,481.
=======================================================
Question: What is the capital of France?
=======================================================
--- Iteration 1 ---
LLM response type: FINAL ANSWER
✅ FINAL ANSWER:
The capital of France is Paris.
Part 3 — Tool Design Patterns
Good tool design is a skill. These patterns matter in production:
Create src/03_tool_patterns.js:
import { tool } from "langchain"; import { z } from "zod"; import { createAgent } from "langchain"; import { ChatOpenAI } from "@langchain/openai"; import * as dotenv from "dotenv"; dotenv.config();
// ───────────────────────────────────────── // PATTERN 1 — Return structured data as string // Always return strings — LLM reads text, not objects // ─────────────────────────────────────────
const stockPriceTool = tool( async ({ ticker }) => { // Simulated stock data const prices = { AAPL: 189.50, GOOGL: 142.30, MSFT: 415.80, TSLA: 248.20, };
const price = prices[ticker.toUpperCase()]; if (!price) return `No data found for ticker: ${ticker}`;
return `${ticker.toUpperCase()}: $${price} (as of today)`; // Return a descriptive string — NOT an object like { price: 189.50 } // LLM reads text — structured strings are easier to reason about }, { name: "get_stock_price", description: `Gets the current stock price for a ticker symbol. Use when asked about stock prices, market values, or share prices. Ticker examples: AAPL (Apple), GOOGL (Google), MSFT (Microsoft), TSLA (Tesla)`, schema: z.object({ ticker: z.string().describe("stock ticker symbol, e.g. AAPL, GOOGL"), }), } );
// ───────────────────────────────────────── // PATTERN 2 — Tool with optional parameters // Some inputs are not always required // ─────────────────────────────────────────
const searchTool = tool( async ({ query, maxResults = 5, category }) => { // maxResults has a default value of 5 // category is truly optional — might be undefined
console.log(`Searching: "${query}" | max: ${maxResults} | category: ${category || "all"}`);
// Simulated search results return `Found ${maxResults} results for "${query}"${category ? ` in category "${category}"` : ""}. Top result: This is a simulated result about ${query}.`; }, { name: "search", description: "Searches for information on any topic. Use when you need to find current information.", schema: z.object({ query: z.string() .describe("the search query"),
maxResults: z.number().optional().default(5) .describe("maximum number of results to return, default is 5"), // .optional() = LLM doesn't have to provide this // .default(5) = use 5 if LLM doesn't provide it
category: z.string().optional() .describe("optional category to filter results: 'news', 'academic', 'shopping'"), // LLM can leave this out entirely }), } );
// ───────────────────────────────────────── // PATTERN 3 — Tool that can fail gracefully // Always handle errors inside the tool // Never let a tool throw — it crashes the agent // ─────────────────────────────────────────
const databaseTool = tool( async ({ userId }) => { // Simulated database lookup const users = { "U001": { name: "Sofia", plan: "Pro", joinDate: "2024-01-15" }, "U002": { name: "Arjun", plan: "Free", joinDate: "2024-03-20" }, };
try { const user = users[userId];
if (!user) { // User not found — return informative message, don't throw return `No user found with ID: ${userId}. Please check the ID and try again.`; // LLM receives this and can tell the user "no such user found" }
return `User ${userId}: Name=${user.name}, Plan=${user.plan}, Joined=${user.joinDate}`; // LLM receives this and includes it in the answer
} catch (error) { // Something unexpected went wrong — return error message return `Database error: ${error.message}. Please try again.`; // Never throw from inside a tool — always return a string // Throwing crashes the agent loop } }, { name: "get_user_info", description: "Gets user account information from the database. Use when asked about a user's account, plan, or profile.", schema: z.object({ userId: z.string().describe("the user ID to look up, e.g. U001"), }), } );
// ───────────────────────────────────────── // PATTERN 4 — Tool with side effects // Some tools DO things (not just look up data) // ─────────────────────────────────────────
const emailTool = tool( async ({ to, subject, body }) => { // In production: use nodemailer or SendGrid API // Here we simulate it
console.log(`\n📧 SENDING EMAIL:`); console.log(` To: ${to}`); console.log(` Subject: ${subject}`); console.log(` Body: ${body.substring(0, 50)}...`);
// Simulate API call delay await new Promise(resolve => setTimeout(resolve, 100));
return `Email sent successfully to ${to} with subject "${subject}"`; // Return confirmation so LLM knows the action succeeded }, { name: "send_email", description: `Sends an email to a recipient. Use when the user explicitly asks to send an email. Do NOT use this unless the user specifically requests an email be sent.`, // "Do NOT use unless explicitly requested" = important guardrail // prevents agent from sending emails accidentally schema: z.object({ to: z.string().describe("recipient email address"), subject: z.string().describe("email subject line"), body: z.string().describe("email body content"), }), } );
// ───────────────────────────────────────── // USE ALL TOOLS WITH AN AGENT // ─────────────────────────────────────────
const agent = createAgent({ model: new ChatOpenAI({ model: "gpt-4o", temperature: 0 }), tools: [stockPriceTool, searchTool, databaseTool, emailTool], systemPrompt: `You are a helpful assistant with access to tools. Use tools when needed. Be concise in your answers. Never send emails unless the user explicitly asks you to.`, });
async function ask(question) { console.log("\n" + "─".repeat(55)); console.log("Q:", question); console.log("─".repeat(55));
const result = await agent.invoke({ messages: [{ role: "user", content: question }], });
const lastMsg = result.messages[result.messages.length - 1]; console.log("A:", lastMsg.content); }
async function main() { console.log("🔧 TOOL PATTERNS DEMO\n");
await ask("What is the current price of Apple stock?"); // uses get_stock_price tool
await ask("Search for information about AI agents, show me top 3 results"); // uses search tool with maxResults=3
await ask("Get me information about user U001"); // uses get_user_info tool
await ask("Get me information about user U999"); // uses get_user_info — user not found — graceful error
await ask("What is 2+2?"); // no tool needed — LLM answers directly }
main().catch(console.error);
Run:
node src/03_tool_patterns.js
Part 4 — Parallel Tool Calling
LLMs can call multiple tools at the same time in one step:
Create src/04_parallel_tools.js:
import { tool } from "langchain"; import { z } from "zod"; import { createAgent } from "langchain"; import { ChatOpenAI } from "@langchain/openai"; import * as dotenv from "dotenv"; dotenv.config();
// Three independent tools const weather = tool( async ({ city }) => { await new Promise(r => setTimeout(r, 100)); // simulate API delay const data = { Delhi: "32°C sunny", Mumbai: "28°C cloudy" }; return data[city] || `No data for ${city}`; }, { name: "get_weather", description: "Gets weather for a city.", schema: z.object({ city: z.string() }), } );
const news = tool( async ({ topic }) => { await new Promise(r => setTimeout(r, 100)); return `Latest news about ${topic}: [Simulated headline 1], [Simulated headline 2]`; }, { name: "get_news", description: "Gets latest news headlines for a topic.", schema: z.object({ topic: z.string() }), } );
const calculator = tool( async ({ expression }) => { try { const result = Function(`"use strict"; return (${expression})`)(); return `${expression} = ${result}`; } catch { return `Cannot calculate: ${expression}`; } }, { name: "calculator", description: "Evaluates math expressions.", schema: z.object({ expression: z.string().describe("math expression like '25 * 4'"), }), } );
const agent = createAgent({ model: new ChatOpenAI({ model: "gpt-4o", temperature: 0 }), tools: [weather, news, calculator], systemPrompt: "You are a helpful assistant. Use multiple tools in parallel when a question needs several pieces of information.", });
async function main() { console.log("⚡ PARALLEL TOOL CALLING DEMO\n");
const startTime = Date.now();
const result = await agent.invoke({ messages: [{ role: "user", content: "What is the weather in Delhi, what are the latest AI news headlines, and what is 25 multiplied by 48?" // This question needs THREE tools // With parallel calling: all three run simultaneously // Without: they'd run one by one (3x slower) }], });
const elapsed = Date.now() - startTime; const lastMsg = result.messages[result.messages.length - 1];
console.log("Answer:", lastMsg.content); console.log(`\nCompleted in: ${elapsed}ms`); console.log("(All three tools ran in parallel — much faster than sequential)"); }
main().catch(console.error);
Run:
node src/04_parallel_tools.js
Function Calling vs Tool Calling — The Technical Difference
OpenAI Function Calling (2023 original):
→ You define "functions" in the API request
→ LLM outputs { "function_call": { "name": "...", "arguments": "{...}" } }
→ You manually execute and send result back
OpenAI Tool Calling (2024 current):
→ You define "tools" in the API request (same concept, new name)
→ LLM outputs tool_calls array (supports multiple at once)
→ You manually execute and send results back as tool messages
LangChain tool() wrapper:
→ Abstracts both — works with OpenAI, Anthropic, Gemini, all providers
→ Same tool() code works with any LLM that supports tool calling
→ LangChain handles the format differences between providers
→ createAgent() handles the loop automatically
3-Line Summary
- When an agent "calls a tool" — the LLM outputs JSON describing what function to call with what arguments — the LLM never executes code — LangChain reads that JSON, runs your actual JavaScript function, and sends the result back as a
ToolMessage. - Tool design is critical — the description is what the LLM reads to decide when to use the tool, always return strings not objects, always handle errors inside the tool without throwing, and use
.optional()in Zod for parameters the LLM doesn't always need. - Parallel tool calling means the LLM can request multiple tools in one step — LangChain runs them simultaneously — so a question needing weather + news + calculator gets all three answered at the same time instead of one by one.
Module 8.2 — Complete ✅
Coming up — Module 8.3 — Agent Memory & Planning
How agents remember things across sessions, how they plan multi-step tasks before acting, and the difference between short-term and long-term memory in production agent systems.
No comments:
Post a Comment