Módulo 2: Tools y Tool Calling

bind_tools y el Flujo de Tool Calling

Descripción de la cápsula

Ya sabes crear tools con el decorador @tool — funciones Python con nombre, descripción y schema de argumentos que un modelo puede entender. Pero crear tools no sirve de nada si no las conectas a un modelo. ¿Cómo le dices al modelo "estas son las herramientas que tienes disponibles"?

La respuesta es bind_tools(). Este método toma una lista de tools y las "adjunta" al modelo, creando una nueva instancia que sabe qué herramientas existen y cuándo usarlas. Lo que el modelo retorna cuando decide usar una tool no es el resultado de la ejecución — es una instrucción: "quiero llamar la tool X con los argumentos Y". Esa instrucción viene dentro del AIMessage como una lista de tool_calls.

Este es un punto que muchas personas no entienden al principio: el modelo NO ejecuta tools. Solo dice qué quiere llamar. Tú eres quien ejecuta la herramienta y devuelve el resultado. En esta cápsula vas a entender ese flujo completo, cómo inspeccionar tool_calls, y cómo controlar el comportamiento del modelo con tool_choice.


bind_tools(): conectar herramientas a un modelo

bind_tools() toma una lista de tools y retorna un nuevo modelo que las conoce. El modelo original no se modifica — obtienes una versión nueva con las tools adjuntas.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    return f"El clima en {city} es soleado, 22°C"

@tool
def calculator(expression: str) -> str:
    """Calcula una expresión matemática."""
    return str(eval(expression))

model = init_chat_model("openai:gpt-4.1-mini")

model_with_tools = model.bind_tools([get_weather, calculator])

response = model_with_tools.invoke("¿Qué clima hace en Madrid?")
print(response.tool_calls)
# [{'name': 'get_weather', 'args': {'city': 'Madrid'}, 'id': 'call_abc123'}]
print(response.content)
# '' (vacío cuando hay tool_calls)

¿Qué pasó aquí?

  1. Creaste dos tools: get_weather y calculator
  2. Llamaste model.bind_tools([get_weather, calculator]) para crear un modelo que conoce ambas herramientas
  3. Invocaste el modelo con una pregunta sobre el clima
  4. El modelo no respondió con texto — respondió con una instrucción: "llama a get_weather con city='Madrid'"
  5. Esa instrucción vive en response.tool_calls

El modelo original (model) sigue intacto. Puedes usar model sin tools y model_with_tools con tools en la misma aplicación.


El modelo NO ejecuta tools

Este es el concepto más importante de esta cápsula y el error más común de principiantes.

Cuando llamas a model_with_tools.invoke("¿Qué clima hace en Madrid?"), el modelo no llama a la función get_weather. No accede a ninguna API. No ejecuta código. Lo único que hace es retornar un mensaje que dice:

"Creo que deberías llamar a la función get_weather con el argumento city='Madrid'."

Eso es todo. La ejecución es tu responsabilidad. Este diseño es intencional:

  • Seguridad — Tú decides si ejecutar la tool o no. Puedes validar argumentos, aplicar rate limiting, o pedir confirmación al usuario
  • Control — Puedes interceptar, modificar o rechazar tool calls antes de ejecutarlos
  • Flexibilidad — La misma tool call puede ejecutarse de diferentes maneras según el contexto
  • Debugging — Puedes inspeccionar exactamente qué quiere hacer el modelo antes de que pase nada
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool

@tool
def delete_user(user_id: str) -> str:
    """Elimina un usuario del sistema."""
    return f"Usuario {user_id} eliminado"

model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools([delete_user])

response = model_with_tools.invoke("Elimina al usuario 42")
print(response.tool_calls)
# [{'name': 'delete_user', 'args': {'user_id': '42'}, 'id': 'call_xyz789'}]

# El usuario NO fue eliminado. Solo tienes la instrucción.
# Tú decides si ejecutarla o no.

Inspeccionar AIMessage.tool_calls

Cuando el modelo decide llamar una o más tools, la respuesta (AIMessage) contiene una lista tool_calls. Cada elemento es un diccionario con tres campos:

