Function Calling : Comment les LLMs appellent des outils et APIs

tl;dr: Function calling = capacité des LLMs à appeler fonctions/APIs externes de manière structurée. Le modèle analyse la requête, choisit la fonction, extrait les paramètres, retourne un appel JSON. Base des agents IA. Support : OpenAI, Anthropic, Google.

💡 🔧 Le function calling est LA technique clé pour créer des agents IA et connecter les LLMs au monde réel : APIs, bases de données, calculateurs, et tout outil imaginable !

Le function calling (ou tool use) est la capacité des LLMs modernes à appeler des fonctions et outils externes de manière structurée. C’est ce qui transforme un simple chatbot en assistant intelligent capable d’actions concrètes : interroger une base de données, appeler une API météo, effectuer des calculs, créer un ticket, envoyer un email, et bien plus.

Illustration détaillée de le function calling avec les modèles de langage

Qu’est-ce que le Function Calling ?

Le problème sans function calling

Imaginons que vous demandiez à un LLM :

Quelle est la météo à Paris actuellement ?

Sans function calling, le LLM répondrait :

Je n'ai pas accès à des données météo en temps réel. Mes données s'arrêtent
en janvier 2025. Je vous suggère de consulter meteo.fr ou OpenWeatherMap.

Avec function calling, le flow est :

  1. L’utilisateur demande : “Quelle est la météo à Paris ?”
  2. Le LLM analyse : “Je dois appeler la fonction get_weather avec city=‘Paris’”
  3. Votre code exécute la fonction : get_weather("Paris")
  4. L’API retourne : {"temp": 12, "condition": "Nuageux", "humidity": 75}
  5. Le LLM génère une réponse naturelle : “Il fait actuellement 12°C à Paris avec un ciel nuageux. L’humidité est de 75%.”

Architecture du function calling

┌─────────────┐
│   Utilisateur │
│ "Météo Paris?" │
└───────┬───────┘
        v
┌───────────────────┐
│       LLM          │
│ Analyse requête    │
│ Détermine fonction │
│ Extrait paramètres │
└───────┬───────────┘
        │ function_call: {
        │   name: "get_weather",
        │   arguments: {"city": "Paris"}
        │ }
        v
┌───────────────────┐
│   Votre Code       │
│ Exécute fonction   │
│ get_weather("Paris")│
└───────┬───────────┘
        v
┌───────────────────┐
│   API Météo        │
│ Retourne JSON      │
└───────┬───────────┘
        │ {"temp": 12, ...}
        v
┌───────────────────┐
│       LLM          │
│ Génère réponse     │
│ naturelle          │
└───────┬───────────┘
        v
┌─────────────────┐
│ "Il fait 12°C à  │
│ Paris, nuageux"  │
└─────────────────┘
💡 💡 Le LLM ne peut pas directement exécuter du code ou appeler des APIs. Il génère un JSON structuré indiquant QUELLE fonction appeler et AVEC QUELS paramètres. C’est votre code qui exécute réellement la fonction.

OpenAI Functions (GPT-4, GPT-4o)

Définir une fonction

Les fonctions sont définies via un JSON Schema :

functions = [
    {
        "name": "get_weather",
        "description": "Obtient la météo actuelle d'une ville",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "Nom de la ville (ex: Paris, London)"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Unité de température"
                }
            },
            "required": ["city"]
        }
    }
]

Composants clés :

  • name : Identifiant de la fonction (snake_case recommandé)
  • description : Ce que fait la fonction (crucial pour que le LLM comprenne quand l’utiliser)
  • parameters : Schéma JSON des paramètres
    • type : Type de chaque paramètre (string, number, boolean, object, array)
    • description : Explication de chaque paramètre
    • enum : Valeurs autorisées (optionnel)
    • required : Liste des paramètres obligatoires

Appeler l’API avec functions

import openai
import json

client = openai.OpenAI(api_key="sk-...")

messages = [
    {"role": "user", "content": "Quelle est la météo à Paris ?"}
]

# Premier appel : le LLM détermine quelle fonction appeler
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    functions=functions,
    function_call="auto"  # "auto", "none", ou {"name": "fonction_specifique"}
)

