Skip to content
This page has been auto-translated and may contain errors.View in English

Uso de herramientas

docs.scrimba.com

El uso de herramientas (frecuentemente llamado invocación de funciones) es cómo un modelo de lenguaje se extiende más allá de su propia caja de texto: le entregas una lista de funciones que puede solicitar que ejecutes, y decide cuándo usarlas. El modelo nunca ejecuta nada por sí mismo, solo solicita una llamada, y tu código hace el trabajo.

Un modelo por sí solo está encerrado en una caja. No puede verificar el clima actual, buscar un usuario en tu base de datos o enviar un correo electrónico. Solo predice texto basado en lo que aprendió, congelado en el momento del entrenamiento, la misma imagen de cómo funcionan los LLM. El uso de herramientas es cómo le permites llegar más allá de la caja, y es la base sobre la cual se construyen los agentes.

La palabra clave es "solicitar". El modelo te dice qué función quiere y con qué argumentos. Tu código la ejecuta y devuelve el resultado. Mantienes el control todo el tiempo.

Un modelo solo produce texto, por lo que por sí solo no puede obtener datos en vivo, acceder a tu base de datos o activar una acción. El uso de herramientas cierra esa brecha: describes un conjunto de funciones, el modelo emite una solicitud estructurada para llamar a una, tu código la ejecuta, y devuelves el resultado. El modelo orquesta, tu código ejecuta.

Este capítulo es el mecanismo subyacente a cada agente que construirás. Domina el bucle, el esquema de herramientas y la ruta de error aquí, y los agentes se convierten en este mismo patrón repetido.

El uso de herramientas es la costura donde un generador de texto probabilístico se encuentra con tu código determinístico, y la mayoría del dolor de producción vive justo en esa costura. El modelo predice una llamada estructurada, tu código la ejecuta, y el resultado se devuelve como más contexto. Todo lo que hace esto difícil, validar argumentos no confiables, idempotencia, viajes redondos adicionales, llamadas alucinadas, observabilidad, surge de ese único intercambio.

Nada aquí es matemática nueva. Es la disciplina de ingeniería de permitir que un sistema estocástico desencadene efectos secundarios reales en tus sistemas, y es el sustrato sobre el cual descansa todo el capítulo de agentes.

El bucle

El uso de herramientas es un ir y venir, no una sola llamada:

  1. Envías el mensaje del usuario más una lista de herramientas que el modelo puede usar.
  2. El modelo responde de una de dos formas: una respuesta normal, o una solicitud para llamar a una herramienta, con los argumentos que desea.
  3. Si solicitó una herramienta, tu código ejecuta esa función y devuelve el resultado al modelo.
  4. El modelo usa el resultado para escribir su respuesta final.

Ese es todo el patrón. El modelo solicita, tu código ejecuta. Los pasos 2 y 3 pueden repetirse si el modelo necesita varias herramientas, que es la semilla de los agentes un par de capítulos adelante.

JunoEl bucle El uso de herramientas es un bucle: envías un mensaje más una lista de herramientas, el modelo responde o solicita llamar a una herramienta con argumentos, tu código ejecuta la función, y devuelves el resultado para que el modelo lo use. El modelo nunca ejecuta código por sí mismo, solo solicita llamadas, así que mantienes el control. Repite los pasos intermedios y tienes los comienzos de un agente.

El bucle tiene cuatro pasos: envía el mensaje más la lista de herramientas, el modelo responde o solicita una llamada, tu código ejecuta la función, el resultado vuelve, el modelo continúa. La pieza que los principiantes suelen perder es que esto raramente es una sola ronda. El modelo puede solicitar una herramienta, leer tu resultado, luego solicitar otra herramienta basada en lo que vio, y así sucesivamente.

Así que la forma real es un bucle multi-turno: sigue llamando al modelo y ejecutando las herramientas que solicita hasta que una respuesta llegue sin llamadas de herramientas en ella. Esa respuesta final sin herramientas es la respuesta para el usuario.

python
messages = [{"role": "user", "content": user_text}]