CampoTipoDescripción
namestrNombre de la tool que el modelo quiere llamar
argsdictArgumentos que el modelo quiere pasar a la tool
idstrIdentificador único del tool call (necesario para el ToolMessage de respuesta)
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    return f"El clima en {city} es soleado, 22°C"

@tool
def calculator(expression: str) -> str:
    """Calcula una expresión matemática."""
    return str(eval(expression))

model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools([get_weather, calculator])

response = model_with_tools.invoke("¿Qué clima hace en Tokio?")

print(f"Cantidad de tool calls: {len(response.tool_calls)}")
# 1

for tc in response.tool_calls:
    print(f"  Tool: {tc['name']}")
    print(f"  Args: {tc['args']}")
    print(f"  ID:   {tc['id']}")
# Output:
#   Tool: get_weather
#   Args: {'city': 'Tokio'}
#   ID:   call_abc123

Cuando NO hay tool calls

Si el modelo decide que no necesita ninguna herramienta (porque puede responder directamente), tool_calls estará vacío y content tendrá la respuesta en texto:

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    return f"El clima en {city} es soleado, 22°C"

model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools([get_weather])

response = model_with_tools.invoke("¿Cuál es la capital de Francia?")
print(f"tool_calls: {response.tool_calls}")
# tool_calls: []
print(f"content: {response.content}")
# content: La capital de Francia es París.

La pregunta "¿Cuál es la capital de Francia?" no requiere ninguna herramienta — el modelo lo sabe directamente. Así que responde con texto normal.

Regla clave

Situacióncontenttool_calls
El modelo responde directamenteTexto de respuesta[] (lista vacía)
El modelo quiere llamar tools"" (vacío o breve)Lista con uno o más tool calls

tool_choice: controlar cuándo el modelo usa tools

Por defecto, el modelo decide autónomamente si usar una tool o no. Pero puedes controlar ese comportamiento con el parámetro tool_choice en bind_tools().

"auto" — el modelo decide (default)

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    return f"El clima en {city} es soleado, 22°C"

@tool
def calculator(expression: str) -> str:
    """Calcula una expresión matemática."""
    return str(eval(expression))

model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools(
    [get_weather, calculator],
    tool_choice="auto"
)

response = model_with_tools.invoke("Hola, ¿cómo estás?")
print(f"tool_calls: {response.tool_calls}")
# tool_calls: [] (no necesita tools para saludar)
print(f"content: {response.content}")
# content: ¡Hola! Estoy bien, gracias. ¿En qué puedo ayudarte?

Con "auto", el modelo evalúa cada pregunta y decide si alguna tool es relevante. Si no, responde con texto normal.

"any" — forzar que use alguna tool

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    return f"El clima en {city} es soleado, 22°C"

@tool
def calculator(expression: str) -> str:
    """Calcula una expresión matemática."""
    return str(eval(expression))

model = init_chat_model("openai:gpt-4.1-mini")
model_forced = model.bind_tools(
    [get_weather, calculator],
    tool_choice="any"
)

response = model_forced.invoke("Hola, ¿cómo estás?")
print(f"tool_calls: {response.tool_calls}")
# tool_calls: [{'name': 'get_weather', 'args': {'city': '...'}, 'id': '...'}]
# El modelo fue FORZADO a llamar alguna tool, incluso cuando no tenía sentido

Con "any", el modelo siempre genera al menos un tool call. Incluso si la pregunta no tiene nada que ver con las tools disponibles. Úsalo con cuidado — es útil cuando sabes que toda interacción debe resultar en una tool call.

Forzar una tool específica

Puedes pasar el nombre de una tool para que el modelo siempre la llame:

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    return f"El clima en {city} es soleado, 22°C"

@tool
def calculator(expression: str) -> str:
    """Calcula una expresión matemática."""
    return str(eval(expression))

model = init_chat_model("openai:gpt-4.1-mini")
model_weather_only = model.bind_tools(
    [get_weather, calculator],
    tool_choice="get_weather"
)

response = model_weather_only.invoke("¿Cuánto es 2 + 2?")
print(f"tool_calls: {response.tool_calls}")
# [{'name': 'get_weather', 'args': {'city': '...'}, 'id': '...'}]
# Forzó get_weather aunque la pregunta era de cálculo

