Características avanzadas de las herramientas de LangChain

En la sección anterior aprendimos el uso básico de @tool. Esta sección presenta las características avanzadas de las herramientas: return_direct, InjectedToolCallId, ToolException y manejo de errores.


return_direct: devolver directamente el resultado final

De forma predeterminada, después de que una herramienta se ejecuta, el resultado se devuelve al modelo, y el modelo genera la respuesta final basándose en el resultado de la herramienta. Pero a veces el resultado de la herramienta es en sí mismo la respuesta final que deseas.

Al configurarreturn_direct=Truereturn_direct=True, el ciclo del Agente termina inmediatamente después de que la herramienta se ejecuta, y el contenido devuelto por la herramienta se convierte directamente en la salida final.

Ejemplo

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


# Herramienta normal: el resultado se devuelve al modelo, y el modelo hace un resumen
@tool
def search_normal(keyword: str) -> str:
    """Buscar cursos de Ejemplo (modo normal)"""
    return f"Resultados de búsqueda: Tutorial básico de Python3, Análisis de datos con Python, Introducción al scraping con Python"


# Herramienta return_direct: el resultado se usa directamente como salida final
@tool(return_direct=True)
def search_direct(keyword: str) -> str:
    """Buscar cursos de Ejemplo (modo de devolución directa).

Usa esta herramienta cuando el usuario solo necesite los resultados de búsqueda, sin análisis adicional.
    """

    return f"Resultados de búsqueda: Tutorial básico de Python3, Análisis de datos con Python, Introducción al scraping con Python"


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

# Comparación: modo normal vs modo de devolución directa
agent_normal = create_agent(
    model=model,
    tools=[search_normal],
    system_prompt="Eres el asesor de aprendizaje de Ejemplo.",
)

agent_direct = create_agent(
    model=model,
    tools=[search_direct],
    system_prompt="Eres el asesor de aprendizaje de Ejemplo.",
)

# Modo normal: el modelo genera un resumen basado en los resultados de búsqueda
result = agent_normal.invoke({
    "messages": [HumanMessage(content="Buscar cursos de Python")]
})
print("=== Modo normal (el modelo procesa de nuevo) ===")
print(result["messages"][-1].content[:150])