message = response.choices[0].message

# Vérifier si le LLM veut appeler une fonction
if message.function_call:
    function_name = message.function_call.name
    function_args = json.loads(message.function_call.arguments)

    print(f"Fonction appelée : {function_name}")
    print(f"Arguments : {function_args}")
    # → Fonction appelée : get_weather
    # → Arguments : {'city': 'Paris', 'unit': 'celsius'}

    # Exécuter la fonction (votre code)
    if function_name == "get_weather":
        result = get_weather(**function_args)  # Votre implémentation

    # Ajouter le résultat aux messages
    messages.append(message)  # Appel de fonction du LLM
    messages.append({
        "role": "function",
        "name": function_name,
        "content": json.dumps(result)
    })

    # Second appel : le LLM génère la réponse finale
    final_response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages
    )

    print(final_response.choices[0].message.content)
    # → "Il fait actuellement 12°C à Paris avec un ciel nuageux..."

Implémentation complète

import openai
import json
import requests

def get_weather(city: str, unit: str = "celsius") -> dict:
    """Récupère la météo via OpenWeatherMap API"""
    api_key = "YOUR_OPENWEATHER_API_KEY"
    base_url = "https://api.openweathermap.org/data/2.5/weather"

    params = {
        "q": city,
        "appid": api_key,
        "units": "metric" if unit == "celsius" else "imperial"
    }

    try:
        response = requests.get(base_url, params=params)
        response.raise_for_status()
        data = response.json()

        return {
            "city": city,
            "temperature": data["main"]["temp"],
            "condition": data["weather"][0]["description"],
            "humidity": data["main"]["humidity"],
            "unit": "°C" if unit == "celsius" else "°F"
        }
    except Exception as e:
        return {"error": str(e)}

def run_conversation(user_message: str):
    """Execute une conversation avec function calling"""

    messages = [{"role": "user", "content": user_message}]

    functions = [
        {
            "name": "get_weather",
            "description": "Obtient la météo actuelle d'une ville",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "Nom de la ville"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"]
                    }
                },
                "required": ["city"]
            }
        }
    ]

    # Appel initial
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        functions=functions
    )

    message = response.choices[0].message

    # Si fonction appelée
    if message.function_call:
        function_name = message.function_call.name
        function_args = json.loads(message.function_call.arguments)

        # Exécuter la fonction
        if function_name == "get_weather":
            function_response = get_weather(**function_args)

        # Ajouter aux messages
        messages.append(message)
        messages.append({
            "role": "function",
            "name": function_name,
            "content": json.dumps(function_response)
        })

        # Réponse finale
        final_response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages
        )

        return final_response.choices[0].message.content

    # Pas de fonction appelée, réponse directe
    return message.content

# Utilisation
print(run_conversation("Quelle est la météo à Paris ?"))

Anthropic Tools (Claude)

Claude utilise une API légèrement différente appelée “Tools” :

Définir un tool

import anthropic

tools = [
    {
        "name": "get_weather",
        "description": "Obtient la météo actuelle d'une ville. Utilisez cette fonction quand l'utilisateur demande la météo, la température, ou les conditions climatiques.",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "Le nom de la ville (ex: Paris, Tokyo, New York)"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "L'unité de température souhaitée"
                }
            },
            "required": ["city"]
        }
    }
]

Différence avec OpenAI : input_schema au lieu de parameters.

Appel avec Claude

client = anthropic.Anthropic(api_key="sk-ant-...")

message = client.messages.create(
    model="claude-3-5-sonnet-20241022",
    max_tokens=1024,
    tools=tools,
    messages=[
        {"role": "user", "content": "Quelle température fait-il à Tokyo ?"}
    ]
)

