Módulo 2: Tools y Tool Calling

Structured Output con Tools

Descripción de la cápsula

En el Módulo 1 aprendiste with_structured_output — una forma de obtener respuestas tipadas usando Pydantic, TypedDict o JSON Schema. En este módulo has aprendido tool calling — cómo los modelos invocan funciones externas. Ahora vas a descubrir que estos dos conceptos están más conectados de lo que parece.

Tool calling no es solo para ejecutar acciones. Cuando defines una tool con un schema Pydantic detallado y el modelo la "llama", los args que genera son exactamente los datos estructurados que querías extraer. No necesitas ejecutar la tool — los argumentos ya son tu resultado. Este patrón se llama tool-based extraction: defines una "tool" que en realidad es solo un schema de datos, el modelo la "llama" rellenando los campos, y tú simplemente lees los args.

¿Por qué no usar with_structured_output siempre? Porque tool-based extraction tiene ventajas únicas: puedes mezclar tools reales (que ejecutan acciones) con tools de extracción (que solo recogen datos) en la misma conversación. Puedes extraer múltiples entidades usando parallel tool calls. Y puedes mantener el control total sobre cómo se procesan los resultados.


Tool calling como mecanismo de extracción

La idea central: una tool no tiene que hacer nada. Si defines una tool con un schema que describe los datos que quieres extraer, el modelo "llamará" esa tool con los datos como argumentos.

Ejemplo: extraer información de una persona

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from pydantic import BaseModel, Field

class Person(BaseModel):
    name: str = Field(description="Nombre completo de la persona")
    age: int = Field(description="Edad de la persona")
    occupation: str = Field(description="Ocupación profesional")

@tool(args_schema=Person)
def extract_person(name: str, age: int, occupation: str) -> dict:
    """Extrae información de una persona mencionada en el texto."""
    return {"name": name, "age": age, "occupation": occupation}

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

response = model_with_tools.invoke(
    "Juan García tiene 35 años y trabaja como ingeniero de software"
)

print(f"Tool calls: {len(response.tool_calls)}")
print(f"Datos extraídos: {response.tool_calls[0]['args']}")
# Output esperado:
# Tool calls: 1
# Datos extraídos: {'name': 'Juan García', 'age': 35, 'occupation': 'ingeniero de software'}

El modelo "llamó" extract_person con los datos extraídos del texto. No ejecutamos la tool — los args ya contienen toda la información.

¿Qué hace args_schema?

El parámetro args_schema en @tool le dice a LangChain que use tu modelo Pydantic como schema de argumentos. Esto te da:

  • ✅ Descripciones detalladas por campo vía Field(description=...)
  • ✅ Validaciones de Pydantic (tipos, constraints, valores por defecto)
  • ✅ Control total sobre cómo el modelo interpreta cada campo

with_structured_output vs tool-based extraction

Hagamos la misma extracción con ambos enfoques:

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from pydantic import BaseModel, Field

class MovieReview(BaseModel):
    title: str = Field(description="Título de la película")
    rating: int = Field(description="Rating de 1 a 10")
    summary: str = Field(description="Resumen en una frase")

text = "Inception es una obra maestra de Nolan. Le doy un 9. Un thriller sobre sueños dentro de sueños."

# Enfoque 1: with_structured_output
model = init_chat_model("openai:gpt-4.1-mini")
structured_model = model.with_structured_output(MovieReview)
result_structured = structured_model.invoke(f"Analiza esta reseña: {text}")
print(f"Structured Output: {result_structured}")
# Output: title='Inception' rating=9 summary='Un thriller sobre sueños dentro de sueños'

# Enfoque 2: tool-based extraction
@tool(args_schema=MovieReview)
def extract_review(title: str, rating: int, summary: str) -> dict:
    """Extrae datos de una reseña de película."""
    return {"title": title, "rating": rating, "summary": summary}

model_with_tools = model.bind_tools([extract_review])
response = model_with_tools.invoke(f"Analiza esta reseña: {text}")
result_tool = response.tool_calls[0]["args"]
print(f"Tool Extraction: {result_tool}")
# Output: {'title': 'Inception', 'rating': 9, 'summary': 'Un thriller sobre sueños dentro de sueños'}

Tabla de comparación

