07. Function Calling & Tool Execution

Extending model agency with dynamic schema definitions, multi-turn loop orchestration, sandboxed execution, and safe exception bubbling.

Android Architecture Mental Model: Large language models do not execute code or run SQL queries themselves. Function calling is identical to Android Intent Resolution and Deep-Link Dispatch. Your app defines intent filters with specific expected extras. When an action occurs, the OS resolves the intent, constructs the bundle arguments, and hands execution back to the registered Activity or BroadcastReceiver. In AI systems, the LLM merely acts as an intelligent router deciding which function to invoke and emitting serialized JSON parameters matching your schema.

1. The Tool Execution Loop

A tool execution sequence requires a multi-turn lifecycle. The LLM acts as an orchestrator, signaling tool calls back to the application server for physical execution before ingesting the returned payload to formulate a final user-facing response.

sequenceDiagram autonumber participant App as Backend Host (FastAPI / Ktor) participant LLM as Model Inference (Gemini / GPT) participant Tool as Tool Dispatcher / Local Service App->>LLM: User Prompt + Registered Tool JSON Schemas Note over LLM: Evaluates intent against schemas.
Selects tool & generates JSON arguments. LLM-->>App: Stop Reason: tool_calls [name, args_json, call_id] App->>Tool: Execute function locally with parsed args Tool-->>App: Execution Output Payload (JSON / String) App->>LLM: Append Tool Result Turn (role='tool', call_id, content) Note over LLM: Synthesizes final answer
incorporating tool data. LLM-->>App: Final Natural Language / Structured Response

2. Tool Declaration Schema Architecture

Tools are declared using JSON Schema specifications. The model reads the descriptions to determine suitability during inference:

Schema Element System Function Design Best Practice
name Unique identifier used by the router to route execution to a handler. Use snake_case action verbs: get_user_account_balance.
description Semantic documentation read by the model to decide whether to call the tool. Explicitly document edge cases and required formats (e.g., ISO-8601 dates).
parameters JSON schema object defining types, descriptions, and required keys. Mark mandatory parameters in the required list; provide strict enums where applicable.

3. Parallel Tool Calling vs. Sequential Execution

Modern models support Parallel Tool Calling, emitting multiple tool invocations within a single inference response.

flowchart TD Q["User: 'Compare release metrics for build 102 and build 105'"] --> LLM["LLM Router Engine"] LLM --> CALL1["Tool Call 1: fetch_metrics(build_id=102)"] LLM --> CALL2["Tool Call 2: fetch_metrics(build_id=105)"] subgraph HostConcur["Host Execution (Dispatchers.IO / asyncio.gather)"] CALL1 --> EXEC1["HTTP Fetch 102"] CALL2 --> EXEC2["HTTP Fetch 105"] end EXEC1 --> COMB["Aggregate Results"] EXEC2 --> COMB COMB --> LLM_SYNTH["LLM Synthesis Turn"] LLM_SYNTH --> RESP["Comparative Summary Response"]

4. Dual-Stack Implementations: Full Function Calling Loop

Registering a mock database lookup tool, processing the model's call request, executing the handler, and returning the final synthesized answer:

import json
from openai import OpenAI

client = OpenAI()

# 1. Local Tool Implementation
def query_user_tier(user_id: str) -> dict:
    # Simulated database lookup
    database = {"usr_42": {"tier": "PLATINUM", "retention_score": 0.94}}
    return database.get(user_id, {"tier": "FREE", "retention_score": 0.10})

# 2. Tool Schema Declaration
tools = [{
    "type": "function",
    "function": {
        "name": "query_user_tier",
        "description": "Fetch user loyalty tier and retention metrics by user ID.",
        "parameters": {
            "type": "object",
            "properties": {
                "user_id": {"type": "string", "description": "The unique user identifier (e.g. usr_42)"}
            },
            "required": ["user_id"]
        }
    }
}]

# 3. Initial Inference Turn
messages = [{"role": "user", "content": "What is the membership tier of usr_42?"}]
response = client.chat.completions.create(model="gpt-4o-mini", messages=messages, tools=tools)
choice = response.choices[0]

# 4. Handle Tool Calling Loop
if choice.message.tool_calls:
    messages.append(choice.message) # Append assistant's tool-request turn
    
    for tool_call in choice.message.tool_calls:
        if tool_call.function.name == "query_user_tier":
            args = json.loads(tool_call.function.arguments)
            result = query_user_tier(args["user_id"])
            
            # Append tool result turn
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result)
            })

    # 5. Final Synthesis
    final_response = client.chat.completions.create(model="gpt-4o-mini", messages=messages)
    print(final_response.choices[0].message.content)
import dev.langchain4j.agent.tool.Tool
import dev.langchain4j.memory.chat.MessageWindowChatMemory
import dev.langchain4j.model.openai.OpenAiChatModel
import dev.langchain4j.service.AiServices

// 1. Define Tool Service with Annotations
class UserMembershipTools {
    @Tool("Fetch user loyalty tier and retention metrics by user ID")
    fun queryUserTier(userId: String): String {
        return if (userId == "usr_42") {
            """{"tier": "PLATINUM", "retention_score": 0.94}"""
        } else {
            """{"tier": "FREE", "retention_score": 0.10}"""
        }
    }
}

// 2. High-Level AI Service Interface
interface CustomerSupportAgent {
    fun chat(message: String): String
}

fun main() {
    val chatModel = OpenAiChatModel.builder()
        .apiKey(System.getenv("OPENAI_API_KEY"))
        .modelName("gpt-4o-mini")
        .build()

    // 3. Bind Service with Reflective Tool Dispatch
    val agent = AiServices.builder(CustomerSupportAgent::class.java)
        .chatLanguageModel(chatModel)
        .tools(UserMembershipTools())
        .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
        .build()

    // LangChain4j automatically extracts JSON schema, detects tool calls, 
    // runs queryUserTier() on JVM, and feeds the response back to the LLM.
    val response = agent.chat("What is the membership tier of usr_42?")
    println(response)
}

5. Exception Handling & Sandboxed Execution

6. Progressive Glossary

Term Technical Definition Mobile / Systems Analogy
Tool Calling Inference mode where the model returns an operational command and typed parameters instead of text. Android Explicit Intent creation with serialized Bundle arguments.
Tool Call ID A unique transaction token pairing an LLM tool request with its subsequent execution output. requestCode in startActivityForResult() or a Coroutine Job handle.
Parallel Tool Calling Emitting multiple tool requests in a single forward pass to execute independent side-effects concurrently. kotlinx.coroutines.awaitAll() over multiple concurrent async network requests.
Tool Choice Constraint Forcing the model to invoke a specific tool (tool_choice: {"name": "..."}) or generate plain text (tool_choice: "none"). Explicit Intent targeting an exact ComponentName vs. Implicit Intent broadcast resolution.

Sources & Reference Standards