# Modo de devolución directa: el resultado de la herramienta es la respuesta final
result = agent_direct.invoke({
    "messages": [HumanMessage(content="Buscar cursos de Python")]
})
print("\n"=== Modo de devolución directa (el resultado de la herramienta es la respuesta final) ===")
print(result["messages"][-1].content[:150])

Resultado de la ejecución:

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

=== 直接返回模式(工具结果即最终答案)===
搜索结果:Python3 基础教程、Python 数据分析、Python 爬虫入门
ModoDespués de ejecutar la herramientaEscenario de aplicación
return_direct=False (predeterminado)El modelo recibe el resultado de la herramienta → el modelo continúa razonando → genera la respuesta finalRequiere análisis/resumen/toma de decisiones adicionales
return_direct=TrueFinaliza inmediatamente después de ejecutar la herramienta → el resultado de la herramienta es la salida finalConsultas, obtención de datos, resultados ya formateados

Cuando configuras return_direct=True, el Agente omite los pasos posteriores de razonamiento del modelo y devuelve directamente el resultado de la herramienta. Esto es muy valioso para ahorrar tokens y reducir la latencia, pero también significa que el modelo no realizará ningún procesamiento adicional sobre el resultado de la herramienta.

Nota:Si un Agente tiene varias herramientas montadas a la vez, algunas con return_direct=True y otras normales, en cuanto el modelo active cualquier herramienta return_direct en esa ronda de llamadas, el ciclo del Agente finalizará de inmediato — incluso si en la misma ronda también se invocaron en paralelo otras herramientas normales, sus resultados ya no serán procesados ni resumidos por el modelo. Ten esto en cuenta al diseñar un Agente con múltiples herramientas para evitar que 'el contenido que debía resumirse se omita'.


InjectedToolCallId: obtener el ID de llamada de herramienta

A veces una herramienta necesita saber 'quién la llamó' —InjectedToolCallIdPuedes inyectar el tool_call_id actual en la función de la herramienta:

Ejemplo

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:
    """Registrar las acciones del usuario en el sistema de registros.

    Args:
action: descripción de la acción del usuario
tool_call_id: ID de llamada de herramienta inyectado automáticamente por el sistema
    """


    # En un proyecto real, aquí se escribiría en la base de datos o se enviaría al servicio de registros

    return f"Operación registrada (ID de llamada: {tool_call_id}): {action}"


# Simular manualmente un ToolCall generado por el modelo
tool_call: ToolCall = {
    "name": "log_user_action",
    "args": {
        "action": "El usuario consultó cursos de Python"
    },
    "id": "call_log_001",
    "type": "tool_call",
}


# InjectedToolCallId se inyecta automáticamente desde tool_call.id
result = log_user_action.invoke(tool_call)

print(result)

Resultado de la ejecución:

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

Los parámetros marcados conInjectedToolArgno necesitan ser proporcionados por el Agente (modelo); estos parámetros serán inyectados automáticamente por el runtime de LangChain al ejecutar la herramienta. Como estos parámetros no aparecen en el schema de la herramienta, el modelo no puede verlos, por lo tanto no deben describirse como parámetros de herramienta que el usuario necesita completar.

Por ejemplo,InjectedToolCallIdes un parámetro de inyección especial; LangChain pasa automáticamente el ID único del ToolCall actual durante la fase de ejecución de la herramienta. No aparecerá en los parámetros generados por el modelo, pero se vinculará automáticamente con la llamada de herramienta actual.

Llamar directamente.invoke()y construir manualmenteToolCalles solo para demostrar cómo LangChain implementa el mecanismo de inyección de parámetros. En proyectos reales, normalmente no se pasa manualmente este ID.

En el flujo de trabajo del Agente,InjectedToolCallIdse usa más comúnmente en escenarios que requieren asociar el contexto de la llamada actual, por ejemplo junto conCommandel objeto para actualizar el estado del Agente dentro de la herramienta,messagesagregar a la lista el elemento asociado a la llamada actual,ToolMessageo implementar funciones como seguimiento de llamadas a herramientas, auditoría y correlación de registros.

Esta es también la razón por la que fue diseñado como 'parámetro inyectado' en lugar de un parámetro normal: representa el contexto del ToolCall que el runtime de LangChain está ejecutando actualmente, no una parte de la entrada del usuario.


ToolException: manejo de excepciones de herramientas

Durante la ejecución de la herramienta pueden ocurrir errores. UsaToolExceptionpara lanzar una excepción de herramienta clara y que el Agente sepa que algo ha fallado.

Ejemplo

from langchain.tools import tool, ToolException

@tool
def get_user_info(user_id: int) -> str:
    """Consultar la información del usuario según el ID de usuario.

    Args:
user_id: ID de usuario, debe ser un entero positivo
    """

    # Validación de datos
    if user_id <= 0:
        # Lanzar ToolException, no una Exception normal
        # ToolException será capturada por el Agente y notificada al modelo
        raise ToolException(f"El ID de usuario debe ser un entero positivo, se recibió: {user_id}")

    # Simular la consulta a la base de datos
    users = {
        1: "Zhang San (miembro VIP, registrado el 2024-01-15)",
        2: "Li Si (usuario normal, registrado el 2024-03-20)",
    }

    if user_id not in users:
        raise ToolException(f"No se encontró un usuario con el ID {user_id}")

    return users[user_id]


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

# Llamada anómala 1: ID no válido
try:
    get_user_info.invoke({"user_id": -1})
except ToolException as e:
    print(f"Excepción de herramienta: {e}")

# Llamada anómala 2: el usuario no existe
try:
    get_user_info.invoke({"user_id": 999})
except ToolException as e:
    print(f"Excepción de herramienta: {e}")

Resultado de la ejecución:

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

La razón por la que aquí puedes capturar ToolException con try/except fuera de .invoke() es que el valor predeterminado de la herramientahandle_tool_errores False: la excepción no es absorbida por la propia herramienta, sino que se lanza hacia arriba con normalidad. Si deseas que la herramienta 'soporte' el error por sí misma, convirtiendo el mensaje de error en una cadena y devolviéndolo al modelo en lugar de lanzar una excepción, eso es handle_tool_error, que se explicará en la siguiente sección.


handle_tool_error: que la herramienta maneje sus propios errores

Cuando quieras que una herramienta no interrumpa el programa al fallar, sino que convierta el mensaje de error en un texto y lo entregue al modelo como un valor de retorno normal para que el modelo lo entienda y corrija, puedes configurarhandle_tool_error(nota: es singular, sin 's'). Esta es una propiedad de BaseTool; la forma más sencilla de configurarla es escribirla directamente en el decorador @tool:

Ejemplo

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: cuando ocurra un error, ya no se lanza una excepción,
# sino que el contenido de ToolException se convierte en una cadena y se devuelve normalmente al modelo
@tool(handle_tool_error=True)
def get_weather(city: str) -> str:
    """Consultar el clima de una ciudad especificada.

    Args:
city: nombre de la ciudad, debe ser el nombre completo en chino, como "杭州", "北京"
    """

    weather_data = {
        "Hangzhou": "Despejado, 25°C",
        "Pekín": "Nublado, 18°C",
        Shanghái: "Llovizna, 22°C",
    }
    if city not in weather_data:
        # Lanzar ToolException cuando la ciudad no está en los datos
        raise ToolException(
            f"Ciudad '{city}' no incluida."
            f"Ciudades disponibles: {', '.join(weather_data.keys())}."
            f"Por favor, use el nombre completo de la ciudad en chino."
        )
    return f"Clima de {city}: {weather_data[city]}"


# Prueba: llamar con un nombre de ciudad incorrecto
# Cuando handle_tool_error=True, el mensaje de error se devuelve como resultado normal, en lugar de lanzar una excepción
result = get_weather.invoke({"city": "Norte"})
print(f"handle_tool_error=True: {result}")


# Si desea que todos los errores de las herramientas sean manejados uniformemente por el Agent (en lugar de configurarlos herramienta por herramienta),
# puede configurarlo a nivel de ToolNode utilizado internamente por create_agent.
# En versiones recientes de langchain, puede activar el respaldo de errores para todo el Agent de esta manera:
model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
    model=model,
    tools=[get_weather],
    system_prompt="Eres un asistente de consulta meteorológica.",
)