Forzar una tool específica es útil para pipelines donde sabes exactamente qué tool debe ejecutarse — por ejemplo, un endpoint de API que siempre debe llamar a una tool de extracción de datos.


Comparación: tool_choice "auto" vs "any" vs específico

tool_choiceComportamientoCuándo usarlo
"auto" (default)El modelo decide si usar tools o responder con textoChat conversacional, asistentes generales
"any"El modelo DEBE llamar al menos una toolPipelines donde toda interacción requiere una tool
"nombre_tool"El modelo DEBE llamar esa tool específicaEndpoints dedicados, flujos determinísticos

Ejemplo comparativo

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    return f"El clima en {city} es soleado, 22°C"

@tool
def calculator(expression: str) -> str:
    """Calcula una expresión matemática."""
    return str(eval(expression))

model = init_chat_model("openai:gpt-4.1-mini")
tools = [get_weather, calculator]
question = "¿Cuál es la capital de Francia?"

for choice in ["auto", "any"]:
    m = model.bind_tools(tools, tool_choice=choice)
    r = m.invoke(question)
    has_calls = len(r.tool_calls) > 0
    print(f"tool_choice='{choice}': tool_calls={has_calls}, content='{r.content[:50]}'")
# Output:
# tool_choice='auto': tool_calls=False, content='La capital de Francia es París.'
# tool_choice='any': tool_calls=True, content=''

Con "auto", el modelo responde directamente porque no necesita tools. Con "any", se ve forzado a generar un tool call innecesario.


Múltiples tools en bind_tools

Puedes pasar cualquier número de tools a bind_tools(). El modelo lee las descripciones de cada una y decide cuál usar según la pregunta:

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    return f"El clima en {city} es soleado, 22°C"

@tool
def calculator(expression: str) -> str:
    """Calcula una expresión matemática."""
    return str(eval(expression))

@tool
def search_web(query: str) -> str:
    """Busca información en la web."""
    return f"Resultados para '{query}': [resultado 1, resultado 2]"

@tool
def translate(text: str, target_language: str) -> str:
    """Traduce texto a otro idioma."""
    return f"Traducción de '{text}' a {target_language}: [traducción]"

model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools([get_weather, calculator, search_web, translate])

questions = [
    "¿Qué clima hace en Buenos Aires?",
    "¿Cuánto es 15 * 37?",
    "Busca información sobre LangChain",
    "Traduce 'hello world' al español",
]

for q in questions:
    response = model_with_tools.invoke(q)
    if response.tool_calls:
        tc = response.tool_calls[0]
        print(f"Q: {q}")
        print(f"   → Tool: {tc['name']}, Args: {tc['args']}")
    else:
        print(f"Q: {q}")
        print(f"   → Texto: {response.content[:60]}")
    print()
# Output:
# Q: ¿Qué clima hace en Buenos Aires?
#    → Tool: get_weather, Args: {'city': 'Buenos Aires'}
#
# Q: ¿Cuánto es 15 * 37?
#    → Tool: calculator, Args: {'expression': '15 * 37'}
#
# Q: Busca información sobre LangChain
#    → Tool: search_web, Args: {'query': 'LangChain'}
#
# Q: Traduce 'hello world' al español
#    → Tool: translate, Args: {'text': 'hello world', 'target_language': 'español'}

El modelo elige la tool correcta basándose en la descripción ("""docstring""") de cada una. Por eso es fundamental que las descripciones sean claras y específicas — son las instrucciones que el modelo usa para decidir.


Conexión con el proyecto

En el Asistente con Herramientas Externas (Cápsula 08), usarás bind_tools() para conectar al menos tres herramientas al modelo: weather API, web search y calculator. El modelo decidirá en cada turno de conversación qué tool usar (o si responder directamente).

El patrón base será:

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    return f"El clima en {city} es soleado, 22°C"

@tool
def calculator(expression: str) -> str:
    """Calcula una expresión matemática."""
    return str(eval(expression))

@tool
def search_web(query: str) -> str:
    """Busca información en la web."""
    return f"Resultados para '{query}': ..."

model = init_chat_model("openai:gpt-4.1-mini")
assistant = model.bind_tools([get_weather, calculator, search_web])