while True:
    response = client.chat.completions.create(
        model=MODEL, messages=messages, tools=tools,
    )
    msg = response.choices[0].message
    messages.append(msg)              # registra lo que dijo el modelo

    if not msg.tool_calls:            # sin herramienta solicitada: esta es la respuesta
        return msg.content

    for call in msg.tool_calls:       # ejecuta cada llamada solicitada, no solo la primera
        args = json.loads(call.function.arguments)
        result = tool_impls[call.function.name](args)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": json.dumps(result),
        })

La condición del bucle es la pregunta de seguridad. Un modelo que sigue solicitando herramientas nunca sale por sí solo, así que limita las iteraciones en producción y detente con un error claro si alcanzas el límite. El bucle termina cuando el modelo devuelve una respuesta sin llamadas de herramientas.

JunoEl bucle El bucle es envía herramientas, el modelo responde o solicita una llamada, la ejecutas, devuelves el resultado, repite. Aquí está el movimiento: sigue adelante hasta que el modelo devuelve una respuesta sin llamadas de herramientas, así que escríbelo como un bucle real, no una declaración if. Y limita las iteraciones, porque un modelo que sigue solicitando herramientas te hará girar felizmente para siempre en tu cuenta.

El bucle es mecánicamente pequeño: llama al modelo con herramientas, ejecuta lo que solicita, agrega resultados, repite hasta que devuelva una respuesta sin llamadas de herramientas. La parte interesante es que cada iteración es un completo viaje redondo del modelo, y los viajes redondos son donde va tu latencia y presupuesto de tokens.

Cada turno reenvía todo el historial de mensajes en crecimiento, por lo que una tarea que necesita cinco llamadas a herramientas paga por el avance cinco veces, más la entrada crece cada turno conforme se acumulan resultados. Las palancas:

  • Minimiza el número de viajes redondos dando al modelo herramientas que hagan más por llamada.
  • Mantén los resultados de herramientas compactos para que el historial reenviado no se infle.
  • Ejecuta llamadas a herramientas independientes concurrentemente en lugar de serialmente, así la latencia de reloj de pared sigue la llamada más lenta, no su suma.
python
MAX_TURNS = 8

for turn in range(MAX_TURNS):
    response = client.chat.completions.create(
        model=MODEL, messages=messages, tools=tools,
    )
    msg = response.choices[0].message
    messages.append(msg)

    if not msg.tool_calls:
        return msg.content

    # el modelo puede solicitar varias llamadas en un turno; ejecútalas, luego continúa
    for call in msg.tool_calls:
        messages.append(run_tool(call))   # validado, registrado, envuelto en error

raise RuntimeError("tool loop did not converge within MAX_TURNS")

El límite duro no es opcional. Sin él, un modelo confundido puede iterar indefinidamente, y el modo de fallo es una solicitud lenta y costosa en lugar de un error limpio. Establécelo, y cuando lo alcances, falla ruidosamente y registra el rastro para que puedas ver qué estaba persiguiendo el modelo.

JunoEl bucle El bucle es trivial de escribir y costoso de ejecutar: cada turno es una llamada al modelo completa sobre todo el historial reenviado, así que cinco llamadas a herramientas significa pagar por el avance cinco veces. Mantén los resultados compactos, ejecuta llamadas independientes concurrentemente, y prefiere menos herramientas más gruesas sobre muchas conversacionales. Y establece un límite duro de turnos, porque el día que un modelo decide iterar para siempre, quieres un error limpio en tus registros, no una factura de cinco cifras.

Qué realmente está sucediendo

El uso de herramientas puede parecer que el modelo ganó nuevos poderes, y no fue así. Bajo el capó, el modelo está haciendo la única cosa que siempre hace: predecir texto. Nada ha cambiado sobre la caja en la que vive.

Cuando incluyes definiciones de herramientas en la solicitud, las estás agregando al contexto desde el cual el modelo predice. El modelo fue entrenado en ejemplos donde, dadas herramientas como estas, la continuación correcta es a veces un mensaje especialmente formateado que significa "llama a get_weather con ciudad Ciudad de México" en lugar de una oración normal. Así que cuando tu pregunta hace que una herramienta se vea útil, la salida más probable es ese mensaje de llamada de herramienta estructurado, y la API lo te lo presenta como tool_calls.