Característicawith_structured_outputTool-based extraction
Tipo de resultadoObjeto Pydantic (result.field)Dict (tool_calls[0]["args"]["field"])
Validación automática✅ Pydantic valida el resultado❌ Los args son un dict sin validar
Mezclar con tools reales❌ No — solo retorna el schema✅ Sí — tools reales + extracción
Extraer múltiples entidades⚠️ Necesitas wrapper list[Entity]✅ Parallel tool calls naturales
Streaming❌ No soportado directamente✅ Streaming de tool call chunks
Simplicidad✅ Más simple⚠️ Más pasos
Control del flujo❌ Limitado✅ Total

¿Cuándo usar cada enfoque?

  • with_structured_output cuando solo necesitas extraer datos, no tienes tools reales, y quieres máxima simplicidad
  • Tool-based extraction cuando mezclas extracción con tools reales, necesitas parallel calls para múltiples entidades, o necesitas streaming

Extracción simple: una entidad

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from pydantic import BaseModel, Field
from typing import Optional

class Event(BaseModel):
    name: str = Field(description="Nombre del evento")
    date: str = Field(description="Fecha del evento en formato YYYY-MM-DD")
    location: str = Field(description="Lugar del evento")
    attendees: Optional[int] = Field(
        description="Número de asistentes si se menciona. None si no se indica."
    )

@tool(args_schema=Event)
def extract_event(name: str, date: str, location: str, attendees: Optional[int] = None) -> dict:
    """Extrae información de un evento mencionado en el texto."""
    return {"name": name, "date": date, "location": location, "attendees": attendees}

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

text = """
La conferencia PyCon España 2025 se celebrará el 15 de octubre en el 
Palacio de Congresos de Madrid. Se esperan más de 1500 asistentes.
"""

response = model_with_tools.invoke(f"Extrae el evento del siguiente texto: {text}")
event = response.tool_calls[0]["args"]

print(f"Evento: {event['name']}")
print(f"Fecha: {event['date']}")
print(f"Lugar: {event['location']}")
print(f"Asistentes: {event['attendees']}")
# Output esperado:
# Evento: PyCon España 2025
# Fecha: 2025-10-15
# Lugar: Palacio de Congresos de Madrid
# Asistentes: 1500

Las descripciones en Field() son instrucciones para el modelo: "Fecha del evento en formato YYYY-MM-DD" indica el formato exacto que esperas.


Extracción múltiple: parallel tool calls

Una ventaja exclusiva de tool-based extraction: extraer múltiples entidades usando parallel tool calls. El modelo "llama" la tool una vez por cada entidad.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from pydantic import BaseModel, Field

class Person(BaseModel):
    name: str = Field(description="Nombre completo")
    role: str = Field(description="Rol o cargo en la organización")
    department: str = Field(description="Departamento al que pertenece")

@tool(args_schema=Person)
def extract_person(name: str, role: str, department: str) -> dict:
    """Extrae información de una persona mencionada en el texto."""
    return {"name": name, "role": role, "department": department}

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

text = """
El equipo de liderazgo incluye a María López como VP de Ingeniería en
Tecnología, Carlos Ruiz como Director de Producto, y Ana Martínez
como Head of Design en Diseño.
"""

response = model_with_tools.invoke(
    f"Extrae TODAS las personas mencionadas: {text}"
)

print(f"Personas encontradas: {len(response.tool_calls)}")
for i, tc in enumerate(response.tool_calls):
    p = tc["args"]
    print(f"  {i+1}. {p['name']}{p['role']} ({p['department']})")
# Output esperado:
# Personas encontradas: 3
#   1. María López — VP de Ingeniería (Tecnología)
#   2. Carlos Ruiz — Director de Producto (Producto)
#   3. Ana Martínez — Head of Design (Diseño)

El modelo genera 3 tool calls en paralelo — una por persona. Más natural que definir un wrapper list[Person] en with_structured_output.


Combinar tools reales con tools de extracción

El poder real aparece cuando combinas tools que ejecutan acciones con tools que solo extraen datos.

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 HumanMessage, ToolMessage
from pydantic import BaseModel, Field

@tool
def search_web(query: str) -> str:
    """Busca información en la web."""
    results = {
        "PyCon España 2025": "PyCon España 2025 será en Madrid, 15 de octubre. "
                             "Speakers: Guido van Rossum y Carol Willing.",
    }
    return results.get(query, f"Sin resultados para: {query}")