result = agent.invoke({
    "messages": [HumanMessage(content="Consulta el clima de Norte")]
})
print("\n=== El Agent recibe el mensaje de error transformado por handle_tool_error y se lo explica al usuario ===)
print(result["messages"][-1].content[:200])

Resultado de la ejecución:

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

=== Agent 收到 handle_tool_error 转换后的错误信息,并据此向用户解释 ===
抱歉,我没有查询到"北境"的天气数据。目前支持查询的城市有:杭州、北京、上海。
请问您是想查询其中哪一个城市呢?
handle_tool_errorComportamientoEscenario aplicable
False (predeterminado)ToolException se lanza normalmente hacia arriba; quien llama debe manejar con try/exceptErrores no recuperables, requieren interrumpir el flujo
TrueCapturar ToolException y entregar su contenido al modelo como valor de retorno normal de la herramientaSe espera que el Agent lea el mensaje de error y corrija/reintente
strDespués de capturar la excepción, reemplazar el mensaje de error con una cadena fija especificadaNo se quieren exponer los detalles específicos del error, solo se proporciona un aviso unificado
Callable[[ToolException], str]Después de capturar la excepción, usar una función personalizada para manejarla y generar el contenido de retornoSe necesitan avisos personalizados según el tipo/contenido de la excepción

Nota:No usetool.with_config(handle_tool_errors=True)esta forma de escribir—with_config()es un método de configuración de tiempo de ejecución general de Runnable; solo se usa para establecer callbacks, tags, metadata, etc., y no modifica realmente el comportamiento de manejo de errores de la herramienta. Para controlar el manejo de errores de una sola herramienta, use@tool(handle_tool_error=...)(singular); si desea configurar una estrategia unificada de manejo de errores para todas las herramientas a nivel de Agent/Gráfico (Graph), entonces establezca handle_tool_errors (plural) en el ToolNode subyacente, en lugar de aplicarlo a objetos de herramienta individuales. Los nombres de los campos son similares pero actúan en niveles diferentes; distíngalos al usarlos.


Ejemplo completo: Agente con manejo de errores

Ejemplo

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:
    """Reserva un curso de EJEMPLO para el usuario.

    Args:
user_name: nombre del usuario
course_name: nombre del curso
    """

    # Verificar si el usuario existe
    valid_users = {"Zhang San", "Li Si", "Wang Wu"}
    if user_name not in valid_users:
        raise ToolException(
            f"El usuario '{user_name}' no existe."
            f"Usuarios válidos: {', '.join(sorted(valid_users))}"
        )

    # Verificar si el curso existe
    valid_courses = {"Tutorial básico de Python3", "Tutorial básico de HTML", "Java orientado a objetos"}
    if course_name not in valid_courses:
        raise ToolException(
            f"El curso '{course_name}' no existe."
            f"Cursos válidos: {', '.join(sorted(valid_courses))}"
        )

    return f"Se ha reservado con éxito《{course_name}》para {user_name}"


model = init_chat_model("deepseek:deepseek-v4-flash", temperature=0)
agent = create_agent(
    model=model,
    tools=[book_course],
    system_prompt="Eres el asesor de cursos de EJEMPLO.",
)

# Llamada normal
result = agent.invoke({
    "messages": [HumanMessage(content="Reserva para Zhang San el Tutorial básico de Python3")]
})
print(f"Éxito: {result['messages'][-1].content[:100]}")

# Llamada con error: el usuario no existe
result = agent.invoke({
    "messages": [HumanMessage(content="Reserva para Zhao Liu el Tutorial básico de Python3")]
})
print(f"\n"Error: el usuario no existe:")
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]}")

Resultado de la ejecución:

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

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

Se puede ver que la herramienta tiene configuradohandle_tool_error=Truedespués, el contenido de ToolException aparecerá normalmente en el historial de la conversación como un mensaje ToolMessage (la línea [tool] en la salida anterior), sin que el programa se bloquee; al ver este mensaje de error, el modelo lo transmitirá al usuario en lenguaje natural y ofrecerá alternativas disponibles. Este es el efecto típico de combinar return_direct, InjectedToolCallId, ToolException y handle_tool_error: garantiza la robustez de la llamada a la herramienta sin sacrificar la experiencia del usuario.

Otras extensiones