El modelo no ha extendido su brazo y ejecutado nada. Ha predicho que una llamada a herramienta es el movimiento correcto y ha escrito una solicitud para una, en un formato que tu código sabe leer.

Luego tú, no el modelo, ejecutas la función. Tomas el resultado real y lo pones de vuelta en los mensajes como más contexto, y el modelo predice su respuesta final desde ese contexto ampliado. Toda la característica son dos predicciones ordinarias con tu código haciendo el trabajo real en medio.

Eres el puente entre la caja de texto del modelo y el mundo real. Sostén esa imagen y el resto del capítulo, agentes incluidos, deja de sentirse misterioso: cada acción que toma una IA es una solicitud predicha que tu código elige ejecutar.

JunoQué realmente está sucediendo El uso de herramientas es aún pura predicción: el modelo fue entrenado para que, dado definiciones de herramientas en su contexto, la salida probable sea a veces un mensaje estructurado "llamar esta función con estos argumentos", que la API te entrega como tool_calls. El modelo nunca ejecuta nada, predice una solicitud. Tu código ejecuta la función y devuelve el resultado como más contexto para la siguiente predicción, así que eres el puente entre el modelo y el mundo real.

El uso de herramientas no es una nueva capacidad enganchada al modelo, es la misma predicción de siguiente token de cómo funcionan los LLM apuntada a un objetivo estructurado. Las definiciones de herramientas van en el contexto, y el modelo fue entrenado para que, cuando una herramienta encaja, la continuación de mayor probabilidad sea una llamada formateada en lugar de prosa. La API analiza esa continuación y te la entrega como tool_calls.

La consecuencia útil: una llamada a herramienta es generación limitada, salida que el modelo produce para coincidir con un esquema que proporcionaste. Ese es exactamente el mismo mecanismo que salida estructurada, donde le pides al modelo devolver JSON en una forma que defines.

La línea entre ellos es la intención. La salida estructurada es "dame datos en esta forma y detente". El uso de herramientas es "dame datos en esta forma para que pueda ejecutar algo y devolver el resultado".

Si solo necesitas un objeto analizado del modelo, elige salida estructurada. Elige uso de herramientas cuando el modelo necesita actuar y luego reaccionar a lo que tu código devuelve.

Porque la llamada es predicha, no ejecutada, tu código es lo único que realmente toca tus sistemas. El modelo propone, tu código dispone, y ese límite es donde pones cada verificación antes de que algo real suceda.

JunoQué realmente está sucediendo Una llamada a herramienta es la misma predicción de siguiente token, apuntada a un esquema en lugar de prosa: la API te lo presenta como tool_calls. Es generación limitada, el mismo mecanismo que salida estructurada. La diferencia es intención: salida estructurada te da datos y se detiene, uso de herramientas te da datos para que puedas ejecutar algo y reaccionar. De cualquier forma el modelo solo propone, tu código es lo único que toca algo real.

Una llamada a herramienta es texto predicho, no ejecución, y mantener eso claro es lo que te protege. El modelo emite la continuación de mayor probabilidad dadas las herramientas en el contexto, la API la decodifica en una llamada estructurada, y tu código decide si respetarla. Mecánicamente es salida estructurada con un efecto secundario adjunto.

Ese marco establece el límite de confianza con precisión. Los argumentos en una llamada a herramienta son salida del modelo, lo que significa que son entrada no confiable: texto que un sistema probabilístico generó, con el mismo estatus que cualquier cosa que un usuario escribió en un formulario. Trátalo así.

El modelo también puede ser dirigido por contenido que lee a mitad del bucle, así que si una herramienta devuelve texto de una página web o documento, una inyección de solicitud enterrada en ese texto puede hacer que el modelo solicite una herramienta diferente con argumentos elegidos por el atacante. La defensa es estructural, no una instrucción de cortesía en el aviso del sistema: valida argumentos contra un esquema estricto, limita cada herramienta al permiso más estrecho que necesita, y nunca dejes que la autoridad de una herramienta exceda lo que otorgarías a la parte de confianza mínima que puede influir en sus entradas.

