ruta ai/agentes / módulo 4

A4 — Tools & MCP

Outcome del módulo: diseñar tools que el modelo elige y usa bien (schema tipado + descripción clara), manejar sus errores sin crashear el proceso, y exponerlas de forma interoperable vía un servidor MCP.


Concepto 1 — Diseño de tools (schema tipado + buena descripción)

Idea núcleo: el modelo elige y arma los argumentos de una tool basándose únicamente en su nombre, su descripción y su schema de parámetros — si son ambiguos, el modelo elige mal, no la llama cuando debería, o inventa argumentos.

Entender (capa 1)

Texto: el modelo no “entiende” tu código, solo ve la interfaz declarada: nombre, docstring/descripción, y schema de parámetros (tipos + descripción por campo). Buenas prácticas: nombre en formato verbo+objeto (buscar_pedido, no handler2), descripción que diga explícitamente cuándo usarla (y cuándo no, si hay ambigüedad con otra tool), parámetros tipados con descripción individual, evitar parámetros opcionales ambiguos que el modelo tiene que adivinar. Con varias tools parecidas, descripciones vagas o solapadas son la causa número uno de que el modelo elija mal.

Visual:

flowchart LR
    subgraph Mala["Tool mal descripta"]
        M1["nombre: do_thing / desc: hace algo"] --> M2["modelo elige mal o inventa args"]
    end
    subgraph Buena["Tool bien descripta"]
        B1["nombre: buscar_pedido / desc: busca un pedido por id o email, usar cuando el usuario pregunta por un pedido puntual"] --> B2["modelo elige correcto y arma args válidos"]
    end

Video: What is Tool Calling? Connecting LLMs to Your Data — IBM Technology

Ejemplo:

from pydantic import BaseModel, Field
from langchain_core.tools import tool

# mal: descripción vaga, sin indicar cuándo usarla
@tool
def buscar(query: str) -> str:
    """Busca algo."""
    return db_search(query)

# bien: nombre específico, descripción con criterio de uso, schema tipado
class BuscarPedidoArgs(BaseModel):
    pedido_id: str = Field(..., description="ID exacto del pedido, formato ORD-XXXXX")
    incluir_historial: bool = Field(
        False, description="Si True, incluye el historial de estados del pedido"
    )

@tool(args_schema=BuscarPedidoArgs)
def buscar_pedido(pedido_id: str, incluir_historial: bool = False) -> str:
    """Busca un pedido específico por su ID exacto.
    Usar SOLO cuando el usuario menciona un ID de pedido concreto (formato ORD-XXXXX).
    Para búsquedas por nombre de cliente, usar buscar_cliente en su lugar."""
    return format_pedido(get_pedido(pedido_id, incluir_historial))

Fijar (capa 2)

Nota atómica:

Feynman: “El modelo elige tool leyendo un cartel en la puerta, no mirando adentro del local. Si el cartel dice ‘cosas’ en vez de ‘reparación de celulares, no de laptops’, va a entrar al local equivocado.”

Aplicar (capa 3)

Tomá una tool con descripción de una línea vaga, reescribila con descripción específica (cuándo sí / cuándo no) más schema tipado con descripciones por campo. Probá 3 prompts ambiguos antes y después, y compará qué tool elige el modelo en cada caso.

Límites


Concepto 2 — Manejo de errores de tool (error legible al modelo, no crashear)

Idea núcleo: cuando una tool falla (excepción, timeout, dato inválido), el error se captura y se devuelve como texto al modelo — no se propaga como excepción que tumba el proceso — para que el agente pueda decidir un siguiente paso razonable.

Entender (capa 1)

Texto: una tool en producción puede fallar por mil motivos (API externa caída, timeout, input inválido, rate limit). Si la excepción se propaga sin capturar, tumba todo el grafo/proceso del agente. El patrón correcto: envolver la ejecución en try/except, y en el except devolver un string descriptivo del error como contenido del ToolMessage — el modelo lo lee igual que leería un resultado exitoso, y puede decidir: reintentar, usar otra tool, o avisarle al usuario que algo falló.

Visual:

flowchart LR
    Tool["Tool se ejecuta"] -->|excepción| Catch["try/except captura"]
    Catch --> Msg["ToolMessage: Tool fallo, motivo legible"]
    Msg --> LLM["Modelo lee el error y decide el siguiente paso"]

Video: buscar “LLM tool error handling function calling agent” [verificar].

Ejemplo:

from functools import wraps