# Vérifier si Claude veut utiliser un tool
if message.stop_reason == "tool_use":
    tool_use = next(block for block in message.content if block.type == "tool_use")

    function_name = tool_use.name
    function_args = tool_use.input

    print(f"Tool: {function_name}")
    print(f"Args: {function_args}")
    # → Tool: get_weather
    # → Args: {'city': 'Tokyo', 'unit': 'celsius'}

    # Exécuter la fonction
    result = get_weather(**function_args)

    # Continuer la conversation
    response = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=1024,
        tools=tools,
        messages=[
            {"role": "user", "content": "Quelle température fait-il à Tokyo ?"},
            {"role": "assistant", "content": message.content},
            {
                "role": "user",
                "content": [
                    {
                        "type": "tool_result",
                        "tool_use_id": tool_use.id,
                        "content": json.dumps(result)
                    }
                ]
            }
        ]
    )

    print(response.content[0].text)

Google Function Calling (Gemini)

Définir une function declaration

import google.generativeai as genai

tools = [
    {
        "function_declarations": [
            {
                "name": "get_weather",
                "description": "Obtient la météo actuelle d'une ville",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "city": {
                            "type": "string",
                            "description": "Nom de la ville"
                        },
                        "unit": {
                            "type": "string",
                            "enum": ["celsius", "fahrenheit"]
                        }
                    },
                    "required": ["city"]
                }
            }
        ]
    }
]

genai.configure(api_key="YOUR_GOOGLE_API_KEY")

model = genai.GenerativeModel(
    model_name="gemini-1.5-pro",
    tools=tools
)

response = model.generate_content("Météo à Londres ?")

# Vérifier function call
for part in response.candidates[0].content.parts:
    if fn := part.function_call:
        print(f"Function: {fn.name}")
        print(f"Args: {dict(fn.args)}")

        # Exécuter
        result = get_weather(**dict(fn.args))

        # Retourner le résultat
        response = model.generate_content([
            response.candidates[0].content,
            genai.protos.Content(
                parts=[genai.protos.Part(
                    function_response=genai.protos.FunctionResponse(
                        name=fn.name,
                        response={"result": result}
                    )
                )]
            )
        ])

        print(response.text)

Exemples pratiques avancés

Requête base de données

import sqlite3

def query_database(query_type: str, filters: dict) -> list:
    """Interroge la base de données clients"""

    conn = sqlite3.connect("customers.db")
    cursor = conn.cursor()

    if query_type == "count_customers":
        sql = "SELECT COUNT(*) FROM customers WHERE 1=1"
        params = []

        if "country" in filters:
            sql += " AND country = ?"
            params.append(filters["country"])

        if "min_orders" in filters:
            sql += " AND total_orders >= ?"
            params.append(filters["min_orders"])

        cursor.execute(sql, params)
        count = cursor.fetchone()[0]

        return {"count": count, "filters": filters}

    elif query_type == "top_customers":
        limit = filters.get("limit", 10)
        sql = f"""
            SELECT name, email, total_orders, total_spent
            FROM customers
            ORDER BY total_spent DESC
            LIMIT ?
        """
        cursor.execute(sql, [limit])

        results = []
        for row in cursor.fetchall():
            results.append({
                "name": row[0],
                "email": row[1],
                "orders": row[2],
                "spent": row[3]
            })

        return {"customers": results}

    conn.close()

# Définition de la fonction
database_function = {
    "name": "query_database",
    "description": "Interroge la base de données clients pour obtenir des statistiques ou listes de clients",
    "parameters": {
        "type": "object",
        "properties": {
            "query_type": {
                "type": "string",
                "enum": ["count_customers", "top_customers", "customer_details"],
                "description": "Le type de requête à effectuer"
            },
            "filters": {
                "type": "object",
                "description": "Filtres optionnels",
                "properties": {
                    "country": {"type": "string"},
                    "min_orders": {"type": "integer"},
                    "limit": {"type": "integer"}
                }
            }
        },
        "required": ["query_type"]
    }
}

# Utilisation
user_query = "Combien de clients français ont fait plus de 10 commandes ?"
# → Le LLM appellerait : query_database("count_customers", {"country": "France", "min_orders": 10})

Calculs mathématiques complexes

import sympy as sp