En la siguiente cápsula, aprenderás a ejecutar esos tool calls y devolver los resultados al modelo — completando el ciclo que hace que el asistente realmente funcione.


Troubleshooting

Problema 1: El modelo no llama a ninguna tool

Síntoma: response.tool_calls siempre está vacío, aunque la pregunta debería activar una tool. Causa: La descripción de la tool no es lo suficientemente clara para que el modelo la asocie con la pregunta. Solución: Mejora el docstring de la tool. El modelo usa la descripción para decidir cuándo llamarla.

# Mal — descripción vaga
@tool
def process(data: str) -> str:
    """Procesa datos."""
    return data

# Bien — descripción clara y específica
@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad, incluyendo temperatura y condiciones."""
    return f"El clima en {city} es soleado, 22°C"

Problema 2: El modelo llama la tool equivocada

Síntoma: El modelo elige calculator cuando debería elegir get_weather. Causa: Las descripciones de las tools se solapan o son ambiguas. Solución: Haz que cada descripción sea única y no ambigua. Si tienes tools similares, diferéncialas explícitamente.

# Mal — descripciones similares
@tool
def tool_a(query: str) -> str:
    """Busca información."""
    ...

@tool
def tool_b(query: str) -> str:
    """Encuentra datos."""
    ...

# Bien — descripciones diferenciadas
@tool
def search_web(query: str) -> str:
    """Busca información en internet usando un motor de búsqueda."""
    ...

@tool
def query_database(sql: str) -> str:
    """Ejecuta una consulta SQL en la base de datos interna."""
    ...

Problema 3: bind_tools() lanza error

Síntoma: TypeError o ValueError al llamar bind_tools(). Causa: Las tools no tienen el formato correcto. Cada tool necesita nombre, descripción, y un schema de argumentos válido. Solución: Asegúrate de usar el decorador @tool y que la función tenga un docstring y type hints:

# Mal — sin docstring ni type hints
@tool
def my_tool(x):
    return x

# Bien — con docstring y type hints
@tool
def my_tool(x: str) -> str:
    """Descripción clara de lo que hace la tool."""
    return x

Ejercicios

Ejercicio 1: bind_tools básico (Fácil)

Crea dos tools: convert_currency(amount: float, from_currency: str, to_currency: str) y get_exchange_rate(from_currency: str, to_currency: str). Usa bind_tools() para conectarlas a un modelo. Prueba con 3 preguntas diferentes y muestra qué tool eligió el modelo para cada una.

Ver solución
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool

@tool
def convert_currency(amount: float, from_currency: str, to_currency: str) -> str:
    """Convierte una cantidad de una moneda a otra."""
    rates = {"USD_EUR": 0.92, "EUR_USD": 1.09, "USD_MXN": 17.15, "MXN_USD": 0.058}
    key = f"{from_currency}_{to_currency}"
    rate = rates.get(key, 1.0)
    result = amount * rate
    return f"{amount} {from_currency} = {result:.2f} {to_currency}"

@tool
def get_exchange_rate(from_currency: str, to_currency: str) -> str:
    """Obtiene el tipo de cambio actual entre dos monedas."""
    rates = {"USD_EUR": 0.92, "EUR_USD": 1.09, "USD_MXN": 17.15}
    key = f"{from_currency}_{to_currency}"
    rate = rates.get(key, "desconocido")
    return f"1 {from_currency} = {rate} {to_currency}"

model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools([convert_currency, get_exchange_rate])

questions = [
    "Convierte 100 dólares a euros",
    "¿Cuál es el tipo de cambio de USD a MXN?",
    "¿Cuál es la capital de España?",
]

for q in questions:
    response = model_with_tools.invoke(q)
    if response.tool_calls:
        tc = response.tool_calls[0]
        print(f"Q: {q}")
        print(f"   Tool: {tc['name']}, Args: {tc['args']}")
    else:
        print(f"Q: {q}")
        print(f"   Respuesta directa: {response.content[:60]}")
    print()
# Output:
# Q: Convierte 100 dólares a euros
#    Tool: convert_currency, Args: {'amount': 100.0, 'from_currency': 'USD', 'to_currency': 'EUR'}
#
# Q: ¿Cuál es el tipo de cambio de USD a MXN?
#    Tool: get_exchange_rate, Args: {'from_currency': 'USD', 'to_currency': 'MXN'}
#
# Q: ¿Cuál es la capital de España?
#    Respuesta directa: La capital de España es Madrid.

Explicación: El modelo distingue entre "convertir" (que implica una cantidad) y "tipo de cambio" (solo las monedas). La tercera pregunta no activa ninguna tool porque no tiene relación con monedas.

Ejercicio 2: Inspeccionar tool_calls (Fácil)

Crea una función inspect_response(response) que reciba un AIMessage e imprima de forma detallada toda la información disponible: si hay tool_calls (nombre, args, id de cada uno), si hay content, y la metadata de uso. Pruébala con al menos 2 invocaciones diferentes (una que active tools y otra que no).

Ver solución
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain_core.messages import AIMessage

@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    return f"El clima en {city} es soleado, 22°C"

def inspect_response(response: AIMessage):
    print(f"{'=' * 50}")
    print(f"Tipo: {type(response).__name__}")
    print(f"Content: '{response.content[:80]}'" if response.content else "Content: (vacío)")

    if response.tool_calls:
        print(f"Tool calls: {len(response.tool_calls)}")
        for i, tc in enumerate(response.tool_calls):
            print(f"  [{i}] name: {tc['name']}")
            print(f"      args: {tc['args']}")
            print(f"      id:   {tc['id']}")
    else:
        print("Tool calls: ninguno")

    if response.usage_metadata:
        usage = response.usage_metadata
        print(f"Tokens: {usage['input_tokens']} in / {usage['output_tokens']} out")
    print(f"{'=' * 50}\n")

model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools([get_weather])

response1 = model_with_tools.invoke("¿Qué clima hace en Lima?")
inspect_response(response1)
# Output:
# ==================================================
# Tipo: AIMessage
# Content: (vacío)
# Tool calls: 1
#   [0] name: get_weather
#       args: {'city': 'Lima'}
#       id:   call_abc123
# Tokens: 45 in / 15 out
# ==================================================

response2 = model_with_tools.invoke("¿Qué es Python?")
inspect_response(response2)
# Output:
# ==================================================
# Tipo: AIMessage
# Content: 'Python es un lenguaje de programación de alto nivel...'
# Tool calls: ninguno
# Tokens: 12 in / 35 out
# ==================================================

Explicación: La función inspect_response te da una vista completa del AIMessage. Cuando hay tool calls, content suele estar vacío. Cuando no hay tool calls, content tiene la respuesta. Este patrón de inspección es útil para debugging.

Ejercicio 3: tool_choice comparativo (Medio)

Crea tres tools diferentes. Usa la misma pregunta con tool_choice="auto", tool_choice="any" y tool_choice="nombre_de_tool". Imprime el resultado de cada configuración para comparar el comportamiento.

Ver solución
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    return f"El clima en {city} es soleado, 22°C"

@tool
def calculator(expression: str) -> str:
    """Calcula una expresión matemática."""
    return str(eval(expression))

@tool
def translate(text: str, target_language: str) -> str:
    """Traduce texto a otro idioma."""
    return f"Traducción: {text} → [{target_language}]"

model = init_chat_model("openai:gpt-4.1-mini")
tools = [get_weather, calculator, translate]

question = "¿Cuál es la capital de Japón?"

configs = [
    ("auto", "auto"),
    ("any", "any"),
    ("get_weather", "get_weather"),
    ("calculator", "calculator"),
]

for label, choice in configs:
    m = model.bind_tools(tools, tool_choice=choice)
    r = m.invoke(question)

    if r.tool_calls:
        tc = r.tool_calls[0]
        print(f"tool_choice='{label}': → {tc['name']}({tc['args']})")
    else:
        print(f"tool_choice='{label}': → Texto: '{r.content[:50]}'")
# Output:
# tool_choice='auto': → Texto: 'La capital de Japón es Tokio.'
# tool_choice='any': → get_weather({'city': 'Tokio'})
# tool_choice='get_weather': → get_weather({'city': 'Tokio'})
# tool_choice='calculator': → calculator({'expression': '...'})

Explicación: Con "auto", el modelo responde directamente porque no necesita tools. Con "any", se ve forzado a elegir alguna — y elige get_weather porque la pregunta menciona un lugar. Con el nombre específico, siempre llama esa tool sin importar la relevancia.

Ejercicio 4: Tool router (Medio)

Crea una función route_question(question) que use bind_tools con tool_choice="auto" y retorne un diccionario con type ("tool_call" o "direct_response"), tool_name (si aplica), tool_args (si aplica), y content (si aplica). Prueba con 5 preguntas variadas.

Ver solución
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Obtiene el clima actual de una ciudad."""
    return f"El clima en {city} es soleado, 22°C"

