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 (a menudo llamado llamada de funciones) es cómo un modelo de lenguaje sale de su propia caja de texto: le das una lista de funciones que puede pedirte que ejecutes, y decide cuándo usarlas. El modelo nunca ejecuta nada por sí mismo, solo solicita una llamada, y tu código realiza el trabajo.

Un modelo por sí solo está encerrado en una caja. No puede verificar el clima de hoy, buscar un usuario en tu base de datos o enviar un correo electrónico. Solo predice texto a partir de lo que aprendió, congelado en el momento del entrenamiento, la misma imagen de cómo funcionan los LLMs. El uso de herramientas es cómo dejas que salga de la caja, y es la base sobre la que se construyen los agentes.

La palabra clave es "pedir". 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, tocar tu base de datos o desencadenar 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. Entiende bien el ciclo, 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 determinista, y la mayoría del dolor en 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 lo hace difícil, validar argumentos no confiables, idempotencia, rondas extra, llamadas alucinadas, observabilidad, surge de ese único punto de transferencia.

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

El ciclo

El uso de herramientas es un ir y venir, no una única 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 quiere.
  3. Si pidió 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 el patrón completo. El modelo pide, 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 ciclo El uso de herramientas es un ciclo: envías un mensaje más una lista de herramientas, el modelo responde o pide 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 del medio y tienes los comienzos de un agente.

El ciclo tiene cuatro tiempos: envías el mensaje más la lista de herramientas, el modelo responde o solicita una llamada, tu código ejecuta la función, el resultado regresa, el modelo continúa. La parte que los principiantes a menudo pierden es que esto rara vez es una ronda. El modelo puede solicitar una herramienta, leer tu resultado, luego solicitar otra herramienta basada en lo que vio, y así sucesivamente.

Entonces la forma real es un ciclo de múltiples turnos: sigue llamando al modelo y ejecutando cualquier herramienta que solicite hasta que llegue una respuesta sin llamadas a herramientas. 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 ciclo es la pregunta de seguridad. Un modelo que sigue pidiendo herramientas nunca sale por sí solo, así que limita las iteraciones en producción y detente con un error claro si llegas al límite. El ciclo termina cuando el modelo devuelve una respuesta sin llamadas a herramientas.

JunoEl ciclo El ciclo es enviar herramientas, el modelo responde o pide una llamada, ejecutas, devuelves el resultado, repites. Aquí está el movimiento: sigue hasta que el modelo devuelva una respuesta sin llamadas a herramientas, así que escríbelo como un ciclo real, no como una única declaración if. Y limita las iteraciones, porque un modelo que sigue pidiendo herramientas girará felizmente para siempre a tu costa.

El ciclo es mecánicamente pequeño: llama al modelo con herramientas, ejecuta lo que solicite, añade resultados, repite hasta que devuelva una respuesta sin llamadas a herramientas. La parte interesante es que cada iteración es una ronda completa del modelo, y las rondas son donde va tu latencia y presupuesto de tokens.

Cada turno reenvía el historial de mensajes completo en crecimiento, así que una tarea que necesita cinco llamadas a herramientas paga el prompt cinco veces, más la entrada crece cada turno a medida que se acumulan los resultados. Los controles:

  • Minimiza el número de rondas dando al modelo herramientas que hagan más por llamada.
  • Mantén los resultados de las herramientas compactos para que el historial reenviado no se infle.
  • Ejecuta llamadas a herramientas independientes concurrentemente en lugar de serialmente para que la latencia de pared rastrée 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 girar indefinidamente, y el modo de fallo es una solicitud lenta y cara en lugar de un error limpio. Delimitalo, y cuando llegues al límite, falla de forma ruidosa y registra el rastreo para que puedas ver qué estaba persiguiendo el modelo.

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

Qué realmente está pasando

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 añadiendo al contexto del que el modelo predice. El modelo fue entrenado en ejemplos donde, dadas herramientas como estas, la continuación correcta a veces es un mensaje especialmente formateado que significa "llama a get_weather con la 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 a herramienta estructurado, y la API lo expone a ti como tool_calls.

El modelo no ha llegado 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 cómo leer.

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

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