Entonces el modelo mental es un enrutador de solicitudes, no un colega. El modelo enruta a una función con argumentos propuestos. Tu código autentica, autoriza, valida, y solo luego ejecuta. Todo aguas abajo de "solo luego" es donde este capítulo se gana su lugar.

JunoQué realmente está sucediendo Una llamada a herramienta es texto predicho que la API decodifica para ti, salida estructurada con un efecto secundario, nunca el modelo ejecutando nada. Así que los argumentos son entrada no confiable, mismo estatus que un campo de formulario que un extraño llenó, y una inyección de solicitud escondida en el texto devuelto de una herramienta puede redirigir la siguiente llamada. Valida, limita y autoriza en el límite. El modelo enruta la solicitud, tu código es lo único con su mano en la palanca.

Definir una herramienta

Describes cada herramienta al modelo: su nombre, qué hace, y los argumentos que toma. La descripción no es documentación para ti, es una instrucción para el modelo sobre cuándo usar la herramienta, así que escríbela como la solicitud que es.

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Obtén el clima actual de una ciudad. Úsalo cuando el usuario pregunta sobre el clima.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "El nombre de la ciudad, p. ej. 'Ciudad de México'"},
                },
                "required": ["city"],
            },
        },
    },
]

El bloque parameters es un esquema, la misma idea que salida estructurada: define los argumentos que el modelo debe producir. Cuando el modelo decide llamar a get_weather, devuelve un argumento city que coincide con esta forma. Una descripción vaga ("obtiene clima") lleva al modelo a usar la herramienta en los momentos equivocados. Una clara ("Úsalo cuando el usuario pregunta sobre el clima") la guía bien.

JunoDefinir una herramienta Una definición de herramienta tiene un nombre, una descripción, y un esquema parameters para sus argumentos. La descripción es realmente una solicitud: le dice al modelo cuándo usar la herramienta, así que escríbela cuidadosamente. El esquema de parámetros es el mismo mecanismo que salida estructurada, limitando los argumentos que el modelo envía.

Una definición de herramienta tiene tres partes que el modelo lee: el nombre, la descripción, y el esquema parameters. La descripción es la parte que la gente escribe mal. No es un comentario para tus compañeros de equipo, es una solicitud que el modelo usa para decidir cuándo llamar a la herramienta, así que escríbela como una: di qué hace la herramienta, cuándo usarla, y cuándo no.

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_orders",
            "description": (
                "Busca los pedidos de un cliente por su correo electrónico de cuenta. "
                "Úsalo solo cuando el usuario pregunta sobre un pedido existente. "
                "No lo uses para preguntas de producto o reembolsos."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "email": {"type": "string", "description": "Correo electrónico de cuenta para buscar"},
                    "status": {
                        "type": "string",
                        "enum": ["pending", "shipped", "delivered", "cancelled"],
                        "description": "Filtro de estado opcional",
                    },
                },
                "required": ["email"],
                "additionalProperties": False,
            },
        },
    },
]

Aprieta el esquema y aprietas el modelo:

  • required obliga los argumentos que la herramienta no puede ejecutar sin.
  • Un enum limita un campo a un conjunto fijo, así que el modelo no puede inventar un quinto estado.
  • additionalProperties: false rechaza campos que no definiste.

El esquema es tu primera línea de defensa: cuanto más estrecho sea, menos formas tiene el modelo de llamar a la herramienta mal.

Esta forma de herramienta es estilo OpenAI; las claves exactas varían por proveedor (Anthropic y otros anidan el esquema diferentemente), pero las tres partes, nombre, descripción, esquema, son iguales en todas partes.

JunoDefinir una herramienta Nombre, descripción, y un esquema parameters, eso es una herramienta. La descripción es una solicitud, no una docstring: dile al modelo cuándo llamarla y cuándo no, o la llamará en los momentos equivocados. Aprieta el esquema con required, enum, y ninguna propiedad extra, porque un esquema estrecho son menos formas para que el modelo llame a la herramienta mal. Las claves JSON exactas cambian por proveedor, pero esas tres partes no.