@tool
def calculator(expression: str) -> str:
    """Calcula una expresión matemática."""
    return str(eval(expression))

@tool
def search_web(query: str) -> str:
    """Busca información actualizada en internet."""
    return f"Resultados para '{query}': ..."

model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools([get_weather, calculator, search_web])

def route_question(question: str) -> dict:
    response = model_with_tools.invoke(question)

    if response.tool_calls:
        tc = response.tool_calls[0]
        return {
            "type": "tool_call",
            "tool_name": tc["name"],
            "tool_args": tc["args"],
            "content": None,
        }
    return {
        "type": "direct_response",
        "tool_name": None,
        "tool_args": None,
        "content": response.content,
    }

questions = [
    "¿Qué clima hace en Santiago?",
    "¿Cuánto es 1024 / 16?",
    "¿Qué es un microservicio?",
    "Busca las últimas noticias de AI",
    "Hola, ¿cómo estás?",
]

for q in questions:
    result = route_question(q)
    if result["type"] == "tool_call":
        print(f"[TOOL] {q}")
        print(f"       → {result['tool_name']}({result['tool_args']})")
    else:
        print(f"[TEXT] {q}")
        print(f"       → {result['content'][:60]}")
    print()
# Output:
# [TOOL] ¿Qué clima hace en Santiago?
#        → get_weather({'city': 'Santiago'})
#
# [TOOL] ¿Cuánto es 1024 / 16?
#        → calculator({'expression': '1024 / 16'})
#
# [TEXT] ¿Qué es un microservicio?
#        → Un microservicio es un patrón arquitectónico que divide...
#
# [TOOL] Busca las últimas noticias de AI
#        → search_web({'query': 'últimas noticias de AI'})
#
# [TEXT] Hola, ¿cómo estás?
#        → ¡Hola! Estoy bien, gracias. ¿En qué puedo ayudarte?