JunoQué realmente está pasando El uso de herramientas es aún predicción pura: el modelo fue entrenado para que, dadas definiciones de herramientas en su contexto, la salida probable a veces sea un mensaje estructurado "llama 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 capacidad nueva conectada al modelo, es la misma predicción de siguiente token de cómo funcionan los LLMs apuntada a un objetivo estructurado. Las definiciones de herramientas van al contexto, y el modelo fue entrenado para que, cuando una herramienta encaje, 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 restringida, salida que el modelo produce para coincidir con un esquema que proporcionaste. Ese es el mismo mecanismo que salida estructurada, donde le pides al modelo que devuelva JSON en una forma que defines.

La línea entre ellas 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, recurre a la salida estructurada. Recurre al uso de herramientas cuando el modelo necesita actuar y luego reaccionar a lo que devuelve tu código.

Porque la llamada es predicha, no ejecutada, tu código es la única cosa 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á pasando Una llamada a herramienta es la misma predicción de siguiente token, dirigida a un esquema en lugar de prosa: la API la expone como tool_calls. Es generación restringida, la misma maquinaria que la salida estructurada. La diferencia es la intención: la salida estructurada te da datos y se detiene, el uso de herramientas te da datos para que ejecutes algo y reacciones. De cualquier forma, el modelo solo propone, tu código es la única cosa que toca algo real.

Una llamada a herramienta es texto predicho, no ejecución, y mantenerlo claro es lo que te protege. El modelo emite la continuación de mayor probabilidad dados las herramientas en contexto, la API la decodifica en una llamada estructurada, y tu código decide si ejecutarla. 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 ciclo, así que si una herramienta devuelve texto de una página web o documento, una inyección de prompt enterrada en ese texto puede convencer al modelo de solicitar una herramienta diferente con argumentos elegidos por el atacante. La defensa es estructural, no una instrucción de cortesía en el prompt del sistema: valida argumentos contra un esquema estricto, alcance 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 menos confiable que puede influir en sus entradas.

Así que 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 entonces ejecuta. Todo lo aguas abajo de "solo entonces" es donde este capítulo gana su valor.

JunoQué realmente está pasando 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, con el mismo estatus que un campo de formulario que un extraño llenó, y una inyección de prompt escondida en el texto devuelto de una herramienta puede redirigir la siguiente llamada. Valida, alcance y autoriza en el límite. El modelo enruta la solicitud, tu código es la única cosa 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 al modelo sobre cuándo usar la herramienta, así que escríbela como el prompt que es.

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a city. Use when the user asks about weather.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "The city name, e.g. '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 el clima") lleva a que el modelo use la herramienta en los momentos incorrectos. Una clara ("Usa 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 un prompt: le dice al modelo cuándo usar la herramienta, así que escríbela cuidadosamente. El esquema de parámetros es el mismo mecanismo que la salida estructurada, restringiendo 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 suscribe. No es un comentario para tus compañeros de equipo, es un prompt que el modelo usa para decidir cuándo llamar a la herramienta, así que escríbelo como uno: di qué hace la herramienta, cuándo usarla, y cuándo no.

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_orders",
            "description": (
                "Look up a customer's orders by their account email. "
                "Use only when the user asks about an existing order. "
                "Do not use for product questions or refunds."
            ),
            "parameters": {
                "type": "object",
                "properties": {
                    "email": {"type": "string", "description": "Account email to search"},
                    "status": {
                        "type": "string",
                        "enum": ["pending", "shipped", "delivered", "cancelled"],
                        "description": "Optional status filter",
                    },
                },
                "required": ["email"],
                "additionalProperties": False,
            },
        },
    },
]

Aprieta el esquema y aprietas el modelo:

  • required fuerza los argumentos que la herramienta no puede ejecutar sin.
  • Un enum limita un campo a un conjunto fijo, para que el modelo no pueda 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 hay para que el modelo llame a la herramienta incorrectamente.

Esta forma de herramienta es de estilo OpenAI; las claves exactas varían por proveedor (Anthropic y otros anidan el esquema diferente), 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 un prompt, no una cadena de documentación: cuéntale al modelo cuándo llamarla y cuándo no, o la llamará en los momentos equivocados. Aprieta el esquema con required, enum, y sin propiedades extra, porque un esquema estrecho es menos formas para que el modelo llame a la herramienta incorrectamente. Las claves JSON exactas se desplazan por proveedor, pero esas tres partes no.