def calculate(expression: str, operation: str = "evaluate") -> dict:
    """
    Effectue des calculs mathématiques avancés.

    operation peut être:
    - "evaluate" : Évalue l'expression
    - "simplify" : Simplifie
    - "solve" : Résout une équation
    - "derivative" : Calcule la dérivée
    - "integral" : Calcule l'intégrale
    """

    try:
        if operation == "evaluate":
            result = sp.sympify(expression).evalf()
            return {"result": float(result), "expression": expression}

        elif operation == "simplify":
            result = sp.simplify(expression)
            return {"simplified": str(result), "original": expression}

        elif operation == "solve":
            x = sp.Symbol('x')
            equation = sp.sympify(expression)
            solutions = sp.solve(equation, x)
            return {"solutions": [float(sol) for sol in solutions]}

        elif operation == "derivative":
            x = sp.Symbol('x')
            expr = sp.sympify(expression)
            derivative = sp.diff(expr, x)
            return {"derivative": str(derivative), "function": expression}

        elif operation == "integral":
            x = sp.Symbol('x')
            expr = sp.sympify(expression)
            integral = sp.integrate(expr, x)
            return {"integral": str(integral), "function": expression}

    except Exception as e:
        return {"error": str(e)}

calculate_function = {
    "name": "calculate",
    "description": "Effectue des calculs mathématiques avancés : évaluation, simplification, résolution d'équations, dérivées, intégrales",
    "parameters": {
        "type": "object",
        "properties": {
            "expression": {
                "type": "string",
                "description": "Expression mathématique (ex: '2*x + 3', 'sin(x)*cos(x)', 'x**2 - 4')"
            },
            "operation": {
                "type": "string",
                "enum": ["evaluate", "simplify", "solve", "derivative", "integral"],
                "description": "Opération à effectuer"
            }
        },
        "required": ["expression"]
    }
}

# Exemples d'utilisation via LLM :
# "Calcule la dérivée de x**2 + 3*x + 2"
# → calculate("x**2 + 3*x + 2", "derivative")
# Résultat : 2*x + 3

# "Résous l'équation x**2 - 5*x + 6 = 0"
# → calculate("x**2 - 5*x + 6", "solve")
# Résultat : [2.0, 3.0]

Intégration API multiple

import requests
from datetime import datetime

def search_flights(origin: str, destination: str, date: str, passengers: int = 1) -> list:
    """Recherche de vols (exemple avec API fictive)"""
    # En production, appel réel à une API comme Skyscanner, Amadeus, etc.
    api_url = "https://api.flights.example.com/search"

    response = requests.get(api_url, params={
        "from": origin,
        "to": destination,
        "date": date,
        "passengers": passengers
    })

    return response.json()["flights"]

def book_flight(flight_id: str, passenger_details: dict) -> dict:
    """Réservation d'un vol"""
    api_url = "https://api.flights.example.com/book"

    response = requests.post(api_url, json={
        "flight_id": flight_id,
        "passenger": passenger_details
    })

    return response.json()

def send_confirmation_email(email: str, booking_ref: str, details: dict) -> bool:
    """Envoie email de confirmation"""
    # Implémentation avec SendGrid, SES, etc.
    pass

# Définitions des fonctions
travel_functions = [
    {
        "name": "search_flights",
        "description": "Recherche des vols disponibles entre deux villes",
        "parameters": {
            "type": "object",
            "properties": {
                "origin": {"type": "string", "description": "Ville de départ (code IATA ou nom)"},
                "destination": {"type": "string", "description": "Ville d'arrivée"},
                "date": {"type": "string", "description": "Date de départ (YYYY-MM-DD)"},
                "passengers": {"type": "integer", "description": "Nombre de passagers", "default": 1}
            },
            "required": ["origin", "destination", "date"]
        }
    },
    {
        "name": "book_flight",
        "description": "Réserve un vol spécifique",
        "parameters": {
            "type": "object",
            "properties": {
                "flight_id": {"type": "string"},
                "passenger_details": {
                    "type": "object",
                    "properties": {
                        "name": {"type": "string"},
                        "email": {"type": "string"},
                        "passport": {"type": "string"}
                    },
                    "required": ["name", "email"]
                }
            },
            "required": ["flight_id", "passenger_details"]
        }
    },
    {
        "name": "send_confirmation_email",
        "description": "Envoie un email de confirmation de réservation",
        "parameters": {
            "type": "object",
            "properties": {
                "email": {"type": "string"},
                "booking_ref": {"type": "string"},
                "details": {"type": "object"}
            },
            "required": ["email", "booking_ref", "details"]
        }
    }
]

