Fonctionnalités avancées des outils LangChain

Dans la section précédente, nous avons appris l'utilisation de base de @tool. Cette section présente les fonctionnalités avancées des outils : return_direct, InjectedToolCallId, ToolException et la gestion des erreurs.


return_direct — Renvoyer directement le résultat final

Par défaut, après l'exécution d'un outil, le résultat est renvoyé au modèle, qui génère ensuite la réponse finale en fonction du résultat de l'outil. Mais parfois, le résultat de l'outil lui-même est la réponse finale souhaitée.

En définissantreturn_direct=Trueaprès cela, la boucle de l'Agent se termine immédiatement une fois que l'outil finit de s'exécuter, et le contenu renvoyé par l'outil est directement utilisé comme sortie finale.

Exemple

from dotenv import load_dotenv
load_dotenv()

from langchain.tools import tool
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage


# Outil ordinaire : résultat renvoyé au modèle, le modèle résume ensuite
@tool
def search_normal(keyword: str) -> str:
    """Rechercher les cours du tutoriel EXAMPLE (mode normal)"""
    return f"Résultats de recherche : Tutoriel des bases de Python3, Analyse de données Python, Introduction au crawler Python"


# outil return_direct : résultat directement utilisé comme sortie finale
@tool(return_direct=True)
def search_direct(keyword: str) -> str:
    """Rechercher les cours EXAMPLE Tutorial (mode de retour direct).

Utilisez cet outil lorsque l'utilisateur n'a besoin que des résultats de recherche et qu'aucune analyse supplémentaire n'est nécessaire.
    """

    return f"Résultats de recherche : Tutoriel des bases de Python3, Analyse de données Python, Introduction au crawler Python"


model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)

# Comparaison : mode normal vs mode de retour direct
agent_normal = create_agent(
    model=model,
    tools=[search_normal],
    system_prompt="Vous êtes le conseiller d'études pour EXAMPLE Tutorial.",
)

agent_direct = create_agent(
    model=model,
    tools=[search_direct],
    system_prompt="Vous êtes le conseiller d'études pour EXAMPLE Tutorial.",
)

# Mode normal : le modèle génère un résumé à partir des résultats de recherche
result = agent_normal.invoke({
    "messages": [HumanMessage(content="Rechercher des cours Python")]
})
print("=== Mode normal (le modèle poursuit le traitement) ===")
print(result["messages"][-1].content[:150])

# Mode de retour direct : le résultat de l'outil est la réponse finale
result = agent_direct.invoke({
    "messages": [HumanMessage(content="Rechercher des cours Python")]
})
print("\n=== Mode de retour direct (le résultat de l'outil est la réponse finale) ===")
print(result["messages"][-1].content[:150])

Résultat de l'exécution:

=== 普通模式(模型会再加工)===
在Example 中,我为您找到了以下 Python 相关课程:
1. Python3 基础教程 - 适合零基础入门
2. Python 数据分析 - 进阶学习
3. Python 爬虫入门 - 实战项目

=== 直接返回模式(工具结果即最终答案)===
搜索结果:Python3 基础教程、Python 数据分析、Python 爬虫入门
ModeAprès l'exécution de l'outilScénarios applicables
return_direct=False (par défaut)Le modèle reçoit le résultat de l'outil → le modèle continue de réfléchir → génère la réponse finaleNécessite une analyse/un résumé/une prise de décision supplémentaire
return_direct=TrueSe termine immédiatement après l'exécution de l'outil → le résultat de l'outil est la sortie finaleType de requête, type de récupération de données, résultats préformatés

Lorsque vous définissez return_direct=True, l'Agent ignore les étapes de réflexion ultérieures du modèle et renvoie directement le résultat de l'outil. Cela est très utile pour économiser des tokens et réduire la latence, mais cela signifie aussi que le modèle n'effectuera aucun traitement secondaire sur le résultat de l'outil.

Remarque :Si un Agent a plusieurs outils montés en même temps, incluant à la fois des outils return_direct=True et des outils ordinaires, alors tant que le modèle déclenche un outil return_direct dans ce cycle d'appels, la boucle de l'Agent se termine immédiatement—même si d'autres outils ordinaires sont appelés en parallèle dans le même cycle, leurs résultats ne seront plus traités ni résumés par le modèle. Gardez cela à l'esprit lors de la conception d'un Agent avec plusieurs outils pour éviter "du contenu qui devrait être résumé soit ignoré."