Una definición de herramienta es dos prompts usando un sombrero. La descripción dirige cuándo el modelo llama a la herramienta, el esquema restringe qué argumentos puede enviar, y ambos son instrucciones orientadas al modelo, no documentos internos. El esquema es donde se te permite ser estricto, y la rigidez aquí es el control de alucinaciones más barato que tienes.

python
{
    "name": "issue_refund",
    "description": (
        "Issue a refund for a specific order line. "
        "Use only after confirming the order ID and amount with the user. "
        "Never call for amounts above the order total."
    ),
    "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 restringido (uno con enum, pattern, minimum/maximum, o required establecido) estrecha lo que el modelo puede emitir, pero lee el límite claramente: muchos proveedores validan la estructura del esquema, pero los valores aún provienen de la predicción, así que un esquema apretado reduce llamadas mal formadas sin probar que la llamada es 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 validas cada argumento contra el 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 atrapa la forma equivocada; solo tu código atrapa 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 disminuye y el modelo comienza a alcanzar la herramienta equivocada, y cada definición también se sienta en el contexto quemando tokens en cada llamada. Mantén el conjunto activo de herramientas 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 ambos son prompts. Bloquea 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 es aún entrada no confiable, así que re-valida contra el estado real antes del efecto secundario. Y mantén el conjunto de herramientas pequeño, porque pasado una docena el modelo elige equivocadamente y cada definición está exhaustando 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 los argumentos, ejecutas la función real, y devuelves el resultado como un mensaje tool. Luego llamas al modelo de nuevo para que pueda terminar.

python
import json

# implementación real de la herramienta
def get_weather(city):
    # en una aplicación 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. devuelve la solicitud del modelo y tu resultado
    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 haces json.loads para convertirlos 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 climáticos crudos en una oración natural.

JunoManejar la llamada Cuando el modelo quiere una herramienta, su respuesta lleva tool_calls en lugar de texto. Analizas los argumentos de una cadena JSON, ejecutas la función real, y empujas dos mensajes de vuelta: la solicitud del modelo y tu resultado, vinculados por tool_call_id. Una llamada final del modelo 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. Analizas, ejecutas, y añades 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 acertar es el fallo. Tu herramienta lanzará: un argumento malo, un 404, un tiempo agotado. El instinto es dejar que la excepción se propague, pero eso termina todo el turno. El movimiento mejor es devolver errores como datos: atrapa 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 la cosa.

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 una 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 un ciclo de herramientas resiliente en lugar de frágil. Un modelo que llama a search_orders con un correo electrónico malformado obtiene {"error": "invalid email"} y puede pedir al usuario que lo corrija, en lugar de que toda tu solicitud tenga un 500. La forma de respuesta (tool_calls, tool_call_id, el rol tool) es de estilo OpenAI y varía por proveedor, pero el patrón, analizar, ejecutar, devolver el resultado o el error, se mantiene en todos ellos.

JunoManejar la llamada La respuesta lleva tool_calls: analiza los argumentos JSON, ejecuta la función, añade un mensaje tool con el tool_call_id coincidente. Aquí está el movimiento en fallos: no dejes que la excepción se propague y termine el turno, atrápala 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 analizar, ejecutar, devolver resultado-o-error es igual en todas partes.

Analizar y despachar la llamada es la parte rutinaria. La parte peligrosa es todo lo que hay 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 el estado real, no el esquema: el esquema ya pasó o no estarías aquí, así que ahora confirma que el pedido existe, que el usuario lo posee, que el monto 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 reintentan, los ciclos vuelven a disparar, 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 doble cobre o doble envíe.
  • Tercero, devuelve errores como datos, estructurados lo suficiente para que el modelo pueda actuar sobre ellos, así que un fallo recuperable se convierte en un reintento y uno irrecuperable se convierte 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 al reintentar
        content = json.dumps(result)
    except ValidationError as err:
        content = json.dumps({"error": "invalid", "detail": str(err)})  # el modelo puede reintentar
    except Exception as err:
        log.exception("tool_failed", call_id=call.id)
        content = json.dumps({"error": "tool_failed"})  # no reveles internos al modelo

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

Dos más realidades de producción:

  • Observabilidad: registra qué herramienta se ejecutó, con qué argumentos (redacta secretos), y qué devolvió, porque cuando un agente se comporta mal el rastreo de llamadas a herramientas es la única forma de reconstruir qué realmente hizo.
  • Herramientas destructivas: para cualquier cosa irreversible, eliminar, pagar, enviar correos, no dejes que la llamada del modelo sea la autoridad final. Ciérrala detrás de una confirmación, un ensayo seco que reporta qué pasaría, o un paso de aprobación humana, y alcance la credencial para que el radio de explosión esté acotado incluso si el modelo está equivocado o fue hackeado.

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

JunoManejar la llamada El análisis es la parte rutinaria. Entre los argumentos y el efecto secundario: valida contra el estado real no solo el esquema, haz escrituras idempotentes fuera de tool_call_id porque los reintentos se dispararán dos veces, y devuelve errores como datos que el modelo pueda actuar sin revelar tus internos. Registra cada llamada y sus argumentos para poder reconstruir qué realmente hizo un agente. Y cierra cualquier cosa destructiva detrás de confirmación o una credencial alcanzada, porque una reembolso válido según el 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. Fíjate en que la única cosa que se interpone 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 de herramientas paralelas, que manejarias haciendo un bucle sobre cada entrada en tool_calls. Y si dejas que el modelo siga llamando herramientas en un ciclo hasta que termine, decidiendo su propio siguiente paso cada vez, obtienes un agente, que es exactamente donde va el siguiente capítulo.

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

En un verdadero manejador de herramientas combinas el ciclo de múltiples turnos, llamadas paralelas, y devoluciones de error en una función. El modelo puede pedir varias herramientas en un único turno, y el ciclo se ejecuta hasta que deja de pedir.

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))  # analiza, ejecuta, devuelve resultado-o-error

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