Explicación: La función route_question actúa como un router — determina si la pregunta requiere una tool o una respuesta directa. Este patrón es la base de cómo los agentes deciden qué hacer en cada turno.


Resumen

En esta cápsula aprendiste:

  • bind_tools() conecta una lista de tools a un modelo, creando una nueva instancia que las conoce
  • El modelo NO ejecuta tools — solo retorna instrucciones en tool_calls diciendo qué quiere llamar y con qué argumentos
  • AIMessage.tool_calls es una lista de dicts con name, args, e id — cada uno representa una solicitud de ejecución
  • tool_choice controla cuándo el modelo usa tools: "auto" (decide él), "any" (forzar alguna), o nombre específico
  • Cuando el modelo no necesita tools, tool_calls está vacío y content tiene la respuesta en texto
  • Las descripciones de las tools (docstrings) son críticas — el modelo las usa para decidir cuál llamar
  • Puedes pasar schemas Pydantic además de funciones @tool a bind_tools()

Próxima cápsula: Tool Execution Loop — aprenderás a ejecutar las tools que el modelo solicita, crear ToolMessage con los resultados, y completar el ciclo para que el modelo genere la respuesta final.


Recursos adicionales

  1. Tool Calling — LangChain Docs — Guía conceptual completa de tool calling
  2. How to use chat models to call tools — Tutorial paso a paso con ejemplos
  3. bind_tools API Reference — Referencia del método bind_tools
  4. OpenAI Function Calling — Implementación de OpenAI
  5. Anthropic Tool Use — Implementación de Anthropic
  6. Google Gemini Function Calling — Implementación de Google
  7. AIMessage API Reference — Referencia de AIMessage y tool_calls
  8. How to force a specific tool call — Guía de tool_choice

Módulo 2 — LangChain & LangGraph: From Chains to Agents