InjectedToolCallId — Obtention de l'ID d'appel d'outil

Parfois, un outil a besoin de savoir « qui l'a appelé » —InjectedToolCallIdVous pouvez injecter le tool_call_id actuel dans la fonction d'outil :

Exemple

from typing import Annotated

from langchain.messages import ToolCall
from langchain.tools import tool, InjectedToolCallId


@tool
def log_user_action(
    action: str,
    tool_call_id: Annotated[str, InjectedToolCallId],
) -> str:
    """Enregistrer les actions de l'utilisateur dans le système de journalisation.

    Arguments :
action: description de l'action de l'utilisateur
tool_call_id: ID d'appel d'outil injecté automatiquement
    """


    # Dans un vrai projet, cela serait écrit dans une base de données ou envoyé à un service de journalisation

    return f"Opération enregistrée (ID d'appel : {tool_call_id}) : {action}"


# Simuler manuellement un ToolCall généré par le modèle
tool_call: ToolCall = {
    "name": "log_user_action",
    "args": {
        "action": "L'utilisateur a consulté des cours Python"
    },
    "id": "call_log_001",
    "type": "tool_call",
}


# InjectedToolCallId sera automatiquement injecté à partir de tool_call.id
result = log_user_action.invoke(tool_call)

print(result)

Résultat de l'exécution:

content='操作已记录 (调用ID: call_log_001): 用户查询了 Python 课程' name='log_user_action' tool_call_id='call_log_001'

AvecInjectedToolArgles paramètres marqués n'ont pas besoin d'être fournis par l'Agent (modèle) ; ces paramètres sont automatiquement injectés par le runtime LangChain lors de l'exécution de l'outil. Comme ces paramètres n'apparaissent pas dans le schéma de l'outil, le modèle ne peut pas les voir, donc ils ne doivent pas être décrits comme des paramètres d'outil que l'utilisateur doit renseigner.

Par exemple,InjectedToolCallIdest un type spécial de paramètre injecté. LangChain transmet automatiquement l'ID unique du ToolCall en cours lors de l'exécution de l'outil. Il n'apparaît pas dans les paramètres générés par le modèle, mais il est automatiquement lié à l'appel d'outil en cours.

Appeler directement.invoke()et construire manuellementToolCall, n'a pour but que de démontrer comment LangChain implémente le mécanisme d'injection de paramètres. Dans de vrais projets, vous ne transmettez généralement pas cet ID manuellement.

Dans les workflows d'Agent,InjectedToolCallIdil est plus couramment utilisé dans les scénarios où le contexte de l'appel en cours doit être associé, par exemple, avecCommandobjet pour mettre à jour l'état de l'Agent à l'intérieur de l'outil,messagesajouter à la liste les éléments associés à l'appel en cours,ToolMessage, ou implémenter des fonctionnalités telles que le traçage des appels d'outils, l'audit et la corrélation des journaux.

C'est aussi pourquoi il est conçu comme un "paramètre injecté" plutôt qu'un paramètre ordinaire : il représente le contexte du ToolCall actuellement exécuté par le Runtime LangChain, et non une partie de l'entrée utilisateur.


ToolException — Gestion des exceptions d'outil

Des erreurs peuvent survenir pendant l'exécution d'un outil. UtilisezToolExceptionpour lever une exception d'outil claire, en informant l'Agent qu'une erreur s'est produite.

Exemple

from langchain.tools import tool, ToolException

@tool
def get_user_info(user_id: int) -> str:
    """Interroge les informations utilisateur par identifiant utilisateur.

    Arguments :
user_id: identifiant utilisateur, doit être un entier positif
    """

    # Validation des données
    if user_id <= 0:
        # Lever ToolException, pas une Exception ordinaire
        # ToolException sera interceptée par l'Agent et signalée au modèle
        raise ToolException(f"L'identifiant utilisateur doit être un entier positif, reçu : {user_id}")

    # Simuler une requête de base de données
    users = {
        1: "Zhang San (membre VIP, inscrit le 2024-01-15)",
        2: "Li Si (utilisateur ordinaire, inscrit le 2024-03-20)",
    }

    if user_id not in users:
        raise ToolException(f"Utilisateur avec l'ID {user_id} introuvable")

    return users[user_id]


# Appel normal
print(get_user_info.invoke({"user_id": 1}))