# Conversation multi-étapes
# User: "Je veux réserver un vol Paris → New York pour le 15 juin, 2 personnes"
# → LLM appelle : search_flights("Paris", "New York", "2025-06-15", 2)
# → Résultat : Liste de vols avec IDs
# LLM: "J'ai trouvé 5 vols. Le moins cher est à 450€ avec Air France..."
# User: "Réserve le vol AF123"
# → LLM appelle : book_flight("AF123", {...})
# → Puis : send_confirmation_email(...)

Parallel function calling

Les modèles récents (GPT-4o, Claude 3.5) peuvent appeler plusieurs fonctions en parallèle :

# User: "Quelle est la météo à Paris, Londres et Berlin ?"

# Le LLM peut retourner 3 appels simultanés :
function_calls = [
    {"name": "get_weather", "arguments": {"city": "Paris"}},
    {"name": "get_weather", "arguments": {"city": "Londres"}},
    {"name": "get_weather", "arguments": {"city": "Berlin"}}
]

# Votre code exécute en parallèle
import concurrent.futures

with concurrent.futures.ThreadPoolExecutor() as executor:
    futures = [
        executor.submit(get_weather, call["arguments"]["city"])
        for call in function_calls
    ]
    results = [f.result() for f in futures]

# Retourner tous les résultats au LLM
# Le LLM synthétise : "À Paris il fait 12°C, à Londres 10°C, et à Berlin 8°C..."
🔎 Tip
💡 Le parallel function calling réduit la latence de moitié pour les requêtes multi-outils ! Au lieu de 3 appels séquentiels LLM, vous n’en faites que 2 (1 pour décider, 1 pour synthétiser).

Gestion d’erreurs et robustesse

Validation des paramètres

from pydantic import BaseModel, validator

class WeatherParams(BaseModel):
    city: str
    unit: str = "celsius"

    @validator('city')
    def city_not_empty(cls, v):
        if not v or not v.strip():
            raise ValueError('City cannot be empty')
        return v.strip().title()

    @validator('unit')
    def unit_valid(cls, v):
        if v not in ['celsius', 'fahrenheit']:
            raise ValueError('Unit must be celsius or fahrenheit')
        return v

def get_weather_safe(city: str, unit: str = "celsius") -> dict:
    """Version sécurisée avec validation"""
    try:
        params = WeatherParams(city=city, unit=unit)
        # Appel API avec params validés
        return call_weather_api(params.city, params.unit)
    except ValueError as e:
        return {"error": f"Invalid parameters: {str(e)}"}
    except requests.RequestException as e:
        return {"error": f"API error: {str(e)}"}

Retry et fallbacks

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=1, min=2, max=10)
)
def get_weather_with_retry(city: str, unit: str = "celsius") -> dict:
    """Retry automatique en cas d'erreur réseau"""
    response = requests.get(weather_api_url, params={"city": city, "unit": unit})
    response.raise_for_status()
    return response.json()

def get_weather_with_fallback(city: str, unit: str = "celsius") -> dict:
    """Fallback sur API secondaire si primaire échoue"""
    try:
        return get_weather_primary_api(city, unit)
    except:
        try:
            return get_weather_backup_api(city, unit)
        except:
            return {
                "error": "Weather data unavailable",
                "message": "Please try again later"
            }

Error handling dans le flow

def safe_function_execution(function_name: str, args: dict) -> dict:
    """Wrapper sécurisé pour exécution de fonctions"""

    available_functions = {
        "get_weather": get_weather_safe,
        "query_database": query_database_safe,
        "calculate": calculate_safe
    }

    if function_name not in available_functions:
        return {
            "error": f"Function '{function_name}' not found",
            "available": list(available_functions.keys())
        }

    try:
        func = available_functions[function_name]
        result = func(**args)
        return {"success": True, "data": result}
    except TypeError as e:
        return {
            "error": "Invalid arguments",
            "message": str(e),
            "received": args
        }
    except Exception as e:
        return {
            "error": "Execution failed",
            "message": str(e),
            "function": function_name
        }