def tool_safe(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        try:
            return fn(*args, **kwargs)
        except TimeoutError:
            return "Error: la tool tardó demasiado en responder, podés reintentar."
        except ValueError as e:
            return f"Error: argumento inválido para la tool ({e}). Revisá el formato esperado."
        except Exception as e:
            logger.exception("fallo inesperado en tool %s", fn.__name__)  # log completo, para vos
            return f"Error: la tool falló de forma inesperada ({type(e).__name__})."  # frase, para el modelo
    return wrapper

@tool
@tool_safe
def consultar_api_externa(endpoint: str) -> str:
    """Consulta un endpoint externo y devuelve la respuesta."""
    return http_get(endpoint, timeout=5)

Fijar (capa 2)

Nota atómica:

Feynman: “El modelo no lee un traceback, lee una frase. Si la tool explota sin avisar, el agente entero se cae; si le devolvés ‘no se pudo, motivo X’, el agente sigue pensando qué hacer con eso.”

Aplicar (capa 3)

Envolvé una tool que puede fallar realmente (una llamada HTTP a un endpoint que no existe) con el patrón try/except → mensaje legible. Verificá que el agente recibe el error como texto y responde razonablemente en vez de crashear.

Límites


Concepto 3 — MCP (Model Context Protocol)

Idea núcleo: MCP es un protocolo estándar para exponer tools, recursos y prompts a cualquier cliente LLM compatible — desacopla “quién define la tool” de “quién la consume”, dando interoperabilidad en vez de integración ad-hoc por framework.

Entender (capa 1)

Texto: sin MCP, cada framework o cliente (LangChain, tu agente propio, Claude Desktop, otro servicio interno) tiene su propia forma de declarar tools — terminás reimplementando la misma tool N veces para N consumidores. Con MCP, un servidor MCP declara las tools una sola vez (schema + implementación) y habla el protocolo (JSON-RPC, típicamente sobre stdio o HTTP/SSE); cualquier cliente MCP compatible las descubre y las usa igual, sin conocer los detalles de implementación. Es la misma idea que un servidor HTTP/REST, pero pensado específicamente para exponer capacidades a modelos: tools (acciones), resources (datos de contexto) y prompts (plantillas reusables).

Visual:

flowchart TD
    Server["Servidor MCP: expone las tools una vez"]
    Server --> C1["Claude Desktop"]
    Server --> C2["Agente LangGraph propio"]
    Server --> C3["Otro servicio / cliente MCP"]

Video: The Model Context Protocol (MCP) — Anthropic

Ejemplo:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("rag-tools")

@mcp.tool()
def buscar_en_corpus(query: str, top_k: int = 5) -> str:
    """Busca en el corpus indexado y devuelve los pasajes más relevantes.
    Usar cuando la pregunta requiere información específica del corpus, no conocimiento general."""
    resultados = retrieve(query, top_k=top_k)
    return format_results(resultados)

@mcp.tool()
def calcular_totales(items: list[dict]) -> str:
    """Suma precios de una lista de items con formato {"precio": float}."""
    total = sum(i["precio"] for i in items)
    return f"Total: {total:.2f}"

if __name__ == "__main__":
    mcp.run()   # expone ambas tools por stdio; cualquier cliente MCP las descubre

Fijar (capa 2)

Nota atómica:

Feynman: “Function calling ad-hoc es un enchufe distinto por marca de aparato — cada framework tiene el suyo. MCP es el enchufe universal: definís la tool una vez del lado del servidor, y cualquier cliente compatible la usa sin adaptador.”

Aplicar (capa 3)

Envolvé la tool RAG del build step de A3 en un servidor MCP con FastMCP. Conectala desde un cliente MCP (o el MCP Inspector) y probala fuera de tu agente, confirmando que responde igual que dentro del grafo LangGraph.

Límites


Build step

Agregá una segunda tool al agente de A3 (ej. calcular_totales, o una acción distinta relevante a tu dominio). Exponé ambas tools — la del RAG y la nueva — vía un servidor MCP con FastMCP, y verificá que se puedan invocar desde un cliente MCP fuera de tu grafo LangGraph.

Checklist de dominio

Marcás cuando podés explicar (Feynman) + aplicar cada uno:

Salida verificable del módulo: un servidor MCP corriendo que expone al menos 2 tools con schema y descripciones bien diseñadas, cada una con manejo de errores que devuelve texto legible en vez de crashear, testeado desde un cliente MCP fuera de tu agente.