# Appel d'exception 1 : ID invalide
try:
    get_user_info.invoke({"user_id": -1})
except ToolException as e:
    print(f"Exception d'outil : {e}")

# Appel d'exception 2 : l'utilisateur n'existe pas
try:
    get_user_info.invoke({"user_id": 999})
except ToolException as e:
    print(f"Exception d'outil : {e}")

Résultat de l'exécution:

张三(VIP 会员,注册于 2024-01-15)
工具异常: 用户 ID 必须为正整数,收到了: -1
工具异常: 未找到 ID 为 999 的用户

La raison pour laquelle ToolException peut être capturée avec try/except à l'extérieur de .invoke() ici est que la valeur par défaut de l'outilhandle_tool_errorest False—l'exception n'est pas avalée par l'outil lui-même, mais est levée vers le haut comme d'habitude. Si vous voulez que l'outil « gère l'erreur lui-même » et convertisse le message d'erreur en chaîne à renvoyer au modèle au lieu de lever une exception, c'est handle_tool_error, ce qui sera couvert dans la section suivante.


handle_tool_error — Laissez l'outil gérer lui-même les erreurs

Lorsque vous voulez qu'un outil n'interrompe pas le programme en cas d'erreur, mais convertisse à la place le message d'erreur en un texte et le remette au modèle comme une valeur de retour normale pour que le modèle le comprenne et le corrige, vous pouvez le définir lors de la définition de l'outil.handle_tool_error(notez qu'il est au singulier, sans s). C'est un attribut de BaseTool, et la façon la plus simple de le définir est de l'écrire directement dans le décorateur @tool :

Exemple

from langchain.tools import tool, ToolException
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage


# handle_tool_error=True : ne lève plus d'exception en cas d'erreur,
# convertit à la place le contenu de ToolException en chaîne et le renvoie normalement au modèle
@tool(handle_tool_error=True)
def get_weather(city: str) -> str:
    """Interroger la météo pour une ville spécifiée.

    Arguments :
city : nom de la ville, doit être le nom chinois complet, par ex. "Hangzhou", "Beijing"
    """

    weather_data = {
        "Hangzhou": "Ensoleillé, 25°C",
        "Beijing": "Nuageux, 18°C",
        "Shanghai": Pluie légère, 22°C,
    }
    if city not in weather_data:
        # Lève ToolException lorsque la ville n'est pas dans les données
        raise ToolException(
            f"La ville '{city}' n'est pas dans la base de données."
            f"Villes disponibles : {', '.join(weather_data.keys())}."
            f"Veuillez utiliser le nom complet de la ville en chinois."
        )
    return f"Météo de {city} : {weather_data[city]}"


# Test : appel avec un nom de ville incorrect
# Lorsque handle_tool_error=True, le message d'erreur est renvoyé comme un résultat normal au lieu de lever une exception
result = get_weather.invoke({"city": "Territoire du Nord"})
print(f"handle_tool_error=True : {result}")


# Si vous voulez que toutes les erreurs des outils soient gérées uniformément par l'Agent (au lieu de les définir par outil),
# vous pouvez configurer cela au niveau de ToolNode utilisé dans create_agent.
# Dans les versions plus récentes de langchain, vous pouvez activer le repli en cas d'erreur pour tout l'Agent comme ceci :
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
    model=model,
    tools=[get_weather],
    system_prompt="Vous êtes un assistant de requête météo.",
)