class EventSummary(BaseModel):
    event_name: str = Field(description="Nombre del evento")
    date: str = Field(description="Fecha en formato YYYY-MM-DD")
    location: str = Field(description="Ciudad o lugar")
    speakers: list[str] = Field(description="Lista de speakers mencionados")

@tool(args_schema=EventSummary)
def extract_event_summary(
    event_name: str, date: str, location: str, speakers: list[str],
) -> dict:
    """Extrae un resumen estructurado de un evento."""
    return {"event_name": event_name, "date": date, "location": location, "speakers": speakers}

tools = [search_web, extract_event_summary]
tools_by_name = {t.name: t for t in tools}

model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools(tools)

messages = [HumanMessage(
    content="Busca info sobre PyCon España 2025 y extrae un resumen estructurado"
)]

# Ronda 1: el modelo busca
ai_msg = model_with_tools.invoke(messages)
messages.append(ai_msg)

for tc in ai_msg.tool_calls:
    print(f"Ronda 1 → {tc['name']}({tc['args']})")
    if tc["name"] in tools_by_name:
        result = tools_by_name[tc["name"]].invoke(tc["args"])
        messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))

# Ronda 2: el modelo extrae datos estructurados
ai_msg2 = model_with_tools.invoke(messages)
messages.append(ai_msg2)

if ai_msg2.tool_calls:
    for tc in ai_msg2.tool_calls:
        print(f"Ronda 2 → {tc['name']}")
        if tc["name"] == "extract_event_summary":
            for key, value in tc["args"].items():
                print(f"  {key}: {value}")
        else:
            result = tools_by_name[tc["name"]].invoke(tc["args"])
            messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
# Output esperado:
# Ronda 1 → search_web({'query': 'PyCon España 2025'})
# Ronda 2 → extract_event_summary
#   event_name: PyCon España 2025
#   date: 2025-10-15
#   location: Madrid
#   speakers: ['Guido van Rossum', 'Carol Willing']

El modelo usa search_web para obtener información real, y luego extract_event_summary para estructurar los datos encontrados.


Validar datos extraídos con Pydantic

Los args de un tool call son un diccionario sin validar. Para obtener las mismas garantías que with_structured_output, valida manualmente:

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from pydantic import BaseModel, Field, ValidationError

class Product(BaseModel):
    name: str = Field(description="Nombre del producto")
    price: float = Field(description="Precio en USD", ge=0)
    category: str = Field(description="Categoría: electronics, clothing, food")

@tool(args_schema=Product)
def extract_product(name: str, price: float, category: str) -> dict:
    """Extrae datos de un producto."""
    return {"name": name, "price": price, "category": category}

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

text = "El nuevo iPhone 16 Pro cuesta $1199 y está disponible en tiendas"
response = model_with_tools.invoke(f"Extrae el producto: {text}")

raw_args = response.tool_calls[0]["args"]

try:
    validated = Product(**raw_args)
    print(f"Nombre: {validated.name}")
    print(f"Precio: ${validated.price}")
    print(f"Categoría: {validated.category}")
except ValidationError as e:
    print(f"Error de validación: {e}")
# Output esperado:
# Nombre: iPhone 16 Pro
# Precio: $1199.0
# Categoría: electronics

Product(**raw_args) ejecuta todas las validaciones de Pydantic, incluyendo el constraint ge=0 en price.


Forzar extracción con tool_choice

Para garantizar que el modelo siempre use la tool de extracción y nunca responda con texto libre:

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from pydantic import BaseModel, Field

class Sentiment(BaseModel):
    text: str = Field(description="El texto analizado")
    label: str = Field(description="positivo, negativo o neutro")
    score: float = Field(description="Confianza entre 0.0 y 1.0")

@tool(args_schema=Sentiment)
def extract_sentiment(text: str, label: str, score: float) -> dict:
    """Extrae el sentimiento de un texto."""
    return {"text": text, "label": label, "score": score}

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

# Con tool_choice: el modelo SIEMPRE llama la tool
model_forced = model.bind_tools(
    [extract_sentiment],
    tool_choice="extract_sentiment"
)