Sécurité et best practices

Validation stricte des inputs

⚠️ Warning
🔒 JAMAIS de confiance aveugle ! Le LLM peut générer des paramètres malveillants (injection SQL, path traversal). Validez TOUJOURS avant d’exécuter.
def query_database_safe(query_type: str, filters: dict) -> dict:
    """Version sécurisée avec whitelist"""

    # Whitelist des query types autorisés
    ALLOWED_QUERIES = ["count_customers", "top_customers"]
    if query_type not in ALLOWED_QUERIES:
        return {"error": "Query type not allowed"}

    # Whitelist des filtres
    ALLOWED_FILTERS = ["country", "min_orders", "limit"]
    invalid_filters = set(filters.keys()) - set(ALLOWED_FILTERS)
    if invalid_filters:
        return {"error": f"Invalid filters: {invalid_filters}"}

    # Sanitize string inputs
    if "country" in filters:
        # Accepter seulement alphanumériques et espaces
        if not re.match(r'^[a-zA-Z\s]+$', filters["country"]):
            return {"error": "Invalid country format"}

    # Limiter la taille des résultats
    if "limit" in filters:
        filters["limit"] = min(int(filters["limit"]), 100)  # Max 100

    # Exécution sécurisée
    return execute_query(query_type, filters)

Sandboxing pour code execution

import subprocess
import tempfile
import os

def execute_python_code(code: str, timeout: int = 5) -> dict:
    """Exécute du code Python dans un environnement isolé"""

    # Vérifications basiques
    dangerous_keywords = ['import os', 'import subprocess', 'eval', 'exec', '__import__']
    if any(kw in code for kw in dangerous_keywords):
        return {"error": "Dangerous code detected"}

    # Créer fichier temporaire
    with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
        f.write(code)
        temp_file = f.name

    try:
        # Exécuter dans subprocess avec timeout
        result = subprocess.run(
            ['python', temp_file],
            capture_output=True,
            text=True,
            timeout=timeout,
            # Limiter les ressources (sur Linux)
            # preexec_fn=lambda: resource.setrlimit(resource.RLIMIT_AS, (100*1024*1024, 100*1024*1024))
        )

        return {
            "stdout": result.stdout,
            "stderr": result.stderr,
            "returncode": result.returncode
        }
    except subprocess.TimeoutExpired:
        return {"error": "Execution timeout"}
    except Exception as e:
        return {"error": str(e)}
    finally:
        os.unlink(temp_file)

Rate limiting et cost control

from collections import defaultdict
from datetime import datetime, timedelta

class FunctionCallLimiter:
    def __init__(self):
        self.calls = defaultdict(list)
        self.limits = {
            "get_weather": {"per_hour": 100, "cost_per_call": 0.001},
            "query_database": {"per_hour": 50, "cost_per_call": 0.01},
            "send_email": {"per_hour": 10, "cost_per_call": 0.05}
        }

    def can_call(self, function_name: str, user_id: str) -> tuple[bool, str]:
        """Vérifie si l'appel est autorisé"""

        if function_name not in self.limits:
            return False, "Unknown function"

        limit_config = self.limits[function_name]
        key = f"{user_id}:{function_name}"

        # Nettoyer les anciens appels (>1h)
        cutoff = datetime.now() - timedelta(hours=1)
        self.calls[key] = [t for t in self.calls[key] if t > cutoff]

        # Vérifier limite
        if len(self.calls[key]) >= limit_config["per_hour"]:
            return False, f"Rate limit exceeded for {function_name}"

        # Enregistrer appel
        self.calls[key].append(datetime.now())

        return True, "OK"

    def get_cost(self, function_name: str) -> float:
        """Retourne le coût estimé de l'appel"""
        return self.limits.get(function_name, {}).get("cost_per_call", 0)