result = agent.invoke({
    "messages": [HumanMessage(content="Demander la météo dans le Territoire du Nord")]
})
print("\n=== L'Agent reçoit le message d'erreur converti par handle_tool_error et l'explique à l'utilisateur en conséquence ===)
print(result["messages"][-1].content[:200])

Résultat de l'exécution :

handle_tool_error=True: 未收录城市 '北境'。可使用城市:杭州, 北京, 上海。请使用中文城市全称。

=== Agent 收到 handle_tool_error 转换后的错误信息,并据此向用户解释 ===
抱歉,我没有查询到"北境"的天气数据。目前支持查询的城市有:杭州、北京、上海。
请问您是想查询其中哪一个城市呢?
handle_tool_errorComportementScénarios applicables
False (par défaut)ToolException est levée vers le haut comme d'habitude ; l'appelant doit la gérer avec try/exceptErreurs irrécupérables nécessitant d'interrompre le flux
TrueIntercepter ToolException et transmettre son contenu au modèle comme valeur de retour normale de l'outilLorsque vous voulez que l'Agent lise lui-même le message d'erreur et corrige/réessaie
strAprès avoir intercepté l'exception, remplacer le message d'erreur par une chaîne fixe spécifiéeNe pas exposer les détails spécifiques de l'erreur ; donner uniquement une invite unifiée
Callable[[ToolException], str]Après avoir intercepté l'exception, utiliser une fonction personnalisée pour la traiter et générer le contenu de retourBesoin de personnaliser différents prompts selon le type/contenu de l'exception

Remarque :N'utilisez pastool.with_config(handle_tool_errors=True)cette syntaxe —with_config()Il s'agit d'une méthode de configuration d'exécution générique pour Runnable, utilisée uniquement pour définir des callbacks, des tags, des métadonnées, etc., et qui ne modifie pas réellement le comportement de gestion des erreurs de l'outil. Pour contrôler la gestion des erreurs d'un seul outil, utilisez@tool(handle_tool_error=...)(au singulier) ; si vous souhaitez configurer uniformément les stratégies de gestion des erreurs pour tous les outils au niveau Agent/Graph, vous définissez handle_tool_errors (au pluriel) sur le ToolNode sous-jacent, plutôt que sur des objets outil individuels. Les deux noms de champ sont similaires mais opèrent à des niveaux différents ; faites attention à la distinction lorsque vous les utilisez.


Exemple complet — Agent avec gestion des erreurs

Exemple

from langchain.tools import tool, ToolException
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.messages import HumanMessage


@tool(handle_tool_error=True)
def book_course(user_name: str, course_name: str) -> str:
    """Réserver un cours sur EXAMPLE pour l'utilisateur.

    Arguments :
user_name: nom de l'utilisateur
course_name: nom du cours
    """

    # Vérifier si l'utilisateur existe
    valid_users = {"Zhang San", "Li Si", "Wang Wu"}
    if user_name not in valid_users:
        raise ToolException(
            f"L'utilisateur '{user_name}' n'existe pas."
            f"Utilisateurs valides : {', '.join(sorted(valid_users))}"
        )

    # Vérifier si le cours existe
    valid_courses = {"Tutoriel de base Python3", "Tutoriel de base HTML", "Java orienté objet"}
    if course_name not in valid_courses:
        raise ToolException(
            f"Le cours '{course_name}' n'existe pas."
            f"Cours valides : {', '.join(sorted(valid_courses))}"
        )

    return f"Réservation réussie de {course_name} pour {user_name}"


model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
    model=model,
    tools=[book_course],
    system_prompt="Vous êtes un consultant de cours chez EXAMPLE.",
)

# Appel normal
result = agent.invoke({
    "messages": [HumanMessage(content="Réserver Tutoriel de base Python3 pour Zhang San")]
})
print(f"Succès : {result['messages'][-1].content[:100]}")

# Appel d'erreur : l'utilisateur n'existe pas
result = agent.invoke({
    "messages": [HumanMessage(content="Livre Tutoriel de base Python3 pour Zhao Liu")]
})
print(f"\n"Erreur - l'utilisateur n'existe pas :")
for msg in result["messages"]:
    if msg.type == "tool":
        print(f" [{msg.type}] {msg.content[:80]}")
    elif msg.type == "ai" and msg.content:
        print(f" [{msg.type}] {msg.content[:100]}")

Résultat de l'exécution :

Success: 已经为您成功预订《Python3 基础教程》,张三同学,祝您学习愉快!

错误-用户不存在:
  [tool] 用户 '赵六' 不存在。有效用户:张三, 李四, 王五
  [ai] 抱歉,系统中没有找到名为"赵六"的用户,暂时无法为您完成预订。
目前可预订的用户有:张三、李四、王五。请确认姓名后重新尝试。

On peut voir qu'après avoir configuré l'outil avechandle_tool_error=Truele contenu de ToolException apparaîtra normalement dans l'historique de conversation en tant que ToolMessage (la ligne [tool] dans la sortie ci-dessus) sans faire planter le programme ; après avoir vu ce message d'erreur, le modèle le transmettra à l'utilisateur en langage naturel et proposera des alternatives disponibles. C'est l'effet typique de la combinaison de return_direct, InjectedToolCallId, ToolException et handle_tool_error : cela garantit la robustesse des appels d'outils sans sacrifier l'expérience utilisateur.

Autres extensions