response = model_forced.invoke("Analiza: Este producto es bastante bueno")
print(f"Resultado: {response.tool_calls[0]['args']}")
# Output esperado:
# Resultado: {'text': 'Este producto es bastante bueno', 'label': 'positivo', 'score': 0.8}

Conexión con el proyecto

En el Asistente con Herramientas Externas (Cápsula 08), combinarás tools reales (get_weather, search_web, calculate) con tools de extracción que estructuran los resultados para logging y monitoreo. Esto te permite mantener logs estructurados de cada interacción sin parsear texto libre.


Troubleshooting

Problema 1: El modelo responde en texto libre en vez de llamar la tool

Causa: El modelo no interpretó que debía usar la tool. Solución: Usa tool_choice para forzar la llamada:

model_with_tools = model.bind_tools([extract_tool], tool_choice="extract_tool")

Problema 2: Parallel tool calls extrae entidades duplicadas

Causa: El modelo a veces genera tool calls redundantes con textos ambiguos. Solución: Deduplica después de la extracción:

seen = set()
unique = []
for tc in response.tool_calls:
    key = tc["args"].get("name", "")
    if key not in seen:
        seen.add(key)
        unique.append(tc["args"])

Problema 3: Los args extraídos tienen tipos incorrectos

Causa: Los args son diccionarios sin validación de tipos. Solución: Valida con Pydantic:

try:
    validated = MySchema(**response.tool_calls[0]["args"])
except ValidationError as e:
    print(f"Error: {e}")

Problema 4: args_schema no se aplica

Causa: Los parámetros de la función no coinciden con los campos del schema. Solución: Parámetros y campos deben coincidir exactamente:

class Person(BaseModel):
    name: str
    age: int

@tool(args_schema=Person)
def extract(name: str, age: int) -> dict:  # Coinciden con Person
    """Extrae datos."""
    return {"name": name, "age": age}

Ejercicios

Ejercicio 1: Extracción básica con tool (Fácil)

Define un schema BookInfo con campos para título, autor, año y género. Crea una tool de extracción y úsala para extraer datos de una descripción de libro.

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 pydantic import BaseModel, Field

class BookInfo(BaseModel):
    title: str = Field(description="Título del libro")
    author: str = Field(description="Nombre del autor")
    year: int = Field(description="Año de publicación")
    genre: str = Field(description="Género literario")

@tool(args_schema=BookInfo)
def extract_book(title: str, author: str, year: int, genre: str) -> dict:
    """Extrae información de un libro mencionado en el texto."""
    return {"title": title, "author": author, "year": year, "genre": genre}

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

text = "Cien años de soledad, publicada en 1967 por García Márquez, es una novela de realismo mágico."
response = model_with_tools.invoke(f"Extrae los datos del libro: {text}")

book = response.tool_calls[0]["args"]
print(f"Título: {book['title']}")
print(f"Autor: {book['author']}")
print(f"Año: {book['year']}")
print(f"Género: {book['genre']}")
# Output esperado:
# Título: Cien años de soledad
# Autor: Gabriel García Márquez
# Año: 1967
# Género: realismo mágico

Explicación: tool_choice="extract_book" garantiza que el modelo siempre use la tool de extracción. Los args contienen los datos estructurados.

Ejercicio 2: Extracción múltiple con parallel calls (Fácil)

Usa parallel tool calls para extraer todas las tecnologías de una oferta de trabajo. Cada tecnología: nombre, categoría y nivel requerido.

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 pydantic import BaseModel, Field

class Technology(BaseModel):
    name: str = Field(description="Nombre de la tecnología")
    category: str = Field(description="Categoría: language, framework, database, tool")
    level: str = Field(description="Nivel: beginner, intermediate, advanced")

@tool(args_schema=Technology)
def extract_technology(name: str, category: str, level: str) -> dict:
    """Extrae una tecnología mencionada en el texto."""
    return {"name": name, "category": category, "level": level}

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

job = """
Buscamos fullstack con: Python avanzado para backend, React intermedio
para frontend, PostgreSQL avanzado, Docker nivel básico.
"""

response = model_with_tools.invoke(f"Extrae TODAS las tecnologías: {job}")

print(f"Tecnologías: {len(response.tool_calls)}")
for tc in response.tool_calls:
    t = tc["args"]
    print(f"  {t['name']} ({t['category']}) — {t['level']}")