Una definición de herramienta son dos solicitudes usando un sombrero. La descripción dirige cuándo el modelo llama a la herramienta, el esquema limita qué argumentos puede enviar, y ambas son instrucciones orientadas al modelo, no docs internos. El esquema es donde puedes ser estricto, y la strictez aquí es el control de alucinación más barato que tienes.

python
{
    "name": "issue_refund",
    "description": (
        "Emite un reembolso para una línea de pedido específica. "
        "Úsalo solo después de confirmar el ID de pedido y cantidad con el usuario. "
        "Nunca lo llames para cantidades por encima del total del pedido."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {"type": "string", "pattern": "^ord_[0-9]{8}$"},
            "amount_cents": {"type": "integer", "minimum": 1, "maximum": 100000},
            "reason": {"type": "string", "enum": ["damaged", "wrong_item", "late"]},
        },
        "required": ["order_id", "amount_cents", "reason"],
        "additionalProperties": False,
    },
}

Un campo limitado (uno con enum, pattern, minimum/maximum, o required establecido) estrecha qué puede emitir el modelo, pero lee el límite claramente: muchos proveedores validan la estructura del esquema, sin embargo los valores aún vienen de predicción, así que un esquema apretado reduce llamadas mal formadas sin probar que la llamada sea correcta o segura. order_id coincidiendo con un patrón no significa que ese pedido exista o pertenezca a este usuario. Así que el esquema es un filtro, no una verificación de autorización.

Aún así validas cada argumento contra estado real, el pedido existe, la cantidad no excede el total, el llamador lo posee, en tu código antes de que se ejecute el efecto secundario. La validación del esquema captura la forma equivocada; solo tu código captura la acción equivocada. (Las claves del esquema y cuán estrictamente cada proveedor las aplica varían, así que confirma el comportamiento de tu proveedor en lugar de asumir que la restricción se aplica.)

Una palanca más: conteo de herramientas. Pasado aproximadamente una docena de herramientas, la precisión de selección cae y el modelo empieza a alcanzar la equivocada, y cada definición también se sienta en el contexto quemando tokens en cada llamada. Mantén el conjunto de herramientas activas pequeño y relevante para la tarea en lugar de exponer toda tu superficie de API a la vez.

JunoDefinir una herramienta La descripción controla cuándo el modelo llama, el esquema controla qué puede enviar, y ambas son solicitudes. Cierra el esquema con enum, pattern, y límites, pero no confundas una forma válida con una acción válida: un order_id bien formado aún no es confiable, así que revalida contra estado real antes del efecto secundario. Y mantén el conjunto de herramientas pequeño, porque pasada una docena el modelo elige mal y cada definición está agotando tu contexto. El esquema filtra, tu código autoriza.

Manejar la llamada

Cuando el modelo quiere una herramienta, la respuesta contiene tool_calls en lugar de una respuesta final. Lees la función solicitada y argumentos, ejecutas la función real, y devuelves el resultado como un mensaje tool. Luego llamas al modelo nuevamente para que pueda terminar.

python
import json

# implementación real de la herramienta
def get_weather(city):
    # en una app real esto llama a una API de clima; aquí lo fingimos
    return {"city": city, "tempC": 18, "condition": "cloudy"}

messages = [{"role": "user", "content": "¿Cuál es el clima en Ciudad de México?"}]

# 1. primera llamada: ofrece las herramientas
response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
tool_calls = response.choices[0].message.tool_calls
tool_call = tool_calls[0] if tool_calls else None

if tool_call:
    # 2. ejecuta la función solicitada con los argumentos del modelo
    args = json.loads(tool_call.function.arguments)
    result = get_weather(args["city"])

    # 3. envía la solicitud del modelo y tu resultado de vuelta
    messages.append(response.choices[0].message)         # la solicitud de herramienta del asistente
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call.id,
        "content": json.dumps(result),
    })

    # 4. llama de nuevo para que el modelo pueda responder usando el resultado
    response = client.chat.completions.create(model=MODEL, messages=messages)

print(response.choices[0].message.content)
# "Actualmente hay 18 grados y está nublado en Ciudad de México."