# Utilisation
limiter = FunctionCallLimiter()

def execute_function_with_limits(function_name: str, args: dict, user_id: str):
    can_call, message = limiter.can_call(function_name, user_id)

    if not can_call:
        return {"error": message}

    cost = limiter.get_cost(function_name)
    print(f"Executing {function_name} (cost: ${cost})")

    return execute_function(function_name, args)

Audit logging

import logging
from datetime import datetime

# Configuration du logger
logging.basicConfig(
    filename='function_calls.log',
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def log_function_call(function_name: str, args: dict, result: dict, user_id: str, duration_ms: float):
    """Enregistre tous les appels de fonction"""

    log_entry = {
        "timestamp": datetime.now().isoformat(),
        "user_id": user_id,
        "function": function_name,
        "arguments": args,
        "success": "error" not in result,
        "duration_ms": duration_ms
    }

    if "error" in result:
        log_entry["error"] = result["error"]
        logging.warning(f"Function call failed: {log_entry}")
    else:
        logging.info(f"Function call succeeded: {log_entry}")

    # Optionnel : envoyer à un service de monitoring (Datadog, Sentry, etc.)
    # send_to_monitoring_service(log_entry)

    return log_entry

# Wrapper avec logging
import time

def execute_with_logging(function_name: str, args: dict, user_id: str):
    start_time = time.time()

    try:
        result = execute_function(function_name, args)
        duration_ms = (time.time() - start_time) * 1000

        log_function_call(function_name, args, result, user_id, duration_ms)

        return result
    except Exception as e:
        duration_ms = (time.time() - start_time) * 1000
        error_result = {"error": str(e)}

        log_function_call(function_name, args, error_result, user_id, duration_ms)

        return error_result

Use cases en production

Chatbot support client

support_functions = [
    {
        "name": "search_knowledge_base",
        "description": "Recherche dans la base de connaissances",
        "parameters": {...}
    },
    {
        "name": "create_ticket",
        "description": "Crée un ticket support si problème non résolu",
        "parameters": {...}
    },
    {
        "name": "check_order_status",
        "description": "Vérifie le statut d'une commande",
        "parameters": {...}
    },
    {
        "name": "escalate_to_human",
        "description": "Transfère à un agent humain",
        "parameters": {...}
    }
]

Assistant de données

data_functions = [
    {
        "name": "query_sql",
        "description": "Exécute requête SQL (read-only)",
        "parameters": {...}
    },
    {
        "name": "generate_chart",
        "description": "Génère un graphique",
        "parameters": {...}
    },
    {
        "name": "export_csv",
        "description": "Exporte les données en CSV",
        "parameters": {...}
    }
]

Conclusion

Le function calling est la pierre angulaire des agents IA modernes. Il transforme les LLMs de simples générateurs de texte en assistants capables d’actions concrètes. Les clés du succès :

Descriptions claires : Le LLM doit comprendre QUAND utiliser chaque fonction

Validation stricte : Ne jamais faire confiance aveuglément aux paramètres générés

Gestion d’erreurs : Retry, fallbacks, messages d’erreur utiles

Sécurité : Sandboxing, rate limiting, audit logging

Monitoring : Tracer chaque appel pour détecter problèmes et optimiser coûts

Checklist de déploiement

Avant de mettre en production :

  • Toutes les fonctions ont des descriptions précises
  • Validation Pydantic ou équivalent sur tous les paramètres
  • Rate limiting implémenté par fonction et par utilisateur
  • Audit logging de tous les appels (succès + échecs)
  • Gestion d’erreurs avec messages utilisateur clairs
  • Tests unitaires pour chaque fonction
  • Tests d’intégration avec le LLM
  • Monitoring des coûts (appels API + LLM)
  • Documentation pour l’équipe
  • Plan de rollback si problème
💡 🚀 Prochaine étape : Combinez function calling avec les agents LangChain pour créer des systèmes multi-outils encore plus puissants !

Pour aller plus loin :


Le function calling ouvre des possibilités infinies : API, bases de données, outils métier, IoT… L’imagination est la seule limite !