# Output esperado:
# Tecnologías: 4
#   Python (language) — advanced
#   React (framework) — intermediate
#   PostgreSQL (database) — advanced
#   Docker (tool) — beginner

Explicación: El modelo genera 4 parallel tool calls, una por tecnología. Cada call contiene los datos extraídos.

Ejercicio 3: Comparar ambos enfoques (Medio)

Implementa la misma extracción de reseña de restaurante con with_structured_output y con tool-based extraction. Compara resultados y tipos.

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 pydantic import BaseModel, Field

class RestaurantReview(BaseModel):
    name: str = Field(description="Nombre del restaurante")
    rating: int = Field(description="Rating de 1 a 5")
    avg_price: float = Field(description="Precio promedio por persona en USD")
    recommendation: str = Field(description="Recomendación en una frase")

text = "Comimos en La Maison ayer. Le doy 5 estrellas. Unos $45 por persona."
model = init_chat_model("openai:gpt-4.1-mini")

# Enfoque A: with_structured_output
result_a = model.with_structured_output(RestaurantReview).invoke(f"Extrae: {text}")
print(f"=== with_structured_output ===")
print(f"  Tipo: {type(result_a).__name__} | {result_a.name} | {result_a.rating}★ | ${result_a.avg_price}")

# Enfoque B: tool-based extraction
@tool(args_schema=RestaurantReview)
def extract_review(name: str, rating: int, avg_price: float, recommendation: str) -> dict:
    """Extrae datos de una reseña."""
    return {"name": name, "rating": rating, "avg_price": avg_price, "recommendation": recommendation}

response = model.bind_tools([extract_review], tool_choice="extract_review").invoke(f"Extrae: {text}")
result_b = response.tool_calls[0]["args"]
print(f"\n=== Tool extraction ===")
print(f"  Tipo: {type(result_b).__name__} | {result_b['name']} | {result_b['rating']}★ | ${result_b['avg_price']}")

validated = RestaurantReview(**result_b)
print(f"  Validado: {validated.name}")
# Output esperado:
# === with_structured_output ===
#   Tipo: RestaurantReview | La Maison | 5★ | $45.0
#
# === Tool extraction ===
#   Tipo: dict | La Maison | 5★ | $45.0
#   Validado: La Maison

Explicación: Mismos datos, distinto tipo de retorno. with_structured_output retorna Pydantic directamente; tool extraction retorna dict que puedes validar manualmente.

Ejercicio 4: Combinar tools reales con extracción (Medio)

Crea un sistema con calculate(expression) como tool real y extract_math_problem como tool de extracción. El usuario describe un problema en lenguaje natural, el modelo identifica la operación y la calcula.

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 HumanMessage, ToolMessage

@tool
def calculate(expression: str) -> str:
    """Calcula una expresión matemática."""
    try:
        return f"{expression} = {eval(expression)}"
    except Exception as e:
        return f"Error: {e}"

tools = [calculate]
tools_by_name = {t.name: t for t in tools}

model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools(tools)

messages = [HumanMessage(
    content="Si compro 3 laptops a $899 cada una con 10% de descuento, ¿cuánto pago?"
)]

ai_msg = model_with_tools.invoke(messages)
messages.append(ai_msg)

for tc in ai_msg.tool_calls:
    print(f"→ {tc['name']}({tc['args']})")
    result = tools_by_name[tc["name"]].invoke(tc["args"])
    messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
    print(f"  Resultado: {result}")

final = model_with_tools.invoke(messages)
print(f"\n{final.content}")
# Output esperado:
# → calculate({'expression': '3 * 899 * 0.9'})
#   Resultado: 3 * 899 * 0.9 = 2427.3
#
# El total por 3 laptops con 10% de descuento es $2,427.30

Explicación: El modelo combina razonamiento con tool calling. Identifica la expresión matemática y usa calculate para resolverla.

Ejercicio 5: Pipeline extracción + enriquecimiento (Difícil)

Crea un pipeline que: (1) extraiga personas de un texto con tool extraction, (2) busque perfil de cada persona con una tool real, (3) combine los datos en un reporte final.

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 HumanMessage, ToolMessage
from pydantic import BaseModel, Field