Tres decisiones hacen que esto sea listo para producción en lugar de una demostración. Haz bucle sobre todas las tool_calls, no [0], o silenciosamente dejarás caer 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, así que una única llamada mala no aplastaría todo el intercambio.

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

JunoEn la práctica El manejador de producción es el ciclo de múltiples turnos con tres cosas cableadas: haz bucle 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 aplasten la ejecución. Mismo ciclo, más autonomía, y tienes un agente. La arquitectura no cambia, el modelo solo obtiene para decidir su propio siguiente paso.

El manejador de producción pliega todo junto: el ciclo limitado de múltiples turnos, 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 se saltan: cuándo no darle al modelo una herramienta en absoluto.

Una herramienta es la respuesta correcta cuando la acción realmente depende del juicio del modelo sobre lenguaje natural: cuál pedido significa el usuario, si esto cuenta como un caso de reembolso, qué buscar. Es la respuesta incorrecta cuando el camino es determinista, la misma entrada siempre produciendo la misma llamada. Si una solicitud siempre desencadena la misma llamada con los mismos argumentos derivables, cables eso en código y salta la ronda; removes una superficie de alucinación, un salto de latencia, y un costo de token para cero pérdida. Darle al modelo una herramienta compra flexibilidad y paga por ella en no determinismo, así que gástala 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 cuando sí usas herramientas:

  • Restringe alucinación de llamadas a herramientas: un modelo puede inventar una llamada a una herramienta que nunca definiste o fabricar argumentos, así que rechaza nombres de herramientas desconocidos de plano, 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 debería dispararse, en lugar de dejar cada turno al azar.
  • Mantén el rastreo de observabilidad, herramienta, argumentos, resultado, conteo de turno, porque un ciclo autónomo es solo depurable si puedes repetir qué realmente hizo.

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

JunoEn la práctica El manejador completo es el ciclo limitado con ejecuciones de herramientas concurrentes, validadas, registradas y errores como datos. La pregunta saltada es cuándo no darle una herramienta: si la llamada es determinista, cables eso en código y suelta la ronda, 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 rastreo, 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í.