Los argumentos llegan como una cadena JSON, así que json.loads los convierte en un objeto real, luego los verificas antes de usar. Empujas dos mensajes al historial: la solicitud de herramienta del modelo y tu resultado tool, vinculados por tool_call_id. La llamada final convierte los datos de clima crudos en una oración natural.

JunoManejar la llamada Cuando el modelo quiere una herramienta, su respuesta lleva tool_calls en lugar de texto. Parseas los argumentos de una cadena JSON, ejecutas la función real, y empujas dos mensajes atrás: la solicitud del modelo y tu resultado, vinculados por tool_call_id. Una llamada al modelo final convierte tu resultado crudo en una respuesta natural.

Cuando el modelo quiere una herramienta, su respuesta lleva tool_calls en lugar de content. Cada llamada te da un nombre de función, un id, y argumentos como una cadena JSON. Parseas, ejecutas, y adjuntas un mensaje tool etiquetado con el tool_call_id coincidente para que el modelo sepa cuál resultado responde cuál solicitud.

La parte que vale la pena hacer bien es el fallo. Tu herramienta lanzará: un argumento malo, un 404, un tiempo de espera. El instinto es dejar que la excepción suba, pero eso termina todo el turno. El mejor movimiento es devolver errores como datos: captura el error y devuélvelo como el resultado de la herramienta, para que el modelo pueda leer qué salió mal y recuperarse, reintentar con un argumento corregido, o decirle al usuario que no pudo hacer lo.

python
def run_tool(call):
    try:
        args = json.loads(call.function.arguments)
        result = tool_impls[call.function.name](args)
        content = json.dumps(result)
    except Exception as err:
        # devuelve el fallo como datos, no como excepción
        content = json.dumps({"error": str(err)})
    return {"role": "tool", "tool_call_id": call.id, "content": content}

Devolver errores como mensajes de herramienta es lo que hace que un bucle de herramientas sea resiliente en lugar de frágil. Un modelo que llama a search_orders con un correo electrónico mal formado obtiene {"error": "invalid email"} y puede pedirle al usuario que lo corrija, en lugar de que toda tu solicitud falle con 500. La forma de respuesta (tool_calls, tool_call_id, el rol tool) es estilo OpenAI y varía por proveedor, pero el patrón, parsea, ejecuta, devuelve el resultado o el error, se mantiene en todos.

JunoManejar la llamada La respuesta lleva tool_calls: parsea los argumentos JSON, ejecuta la función, adjunta un mensaje tool con el tool_call_id coincidente. Aquí está el movimiento en fallos: no dejes que la excepción suba y termine el turno, captura y devuelve {"error": ...} como el resultado de la herramienta para que el modelo pueda recuperarse o reintentar. Los nombres de campo exactos varían por proveedor, pero parsea, ejecuta, devuelve resultado-o-error es lo mismo en todas partes.

Parsear y despachar la llamada es la parte aburrida. La parte peligrosa es todo entre recibir los argumentos y ejecutar el efecto secundario, porque los argumentos son salida del modelo y la función hace algo real.

Tres hábitos separan una demostración de una herramienta que puedes ejecutar en producción:

  • Primero, valida antes de ejecutar, contra estado real, no el esquema: el esquema ya pasó o no estarías aquí, así que ahora confirma que el pedido existe, el usuario lo posee, la cantidad está en rango.
  • Segundo, construye para idempotencia, la propiedad de que ejecutar la misma operación dos veces tiene el mismo efecto que ejecutarla una vez. Los modelos reintentannen bucles, las redes duplican, así que una herramienta de escritura necesita una clave de idempotencia (a menudo derivada de tool_call_id) para que una repetición no cobre el doble o no envíe el doble.
  • Tercero, devuelve errores como datos, estructurados lo suficientemente para que el modelo pueda actuar sobre ellos, para que un fallo recuperable se convierta en un reintento y uno no recuperable se convierta en un mensaje limpio para el usuario.