class PersonInfo(BaseModel):
    name: str = Field(description="Nombre completo")
    role: str = Field(description="Rol o posición")

@tool(args_schema=PersonInfo)
def extract_person(name: str, role: str) -> dict:
    """Extrae información de una persona."""
    return {"name": name, "role": role}

@tool
def lookup_profile(name: str) -> str:
    """Busca el perfil profesional de una persona."""
    profiles = {
        "Ada Lovelace": "Primera programadora. Contribuciones al motor analítico.",
        "Alan Turing": "Padre de la computación moderna. Creó la máquina de Turing.",
        "Grace Hopper": "Inventora del primer compilador. Popularizó 'bug'.",
    }
    return profiles.get(name, f"Perfil no encontrado para {name}")

tools = [extract_person, lookup_profile]
tools_by_name = {t.name: t for t in tools}
model = init_chat_model("openai:gpt-4.1-mini")
model_with_tools = model.bind_tools(tools)

text = "Ada Lovelace fue la primera programadora, Alan Turing sentó las bases, y Grace Hopper revolucionó los lenguajes."

# Paso 1: extraer personas
msgs = [HumanMessage(content=f"Extrae las personas: {text}")]
ai_msg = model_with_tools.invoke(msgs)
msgs.append(ai_msg)

people = [tc["args"] for tc in ai_msg.tool_calls if tc["name"] == "extract_person"]
for tc in ai_msg.tool_calls:
    msgs.append(ToolMessage(content=str(tc["args"]), tool_call_id=tc["id"]))

print(f"Extraídas: {len(people)} personas")

# Paso 2: buscar perfiles
msgs2 = [HumanMessage(content=f"Busca perfiles de: {', '.join(p['name'] for p in people)}")]
ai_msg2 = model_with_tools.invoke(msgs2)

profiles = {}
for tc in ai_msg2.tool_calls:
    if tc["name"] == "lookup_profile":
        result = tools_by_name["lookup_profile"].invoke(tc["args"])
        profiles[tc["args"]["name"]] = result

# Paso 3: reporte combinado
print("\n=== Reporte ===")
for p in people:
    name = p["name"]
    print(f"\n{name}{p['role']}")
    print(f"  Perfil: {profiles.get(name, 'No disponible')}")
# Output esperado:
# Extraídas: 3 personas
#
# === Reporte ===
#
# Ada Lovelace — primera programadora
#   Perfil: Primera programadora. Contribuciones al motor analítico.
#
# Alan Turing — bases de la computación
#   Perfil: Padre de la computación moderna. Creó la máquina de Turing.
#
# Grace Hopper — revolución de lenguajes
#   Perfil: Inventora del primer compilador. Popularizó 'bug'.

Explicación: El pipeline combina extracción (parallel tool calls para identificar personas) con tools reales (búsqueda de perfiles). La extracción identifica entidades; las tools reales enriquecen los datos.


Resumen

En esta cápsula aprendiste:

  • Tool-based extraction usa tool calling para extraer datos estructurados — el modelo "llama" una tool que es solo un schema
  • La diferencia con with_structured_output: tool extraction permite mezclar tools reales con extracción, y usa parallel calls para múltiples entidades
  • args_schema en @tool usa un modelo Pydantic completo como schema de argumentos
  • Para forzar la extracción, usa tool_choice="nombre_tool" en bind_tools()
  • Los args extraídos son dicts sin validar — usa MySchema(**args) para validar con Pydantic
  • Parallel tool calls extraen múltiples entidades naturalmente, sin wrappers list[Entity]
  • Puedes combinar tools reales + extracción en la misma conversación

Próxima cápsula: Error Handling y Troubleshooting — qué hacer cuando una tool falla, retry logic, validación de schemas, y debugging de tool calls en producción.


Recursos adicionales

  1. Tool Calling — LangChain Docs — Guía conceptual de tool calling
  2. How to do extraction using tool calling — Tutorial de extracción con tools
  3. Structured Output vs Tool Calling — Comparación oficial
  4. Pydantic Field Validators — Validaciones avanzadas para schemas
  5. OpenAI Function Calling — Implementación de OpenAI
  6. LangChain Extraction Use Cases — Casos de uso de extracción
  7. Anthropic Tool Use for Extraction — Extracción con Anthropic

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