13. Connecter des outils au LLM
Dans le chapitre 12, nous avons construit des outils qu'un agent peut utiliser. Il est maintenant temps de connecter ces outils au LLM pour que l'agent puisse réellement les utiliser.
Peu importe la qualité de la construction d'un outil, le LLM ne peut pas l'utiliser sans savoir qu'il existe. Il doit savoir ce qui est disponible et choisir le bon outil pour chaque requête. Cependant, comme nous l'avons appris au chapitre 12, le LLM n'exécute pas les outils directement. Au lieu de cela, il demande « appelle cet outil avec ces arguments », et c'est le code de notre agent qui invoque l'outil. L'ensemble de ce mécanisme s'appelle l'appel d'outils (tool calling).
Dans ce chapitre, nous allons lier des outils au LLM, exécuter les demandes d'appel d'outils et renvoyer les résultats au LLM — en implémentant le cycle complet. Ce cycle constitue la base de la boucle d'agent que nous construirons au chapitre 14.
13.1) Lier des outils et inspecter les demandes d'appel d'outils
Pour que le LLM utilise des outils, il doit d'abord savoir ce qui est disponible. Lorsque nous transmettons au LLM le name, la description et le schéma d'entrée de chaque outil, il apprend quand et comment utiliser chaque outil. Ce processus s'appelle la liaison d'outils (tool binding).
Une fois les outils liés, le LLM fera l'une de deux choses lorsqu'il reçoit une question. Si aucun outil n'est nécessaire, il répond par du texte comme d'habitude. Si un outil est nécessaire, il renvoie une demande structurée : « appelle cet outil avec ces arguments ». Notre agent inspecte ensuite cette demande et invoque l'outil spécifié.
Commençons par voir comment lier des outils.
13.1.1) Lier des outils avec bind_tools()
La liaison d'outils est gérée par une seule méthode : bind_tools(). Tout modèle de chat qui prend en charge l'appel d'outils fournit cette méthode. L'appel de bind_tools() retourne un nouvel objet modèle auquel les outils donnés sont liés.
Lions l'outil get_weather que nous avons construit au chapitre 12.
from langchain_openai import ChatOpenAI
from langchain.tools import tool
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""Récupère la météo actuelle pour une ville donnée."""
return f"Il fait toujours beau à {city} !"
# Lie l'outil au modèle — un nouvel objet modèle avec l'outil lié est retourné
llm_with_tools = llm.bind_tools([get_weather])C'est tout ce qu'il faut. L'objet llm_with_tools retourné est un modèle qui connaît l'outil get_weather. Notez que le llm d'origine n'a pas du tout changé — il ne connaît toujours aucun outil.
bind_tools() prend une liste d'outils comme argument et retourne un nouvel objet modèle auquel ces outils sont liés. En interne, elle convertit les métadonnées de chaque outil (name, description et schéma d'entrée) dans un format que le fournisseur du LLM comprend, de sorte que les schémas d'outils soient envoyés à chaque invocation du modèle.
13.1.2) Inspecter les demandes d'appel d'outils
Que se passe-t-il lorsque nous envoyons une question sur la météo à un modèle auquel un outil météo est lié ?
response = llm_with_tools.invoke("Quel temps fait-il à Paris ?")
print(f"type: {type(response)}\n")
print(f"tool_calls: {response.tool_calls}\n")
print(f"content: {repr(response.content)}\n")Sortie :
type: <class 'langchain_core.messages.ai.AIMessage'>
tool_calls: [{'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': 'call_hgXrHGD', 'type': 'tool_call'}]
content: ''Comme llm_with_tools reste un modèle de chat, il retourne un AIMessage. Mais deux choses sont différentes dans cette réponse.
Premièrement, response.tool_calls contient une liste de demandes d'appel d'outils. Chaque demande est un dictionnaire avec quatre clés :
name— le nom de l'outil à appeler. Cela correspond aunamede l'outil dans ses métadonnées.args— les arguments à transmettre lors de l'appel de l'outil. Le modèle a examiné le schéma d'entrée et la question de l'utilisateur pour construire{'city': 'Paris'}.id— un identifiant unique pour cet appel. Le LLM l'utilise plus tard pour faire correspondre les résultats d'exécution des outils avec les demandes d'appel d'outils.type— toujours'tool_call'.
Deuxièmement, response.content est une chaîne vide. Cette réponse n'est pas une réponse textuelle finale — c'est une demande d'appel d'outil.
Pour déterminer si la réponse est une demande d'appel d'outil, vérifiez tool_calls plutôt que content. Si tool_calls n'est pas vide, c'est une demande d'appel d'outil. S'il est vide, le LLM a fourni directement une réponse textuelle.
13.2) Exécuter les outils et renvoyer les résultats
Le LLM a demandé un appel de l'outil get_weather. Maintenant, le code de notre agent doit exécuter l'outil et renvoyer le résultat au LLM.
Le traitement d'une demande d'appel d'outil comporte trois étapes :
- Extraire le nom de l'outil et les arguments de
tool_callset exécuter l'outil correspondant. - Convertir le résultat de l'exécution en un
ToolMessage. - Envoyer la conversation complète (question de l'utilisateur + demande d'appel d'outil du LLM + résultat de l'exécution de l'outil) au LLM pour recevoir une réponse.
Voici un diagramme de ce processus :
13.2.1) Exécuter l'outil et créer un ToolMessage
D'abord, extrayons le nom de l'outil et les arguments de tool_calls et exécutons l'outil correspondant. Pour rechercher un outil par son nom, nous créons un dictionnaire indexé par le nom de l'outil.
# Un dictionnaire pour rechercher les outils par leur nom
tool_map = {get_weather.name: get_weather}Ensuite, nous extrayons une demande d'appel d'outil de tool_calls, trouvons l'outil correspondant dans tool_map et l'exécutons.
if response.tool_calls:
tool_call = response.tool_calls[0]
# {'name': 'get_weather', 'args': {'city': 'Paris'}, 'id': 'call_hgXrHGD', 'type': 'tool_call'}
selected_tool = tool_map[tool_call["name"]]
result = selected_tool.invoke(tool_call["args"])
print(result) # Sortie : Il fait toujours beau à Paris !Pour renvoyer le résultat de l'exécution de l'outil au LLM, nous devons l'encapsuler dans un ToolMessage. Un ToolMessage possède deux champs obligatoires :
content— le résultat de l'exécution de l'outil sous forme de chaîne.tool_call_id— l'idprovenant detool_calls. Le LLM l'utilise pour identifier à quelle demande le résultat appartient.
from langchain_core.messages import ToolMessage
if response.tool_calls:
tool_call = response.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
result = selected_tool.invoke(tool_call["args"])
tool_message = ToolMessage(
content=result,
tool_call_id=tool_call["id"], # doit correspondre à l'id de la demande
)
print(tool_message)Sortie :
content="Il fait toujours beau à Paris !" tool_call_id='call_hgXrHGD'Dans le code ci-dessus, nous avons transmis args pour exécuter l'outil, puis combiné le résultat avec le tool_call_id pour construire le ToolMessage.
Il existe une façon plus simple de faire cela en une seule étape. Au lieu de transmettre seulement args à .invoke(), transmettez le dictionnaire tool_call entier. LangChain exécutera l'outil et retournera automatiquement un ToolMessage.
if response.tool_calls:
tool_call = response.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
print(tool_message)Sortie :
content="Il fait toujours beau à Paris !" tool_call_id='call_hgXrHGD'Même résultat, mais le code est beaucoup plus simple et il n'y a aucun risque de discordance du tool_call_id. Nous utiliserons cette approche à l'avenir.
13.2.2) Envoyer les résultats des outils au LLM
Maintenant, transmettons le résultat de l'exécution de l'outil au LLM. Nous renvoyons l'historique complet de la conversation — la question de l'utilisateur, la demande d'appel d'outil du LLM et le résultat de l'exécution de l'outil — au LLM, et il génère une réponse basée sur le résultat de l'outil.
Assemblons l'ensemble du processus du début à la fin dans un seul bloc de code.
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
# Définit les outils et les lie
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""Récupère la météo actuelle pour une ville donnée."""
return f"Il fait toujours beau à {city} !"
llm_with_tools = llm.bind_tools([get_weather])
tool_map = {get_weather.name: get_weather}
# Étape 1 : Envoie la question de l'utilisateur et reçoit la réponse du LLM
messages = [HumanMessage("Quel temps fait-il à Paris ?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
# Étape 2 : Si un appel d'outil a été demandé, exécute l'outil et ajoute le résultat
if ai_msg.tool_calls:
tool_call = ai_msg.tool_calls[0]
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
# Étape 3 : Renvoie l'historique complet de la conversation au LLM
final_response = llm_with_tools.invoke(messages)
print(final_response.content)Sortie :
Le temps actuel à Paris est ensoleillé. Passez une excellente journée !Le LLM a généré une réponse textuelle finale basée sur le résultat de l'exécution de l'outil. Précédemment, en 13.1, content était une chaîne vide — mais après avoir renvoyé le résultat de l'outil, nous avons reçu une véritable réponse.
Vérifions quels messages se trouvent dans la liste messages à l'étape 3.
for m in messages:
print(f"{type(m).__name__}: {m.content!r}")Sortie :
HumanMessage: "Quel temps fait-il à Paris ?"
AIMessage: ''
ToolMessage: "Il fait toujours beau à Paris !"Trois messages sont stockés dans l'ordre : la question de l'utilisateur, la demande d'appel d'outil du LLM (contenu vide) et le résultat de l'exécution de l'outil. Le LLM a vu la question de l'utilisateur, a décidé qu'il avait besoin d'un outil et a demandé un appel de get_weather. Le code de l'agent a exécuté get_weather et a ajouté le résultat à la liste de messages. Une fois qu'il avait tout ce dont il avait besoin, le LLM a généré sa réponse finale.
13.3) Donner plusieurs outils au LLM
Les agents du monde réel disposent généralement de plusieurs outils. Tout ce que nous avons couvert jusqu'à présent fonctionne exactement de la même manière avec plusieurs outils.
Ajoutons calculate et search_web aux côtés de get_weather pour lier trois outils.
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
llm = ChatOpenAI(model="gpt-5-mini")
@tool
def get_weather(city: str) -> str:
"""Récupère la météo actuelle pour une ville donnée."""
return f"Il fait toujours beau à {city} !"
@tool
def calculate(expression: str) -> str:
"""Évalue une expression arithmétique simple comme '123 * 456'."""
return str(eval(expression)) # Attention : eval() est un risque de sécurité. Ne pas utiliser en production.
@tool
def search_web(query: str) -> str:
"""Recherche sur le web des informations actuelles sur un sujet."""
return f"Meilleur résultat pour '{query}' : ..."
tools = [get_weather, calculate, search_web]
llm_with_tools = llm.bind_tools(tools)
tool_map = {t.name: t for t in tools}Posons une question de calcul.
response = llm_with_tools.invoke("Combien font 123 multiplié par 456 ?")
print(response.tool_calls)Sortie :
[{'name': 'calculate', 'args': {'expression': '123 * 456'}, 'id': 'call_xyz789', 'type': 'tool_call'}]Le LLM a demandé un appel de calculate. Il a comparé la description de chaque outil à la question de l'utilisateur et a déterminé que calculate était le bon outil.
Nous pouvons exécuter l'outil et obtenir une réponse finale en utilisant le même modèle qu'en 13.2.
messages = [HumanMessage("Combien font 123 multiplié par 456 ?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
if ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
final_response = llm_with_tools.invoke(messages)
print(final_response.content) # Sortie : 123 multiplié par 456 fait 56 088.En 13.2, nous n'avons traité que la première demande avec tool_calls[0], mais ici nous parcourons toutes les demandes avec une boucle for. La raison en est que le LLM peut demander plusieurs appels d'outils à la fois.
Plusieurs demandes d'outils simultanées
Voyons ce qui se passe lorsque plusieurs outils sont demandés en même temps. Le code est le même — seule la question change.
messages = [HumanMessage("Quel temps fait-il à Tokyo, et combien font 123 * 456 ?")]
ai_msg = llm_with_tools.invoke(messages)
messages.append(ai_msg)
if ai_msg.tool_calls:
for tool_call in ai_msg.tool_calls:
print(f"Tool: {tool_call['name']}")
selected_tool = tool_map[tool_call["name"]]
tool_message = selected_tool.invoke(tool_call)
messages.append(tool_message)
final_response = llm_with_tools.invoke(messages)
print(final_response.content)Sortie :
Tool: get_weather
Tool: calculate
Le temps à Tokyo est actuellement ensoleillé, et 123 multiplié par 456 fait 56 088.Le LLM a demandé à la fois get_weather et calculate, et la boucle for a exécuté toutes les demandes dans tool_calls et a ajouté leurs résultats à la liste de messages. Le LLM a ensuite utilisé ces résultats pour générer la réponse finale.