python
def run_tool(call):
    name, args = call.function.name, json.loads(call.function.arguments)
    log.info("tool_call", name=name, args=redact(args), call_id=call.id)  # observabilidad

    try:
        validate(name, args)                       # verificaciones de estado real, lanza en entrada mala
        result = TOOLS[name](args, idem_key=call.id)  # idempotente en reintento
        content = json.dumps(result)
    except ValidationError as err:
        content = json.dumps({"error": "invalid", "detail": str(err)})  # modelo puede reintentar
    except Exception as err:
        log.exception("tool_failed", call_id=call.id)
        content = json.dumps({"error": "tool_failed"})  # no filtres internos al modelo

    return {"role": "tool", "tool_call_id": call.id, "content": content}

Dos realidades de producción más:

  • Observabilidad: registra qué herramienta corrió, con qué argumentos (redacta secretos), y qué devolvió, porque cuando un agente se comporta mal el rastro de llamadas de herramientas es la única forma de reconstruir lo que realmente hizo.
  • Herramientas destructivas: para cualquier cosa irreversible, borrar, pagar, enviar correo electrónico, no dejes que la llamada del modelo sea la autoridad final. Ciérrala detrás de una confirmación, un ensayo que reporta qué sucedería, o un paso de aprobación humana, y limita la credencial para que el radio de explosión esté limitado incluso si el modelo está equivocado o es comprometido.

Una llamada válida por esquema aún es no confiable hasta que tu código la autoriza. (Los nombres de campo y el sobre de reintento son estilo OpenAI; la disciplina se mantiene en cualquier proveedor.)

JunoManejar la llamada Parsear es la parte aburrida. Entre los argumentos y el efecto secundario: valida contra estado real no solo el esquema, haz escrituras idempotentes de `tool_call_id` porque los reintentos se dispararán dos veces, y devuelve errores como datos que el modelo pueda actuar sin filtrar tus internos. Registra cada llamada y sus argumentos para que puedas reconstruir lo que un agente realmente hizo. Y cierra cualquier cosa destructiva detrás de confirmación o una credencial limitada, porque un reembolso válido por esquema es aún una solicitud de un extraño hasta que tu código dice sí.

En la práctica

El mismo flujo que una función reutilizable. Observa que la única cosa entre el modelo y tus sistemas es código que escribiste y controlas:

python
import json

tool_impls = {"get_weather": lambda args: get_weather(args["city"])}

def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    response = client.chat.completions.create(model=MODEL, messages=messages, tools=tools)
    calls = response.choices[0].message.tool_calls
    if not calls:
        return response.choices[0].message.content  # el modelo respondió directamente

    call = calls[0]
    args = json.loads(call.function.arguments)
    result = tool_impls[call.function.name](args)

    messages.append(response.choices[0].message)
    messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})

    response = client.chat.completions.create(model=MODEL, messages=messages)
    return response.choices[0].message.content

Esto maneja una llamada a herramienta. Un modelo también puede solicitar varias a la vez, llamadas paralelas de herramientas, que manejarías haciendo bucle sobre cada entrada en tool_calls. Y si dejas que el modelo siga llamando a herramientas en un bucle hasta que termina, decidiendo su propio siguiente paso cada vez, obtienes un agente, que es exactamente adónde va el próximo capítulo.

JunoEn la práctica Envuelta en una función, el uso de herramientas es una llamada al modelo para escoger una herramienta, tu código ejecutándola, y una segunda llamada para convertir el resultado en una respuesta. La única cosa entre el modelo y tus sistemas es código que escribiste, así que mantienes el control. Buclea esto hasta que el modelo termine y tienes un agente.

En un verdadero manejador de herramientas combinas el bucle multi-turno, llamadas paralelas, y devoluciones de error en una función. El modelo puede solicitar varias herramientas en un turno, y el bucle se ejecuta hasta que deja de solicitar.

python
import json

MAX_TURNS = 6

def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    for _ in range(MAX_TURNS):
        response = client.chat.completions.create(
            model=MODEL, messages=messages, tools=tools,
        )
        msg = response.choices[0].message
        messages.append(msg)

        if not msg.tool_calls:
            return msg.content

        for call in msg.tool_calls:        # maneja todas las llamadas paralelas
            messages.append(run_tool(call))  # parsea, ejecuta, devuelve resultado-o-error

    return "Lo siento, no pude completar eso."  # alcanzó el límite de turno

Tres decisiones hacen esto listo para producción en lugar de una demostración. Buclea sobre todos de tool_calls, no [0], o silenciosamente descartas llamadas que el modelo solicitó. Limita MAX_TURNS para que un modelo no pueda girar para siempre. Y enruta fallos a través de run_tool como mensajes de error, para que una sola llamada mala no bloquee todo el intercambio.

Cuando dejas de codificar los pasos y dejas que el modelo decida su propio movimiento siguiente cada turno, este bucle exacto se convierte en un agente. La diferencia es autonomía, no arquitectura.

JunoEn la práctica El manejador de producción es el bucle multi-turno con tres cosas cableadas: buclea sobre cada entrada en tool_calls no solo la primera, limita los turnos para que no pueda girar para siempre, y devuelve fallos como mensajes de herramienta para que una llamada mala no bloquee la ejecución. Mismo bucle, más autonomía, y tienes un agente. La arquitectura no cambia, el modelo solo obtiene decidir su propio siguiente paso.

El manejador de producción pliega junto todo lo anterior: el bucle multi-turno limitado, llamadas paralelas ejecutadas concurrentemente, ejecución validada y registrada, y errores devueltos como datos. La pregunta restante es la que la mayoría de los equipos saltan: cuándo no dar al modelo una herramienta en absoluto.

Una herramienta es la respuesta correcta cuando la acción realmente depende del criterio del modelo sobre lenguaje natural: cuál pedido quiere el usuario, si esto cuenta como caso de reembolso, qué buscar. Es la respuesta equivocada cuando el camino es determinístico, la misma entrada siempre produciendo la misma llamada. Si una solicitud siempre dispara la misma llamada con argumentos derivables iguales, cablealo en código y salta el viaje redondo; eliminas una superficie de alucinación, un salto de latencia, y un costo de token por cero pérdida. Darle al modelo una herramienta compra flexibilidad y paga por ella en no determinismo, así que gástalo solo donde la flexibilidad es el punto.

python
def answer_with_tools(user_text):
    messages = [{"role": "user", "content": user_text}]

    for turn in range(MAX_TURNS):
        response = client.chat.completions.create(
            model=MODEL, messages=messages, tools=tools, tool_choice="auto",
        )
        msg = response.choices[0].message
        messages.append(msg)
        if not msg.tool_calls:
            return msg.content

        results = run_tools_concurrently(msg.tool_calls)  # llamadas independientes en paralelo
        messages.extend(results)

    log.warning("tool_loop_unconverged", turns=MAX_TURNS)
    return fallback_answer()

Dos palancas para cuándo realmente usas herramientas:

  • Limita alucinación de llamada de herramienta: un modelo puede inventar una llamada a una herramienta que nunca definiste o fabricar argumentos, así que rechaza nombres de herramientas desconocidos de frente, y usa tool_choice (el parámetro que fuerza, prohíbe, o libera uso de herramientas) para requerir una herramienta cuando sabes que una es necesaria o prohibirlas cuando sabes que ninguna debe dispararse, en lugar de dejar cada turno al azar.
  • Mantén el rastro de observabilidad, herramienta, argumentos, resultado, conteo de turno, porque un bucle autónomo solo es debuggable si puedes reproducir qué realmente hizo.

Este manejador es la semilla literal de un agente: el agente es este bucle con un conjunto de herramientas más amplio y la latitud de encadenar llamadas hacia un objetivo, que es también por qué los modos de fallo aquí se escalan allí. (La superficie del SDK es estilo OpenAI; tool_choice y el sobre de mensaje difieren por proveedor.)

JunoEn la práctica El manejador completo es el bucle limitado con ejecuciones de herramientas concurrentes, validadas, registradas y errores como datos. La pregunta saltada es cuándo no dar una herramienta: si la llamada es determinística, cablealo en código y deja caer el viaje redondo, la latencia, y una superficie de alucinación. Usa tool_choice para forzar o prohibir herramientas en lugar de esperar, rechaza llamadas a herramientas que nunca definiste, y mantén el rastro, porque esto es un agente con las ruedas de entrenamiento aún puestas, y los modos de fallo solo se hacen más grandes desde aquí.