1. Errores e idempotencia en ChatGPT App
En la web clásica muchos siguen viviendo en la paradoja de «el usuario pulsa un botón → una petición HTTP → una respuesta». En el mundo LLM esto hace tiempo que no es así. El modelo puede decidir llamar a tu herramienta varias veces, puede regenerar la respuesta tras que el usuario pulse Regenerate, puede repreguntar o tropezar con un error de red por el camino. Como resultado, una misma herramienta puede invocarse dos o tres veces con argumentos muy parecidos.
Al mismo tiempo, cualquier error de repente tiene dos consumidores. Por un lado — el modelo, que necesita una explicación legible por máquina de qué ha fallado para poder corregir los argumentos e intentarlo de nuevo. Por otro — el UI del usuario (el widget y el propio chat), donde hay que mostrar un mensaje humano y sugerir acciones siguientes, y no «Error: 500 (see logs)».
Otro punto importante: la arquitectura clásica rara vez presupone que alguien vaya a pulsar masivamente «repetir respuesta» y, con ello, aumentar el número de peticiones repetidas (reintentos). En ChatGPT este escenario es la norma. Además, la plataforma puede repetir la llamada ante problemas temporales de red. Por eso, el concepto de idempotencia en este ecosistema no es una opción adicional, sino un requisito básico, especialmente para herramientas que hacen cosas «de verdad»: crean pedidos, cargan dinero, envían correos, etc.
Esta lección trata precisamente de cómo evitar que una llamada fallida de la herramienta (tool call) arruine la experiencia del usuario y tu producción.
Insight
ChatGPT no pasa argumentos a tus funciones, sino que más bien adivina el conjunto de argumentos según tu esquema. Observa el JSON Schema, el contexto del diálogo y selecciona valores de forma estadística — y falla con bastante frecuencia. Errores como «tipo incorrecto», «campo obligatorio olvidado», «parámetros contradictorios» son parte normal de la vida de los tool-calls, no un caso de fuerza mayor. Según datos públicos y telemetría, esos fallos pueden fácilmente ocupar hasta ~30% de las llamadas para esquemas complejos.
Para el modelo no es un problema: interpreta tu respuesta como la señal de que «los argumentos eran malos» y simplemente lo intenta de nuevo, quizá dos o tres veces seguidas, cambiando ligeramente la entrada. Para ti significa otra cosa: hay que diseñar cada herramienta como si casi seguro la fuesen a llamar varias veces con parámetros muy parecidos.
Por eso la idempotencia es tan importante. ChatGPT intentará una y otra vez adivinar con qué parámetros debe llamar a tus funciones. Dos o tres intentos por llamada son la norma.
2. Configuración segura del widget: text/html+skybridge y _meta
Antes de pasar a las cuestiones puramente de servidor (errores, reintentos, idempotencia), cerremos un punto específico del Apps SDK sobre seguridad de UI: cómo hacer que tu widget se renderice de forma segura en el chat y no como «una página terrible de internet».
registerResource y el tipo MIME text/html+skybridge
Tu widget desde el punto de vista de ChatGPT es un recurso HTML especial que se ejecuta en la sandbox del cliente de ChatGPT, no directamente en el navegador del usuario. Para que la plataforma entienda que es un widget y no simple HTML, se utiliza el tipo MIME text/html+skybridge.
A nivel de MCP/servidor registras el recurso con algo como (pseudo-TS):
// en algún lugar de la configuración del servidor MCP
registerResource({
name: "giftgenius-widget",
path: "/widget",
mimeType: "text/html+skybridge", // ¡importante!
});
Este mimeType es una señal para el cliente de ChatGPT: «esto no es solo HTML, sino un template de componente para un widget embebido que debe ejecutarse en un entorno aislado». Si indicas un text/html normal, la plataforma puede mostrar HTML en bruto o incluso negarse a renderizarlo.
_meta y el control de seguridad: CSP, dominio y borde
Luego entran en juego los metadatos que se envían junto con la respuesta de la herramienta o del recurso — _meta. A través de ellos controlas qué recursos externos puede cargar el widget, cómo se comporta visualmente e incluso cómo lo describirá el modelo.
Ejemplo típico de la estructura:
const toolResult = {
content: "<!-- HTML del widget -->",
_meta: {
"openai/widgetCSP": "default-src 'self'; img-src https://cdn.example.com",
"openai/widgetDomain": "https://chatgpt.com",
"openai/widgetPrefersBorder": true,
"openai/widgetDescription": "GiftGenius muestra recomendaciones de regalos en forma de tarjetas."
}
};
Veamos los campos clave.
- openai/widgetCSP define la Content Security Policy del widget. Es tu pequeño cortafuegos para el navegador dentro de ChatGPT: enumeras explícitamente desde dónde se pueden cargar scripts, estilos, imágenes, hacer XHR, etc. La plataforma espera una política estricta sin el comodín *; debes indicar explícitamente los dominios usados (el chat, tu API, el CDN).
- openai/widgetDomain establece el origin en cuyo contexto funcionará tu widget. Normalmente es el dominio de ChatGPT; no lo sustituyes por tu sitio, solo indicas cómo debe verse en el entorno aislado.
- openai/widgetPrefersBorder es un indicador puramente visual: si dibujar un borde alrededor del widget. Para GiftGenius es razonable dejar el borde para separar visualmente el bloque de recomendaciones de los mensajes normales del chat.
- openai/widgetDescription es una descripción textual para el modelo. En lugar de «inventar» la explicación por sí misma, el modelo puede usar esta cadena cuando le cuenta al usuario qué interfaz se ha abierto. Esto reduce el riesgo de comentarios raros o redundantes del modelo.
Conclusión práctica: si configuras cuidadosamente una vez el mimeType y el _meta, obtienes una UI segura y aislada que no accede donde no debe y se comporta de forma predecible tanto para el usuario como para la plataforma. Cerrada la parte de seguridad del frontend: el widget vive en una sandbox y solo accede a donde tú le permites. Ahora nos centramos en el lado del servidor: tipos de errores, cómo describirlos y cómo hacer que las herramientas sean idempotentes.
Insight: caché del widget
ChatGPT hace caché del HTML del widget en el momento del registro de la aplicación. El HTML-widget de ChatGPT no es un «frontend vivo», sino un artefacto de build fijado. Al publicar la aplicación (Store o Dev Mode) la plataforma lee el recurso HTML (text/html+skybridge) y después siempre usa esa versión. Cualquier cambio — incluso una línea de texto o un margen en una tarjeta — equivale de facto a un nuevo release.
De aquí se desprende: cambios en la estructura HTML, slots, atributos data-* y el contrato structuredContent → DOM no son un «fix rápido», sino una migración de frontend completa. Si hoy renderizas una lista de items[] y mañana pasas a results[], el widget antiguo no se enterará: seguirá recibiendo el JSON anterior y funcionará de forma incorrecta.
3. Tipos de errores en el trabajo de las herramientas
Pasemos ahora a lo sustancial: qué tipos de errores puede tener una herramienta y en qué se diferencian desde el punto de vista del UX y del backend. Es útil pensar en cuatro capas de errores.
Errores de validación de entrada
El nivel más básico es cuando los argumentos de entrada no se ajustan al contrato.
Ejemplos para nuestra app didáctica GiftGenius y su herramienta suggest_gifts (selección de regalos por intereses y presupuesto):
- edad menor que cero o mayor que 120;
- presupuesto negativo;
- falta el campo obligatorio relationship_type;
- budget_min > budget_max.
Aquí también entra un JSON banal que no coincide con el esquema. Idealmente, el Apps SDK y el JSON Schema filtrarán las llamadas «muy malas» antes de tu código, pero la validación de negocio (como la relación entre budget_min/budget_max) tendrás que hacerla tú igualmente.
Errores de lógica de negocio
Aquí la entrada parece correcta, pero por reglas de dominio no puedes dar un resultado adecuado.
Giros típicos de la trama:
- con los intereses y presupuesto dados no encuentras ningún regalo;
- el usuario superó el límite diario de selecciones;
- el producto que el modelo pide comprar ya no se vende.
No es que «el servidor se haya roto», son situaciones normales y esperadas que deben presentarse al usuario y al modelo de forma digerible, y no como un 500 Internal Server Error.
Errores de infraestructura externa
Esta capa ya trata del «infierno técnico»: base de datos no disponible, API externo con timeout, una excepción no controlada dentro de tu código.
Por ejemplo:
- la petición al catálogo de regalos devuelve 503 o no responde;
- MongoDB de pronto decide pararse;
- en el código de filtrado de regalos divides por cero.
Desde el punto de vista del UX esto suele ser motivo para decir: «El servicio no está disponible temporalmente, inténtalo más tarde», y a veces intentar un retry oculto. Pero es importante no quedarse callado ni mostrar al usuario un stack trace en bruto.
Errores de plataforma/red
Y por último, está la capa que puede ocurrir fuera de tu código: el tool-call no llegó, la conexión se cortó a mitad de respuesta, el escenario de streaming se interrumpió. Pasa más a menudo de lo que crees. Por ejemplo, si usas un túnel gratuito, durante horas pico su velocidad cae tanto que los tool calls de ChatGPT expiran por timeout.
No puedes controlarlo por completo, pero sí puedes diseñar las herramientas y el widget de modo que las llamadas repetidas y las interrupciones no conviertan el sistema en un caos. Precisamente por eso hablamos de idempotencia y de un tratamiento cuidadoso de los errores, y no simplemente de «try/catch y nos olvidamos».
4. Cómo describir y devolver errores: para el modelo y para el UI
Un cambio de mentalidad importante: tu error no es solo lo que has registrado en console.error. Es parte del contrato de la herramienta, con la que trabajarán tanto el modelo como la interfaz.
Estructura del error
Suele ser útil seguir una estructura simple:
type ToolError = {
code: string; // "VALIDATION_ERROR", "NO_RESULTS", "UPSTREAM_TIMEOUT"
message: string; // legible para humanos o compacto para el modelo
retryable: boolean; // si tiene sentido reintentar
};
Y el resultado de la herramienta puede envolverse en una unión discriminada:
type SuggestGiftsResult =
| { ok: true; gifts: GiftCard[] }
| { ok: false; error: ToolError };
En el protocolo MCP hay además una marca separada de «esto es un error», pero internamente es útil mantener tu propio formato para que el UI y el modelo puedan interpretar por igual qué ocurrió.
Estrategia «fail gracefully»
No toda situación desagradable debe presentarse como un error «duro». A veces es más útil devolver un resultado vacío, pero sin error, solo con una explicación.
Por ejemplo, si no se encuentran regalos, tiene sentido devolver ok: true, un array vacío gifts: [] y algún campo noResultsReason para el UI y el modelo, en lugar de "NO_RESULTS" como error. Así el modelo puede continuar el diálogo: «No he encontrado nada en este presupuesto, ¿quieres subir el presupuesto o concretar intereses?».
Pero si el API externo está completamente caído, entonces sería más bien ok: false con code: "UPSTREAM_UNAVAILABLE" y retryable: true, para que el modelo tenga oportunidad de reintentar más tarde o con otros parámetros.
Recordemos: en la sección 3 tenemos cuatro capas de errores. Los errores de validación suelen ir como ok: false y retryable: false — el modelo no debería repetir la misma llamada con los mismos argumentos. Situaciones de negocio como «nada encontrado» se representan más a menudo como ok: true con resultado vacío y una explicación. Los fallos de infraestructura de servicios externos — como ok: false con retryable: true, para que el modelo pueda reintentar con seguridad. Y los errores de plataforma/red pueden ocurrir antes o después de tu código, y en la práctica a menudo se manifiestan como una llamada repetida de la herramienta — por eso la idempotencia cuidadosa es tan importante, de la que hablaremos a continuación.
No filtramos detalles internos
En el código del servidor es fácil caer en la tentación de simplemente pasar error.toString() a la respuesta. Para herramientas LLM no es una buena idea: obtendrás basura en el diálogo y potencialmente expondrás detalles sensibles (URLs de servicios internos, stack traces, nombres de tablas). La recomendación es interceptar las excepciones y convertirlas en códigos de error compactos y mensajes cuidados.
Ejemplo de envoltura mínima:
try {
const gifts = await loadGiftsFromCatalog(input);
return { ok: true, gifts };
} catch (err) {
console.error("suggest_gifts failed", err);
return {
ok: false,
error: {
code: "UPSTREAM_ERROR",
message: "Catalog service is unavailable",
retryable: true
}
};
}
El modelo ve una señal limpia, el UI — un texto comprensible, y los detalles quedan en los logs.
Representación del error en el widget
Desde el punto de vista de un widget React, la tarea es trivial: comprobar ok y, si es false, mostrar un mensaje amigable y, si es posible, una forma de continuar.
function GiftResults({ result }: { result: SuggestGiftsResult }) {
if (!result.ok) {
return (
<div>
<p>No se pudieron seleccionar regalos: {result.error.message}</p>
{result.error.retryable && <p>Prueba a cambiar los parámetros o a repetir la solicitud.</p>}
</div>
);
}
if (result.gifts.length === 0) {
return <p>No se encontraron regalos con esas condiciones. Prueba a ajustar el presupuesto o los intereses.</p>;
}
return <GiftCardsList gifts={result.gifts} />;
}
Este es precisamente el caso en el que un mensaje simple y honesto hace el UX mucho más agradable que «algo salió mal».
Ya hemos acordado que algunas clases de errores se pueden marcar honestamente como retryable: true y proponer al usuario «intentar de nuevo». En cuanto aparecen estos reintentos (explícitos en el UI u ocultos por parte de la plataforma), surge la siguiente pregunta: ¿qué pasa si se llama dos veces a la misma herramienta con los mismos datos? Esto ya va de idempotencia.
5. Idempotencia: protección frente a «otro llamado igual»
Ahora a la parte más divertida. Formalmente, la idempotencia es la propiedad de una operación por la cual una llamada repetida con los mismos datos de entrada no cambia el estado del sistema ni el resultado. En sentido estricto, trata tanto de la ausencia de efectos secundarios repetidos como de una respuesta igual. En la práctica de ChatGPT Apps nos interesa sobre todo lo primero: que las llamadas repetidas no estropeen los datos ni creen nuevas entidades, aunque la respuesta pueda variar ligeramente.
En el contexto de ChatGPT Apps, la idempotencia es la protección contra todo lo que ocurre con los reintentos, Regenerate y la lógica impredecible de las LLM.
Dónde es especialmente importante la idempotencia
Las herramientas solo de lectura son generalmente seguras por defecto: por mucho que llames a suggest_gifts con los mismos parámetros, simplemente obtendrás otra lista de regalos. Aunque difiera un poco, no cambia el estado del sistema ni crea efectos secundarios.
Son críticas las herramientas que modifican el estado de sistemas externos:
- creación de pedido (create_order);
- realización de cargo (charge_card, submit_payment);
- envío de correos y notificaciones (send_email, send_sms);
- creación de entidades con efectos secundarios (por ejemplo, reservas).
Si se llama dos veces seguidas a una herramienta así con argumentos prácticamente idénticos, pueden aparecer pedidos duplicados, cargos dobles y otras «alegrías» contables.
Patrón idempotency_key
El enfoque clásico: añadir a la herramienta un parámetro adicional idempotency_key — un identificador de cadena de la operación. Si ya se ha atendido con éxito una solicitud con esa clave, el servidor no ejecuta la acción otra vez, sino que devuelve el resultado guardado.
Ejemplo de esquema ampliado para la herramienta hipotética create_checkout_session en GiftGenius:
const CreateCheckoutSchema = {
type: "object",
properties: {
giftId: {
type: "string",
description: "ID del regalo seleccionado"
},
idempotency_key: {
type: "string",
description: "Clave única de la operación para evitar duplicados"
}
},
required: ["giftId", "idempotency_key"]
} as const;
En el servidor el manejador hace algo así:
async function createCheckoutSession(input: CreateCheckoutInput) {
const existing = await db.checkoutSessions.findOne({ idempotencyKey: input.idempotency_key });
if (existing) {
return existing; // devolvemos el resultado anterior
}
const session = await paymentProvider.createSession({ giftId: input.giftId });
await db.checkoutSessions.insert({ idempotencyKey: input.idempotency_key, session });
return session;
}
Si el modelo por alguna razón llama a la herramienta por segunda vez con el mismo idempotency_key, el usuario no recibirá un segundo cobro, sino que verá el mismo checkout.
Separación de prepare y commit
Para acciones especialmente sensibles (pagos, cambios irreversibles) se usa a menudo un enfoque bifásico: una herramienta para preparar (prepare_*) y otra para el commit (commit_*).
Por ejemplo:
- prepare_order — comprueba disponibilidad, calcula el coste y devuelve un «borrador de pedido»;
- commit_order — a partir del ID del borrador crea el pedido real e inicia el pago.
Este diseño aporta varias ventajas. Primero, puedes hacer el primer paso completamente idempotente: una prepare_order repetida con los mismos parámetros devolverá el mismo borrador. Segundo, commit_order puede permitirse solo después de la confirmación explícita del usuario, lo cual es cómodo tanto para el UX como para la seguridad.
6. Diseño seguro de herramientas
La idempotencia es necesaria, pero no es el único ingrediente de seguridad. Mucho depende del propio diseño del conjunto de herramientas que expones al modelo.
Principio de mínimos privilegios
La idea es simple: cada herramienta debe hacer exactamente lo que necesita el escenario y ni una línea más. No hace falta una función única do_anything_with_user_account que:
- puede leer, actualizar y borrar todo sin restricción;
- acepta una cadena operation y un JSON payload «a la buena de Dios».
Es mejor tener tools separados y bien descritos:
- get_user_profile;
- update_user_preferences;
- create_order;
- cancel_order.
La misma lógica para GiftGenius: suggest_gifts solo sugiere opciones; create_checkout_session no sabe nada de cómo cancelar pedidos o cambiar el e‑mail del usuario.
Separación de herramientas de «lectura» y de «escritura»
Un buen patrón es separar claramente las herramientas que solo leen datos de las que modifican algo. La consulta del catálogo de regalos (search_products, suggest_gifts) es segura por sí misma, incluso si el modelo abusa de ella. En cambio, create_order o charge_payment ya requieren un manejo más cuidadoso.
En las descripciones de estas herramientas conviene indicar explícitamente qué hacen y en qué contexto puede llamárselas. Por ejemplo:
{
"name": "create_checkout_session",
"description": "Crea una nueva sesión de pago para un regalo. LLÁMALO SOLO después de que el usuario haya confirmado explícitamente su elección.",
"parameters": { /* ... */ }
}
No es una protección infalible (la LLM puede equivocarse), pero al menos le das una señal clara sobre los riesgos.
Human-in-the-loop y confirmaciones
Para acciones realmente «peligrosas» es útil construir un flujo con confirmación. Por ejemplo, el modelo:
- Primero llama a una herramienta que prepara los datos para la compra y los devuelve en un formato cómodo para el UI (nombre del regalo, precio, dirección de entrega).
- La plataforma muestra al usuario un widget con el botón «Confirmar compra».
- Solo tras hacer clic en el botón se llama a la herramienta de commit, que realiza el pago real.
Así no das al modelo la posibilidad de tramitar un pedido «a escondidas» sin la participación del usuario, incluso si decide que es una idea muy inteligente.
Semántica del riesgo en descripciones y anotaciones
En algunas versiones de la plataforma aparecen anotaciones especiales como destructiveHint, que señalan que la herramienta puede realizar acciones irreversibles. Incluso si esos campos no existen o aún son inestables, puedes incorporar esta semántica directamente en la description y en los nombres de parámetros.
Por ejemplo, en lugar de:
{
"name": "delete_user_data",
"description": "Elimina los datos del usuario."
}
hacer:
{
"name": "request_user_data_deletion",
"description": "Marca la cuenta del usuario para la eliminación de sus datos personales conforme a la política del servicio. Usa SOLO después de que el usuario lo haya solicitado explícitamente."
}
Y de paso construir alrededor un UX de confirmación humano.
7. Pequeña mejora práctica de GiftGenius
Conectemos todo esto con nuestra app didáctica GiftGenius — una App para seleccionar regalos. Supongamos que añadimos a GiftGenius otra herramienta — create_checkout_session, para que el usuario no solo pueda elegir un regalo, sino pasar a tramitarlo.
Desde el punto de vista de JSON Schema y seguridad hacemos lo siguiente.
Primero, añadimos idempotency_key y una descripción cuidada:
const CreateCheckoutTool = {
name: "create_checkout_session",
description:
"Crea una sesión de pago para un único regalo seleccionado. " +
"Llámalo solo después de que el usuario haya confirmado que quiere comprar ese regalo.",
parameters: {
type: "object",
properties: {
gift_id: {
type: "string",
description: "Identificador del regalo del resultado de suggest_gifts."
},
idempotency_key: {
type: "string",
description: "Clave única de la operación. Usa la misma clave en llamadas repetidas."
}
},
required: ["gift_id", "idempotency_key"]
}
} as const;
Segundo, implementamos en el servidor un manejador idempotente:
async function handleCreateCheckout(input: CreateCheckoutInput) {
const existing = await db.checkout.findOne({ idempotencyKey: input.idempotency_key });
if (existing) {
return { ok: true, checkout: existing };
}
const checkout = await payments.createSession({ giftId: input.gift_id });
await db.checkout.insert({ idempotencyKey: input.idempotency_key, ...checkout });
return { ok: true, checkout };
}
Tercero, contemplamos los errores:
try {
return await handleCreateCheckout(input);
} catch (err) {
console.error("create_checkout_session failed", err);
return {
ok: false,
error: {
code: "PAYMENT_PROVIDER_ERROR",
message: "No se pudo crear la sesión de pago. Inténtalo más tarde.",
retryable: true
}
};
}
Y en el widget mostramos un estado de error entendible y, quizá, un botón «Reintentar» a nivel de UI que inicie un nuevo diálogo con el modelo.
Así, paso a paso, nuestro amable proyecto didáctico deja de ser «un juguete de demo» y poco a poco se convierte en algo que teóricamente se puede lanzar a producción.
8. Errores típicos al trabajar con errores e idempotencia de herramientas
Error n.º 1: Error = simplemente throw y 500.
Si ante cualquier fallo tu tool simplemente lanza una excepción que se convierte en un «algo salió mal», el modelo y el UI se quedan sin información. El modelo no entiende si debe repetir la llamada con otros argumentos y el usuario no sabe qué hacer después. Es mucho mejor devolver un error estructurado con código, mensaje breve e indicador retryable, y registrar los detalles en el servidor.
Error n.º 2: Ausencia de diferencias entre tipos de errores.
Mezclar en un mismo saco errores de validación, de negocio y de infraestructura es mala idea. Acabas haciendo que la situación «nada encontrado» se vea para el modelo y el usuario igual que «se cayó la base de datos». Esto rompe el UX e impide que el modelo reaccione de forma adecuada: en lugar de proponer cambiar la consulta se pasará al modo «perdón, el servicio está roto». Duele especialmente cuando mezclas, por ejemplo, errores de negocio y errores de infraestructura de la sección 3.
Error n.º 3: Operaciones no idempotentes en un mundo de reintentos.
Diseñar la herramienta create_order como si siempre la fuesen a invocar exactamente una vez es el camino directo a los pedidos duplicados, especialmente cuando el usuario pulsa activamente Regenerate o la conexión se corta a mitad de camino. Si la herramienta tiene efectos secundarios, casi siempre conviene añadir idempotency_key y guardar resultados, para que una llamada repetida no cree nuevas entidades.
Error n.º 4: Una herramienta «universal» monstruosa.
A veces los desarrolladores intentan crear una superherramienta con un parámetro action que hace de todo: buscar, crear, modificar y eliminar. Para una LLM es casi garantía de comportamiento impredecible: el modelo aprende peor cuándo llamar a qué, y las consecuencias de los errores se vuelven mucho más graves. Es más correcto dividir en herramientas pequeñas, bien descritas, preferiblemente de solo lectura, y aparte — herramientas de mutación cuidadas con confirmaciones.
Error n.º 5: Filtración de detalles internos en las respuestas.
Volcar al modelo y al UI un stack trace en bruto o textos completos de excepciones es pereza ingenieril típica. Es incómodo para el usuario, puede exponer la estructura interna del sistema y no ayuda al modelo a corregirse. Conviene interceptar las excepciones, mapearlas a códigos compactos y mensajes simples, y dejar todos los detalles en los logs y el sistema de monitorización.
Error n.º 6: No enlazar los errores con el UX del widget.
A menudo la parte de servidor devuelve códigos de error cuidadosamente, pero el widget de UI cae en un spinner eterno o un bloque vacío. El usuario ve «no pasó nada», el modelo ve que el tool-call terminó y continúa el diálogo como si nada. Es mucho mejor pensar estados separados error y empty, mostrar mensajes comprensibles y, si es posible, sugerir acciones (cambiar parámetros, probar más tarde).
Error n.º 7: Ignorar el principio de mínimos privilegios.
Aunque hagas idempotencia y buen manejo de errores, si describes una herramienta como execute_sql_anywhere que puede hacerlo todo, el riesgo sigue siendo enorme. La LLM puede llamarla en el contexto equivocado o con parámetros erróneos. Cada herramienta debe ser lo más estrecha posible y hacer exactamente una acción comprensible — especialmente cuando se trata de dinero o datos personales del usuario.
GO TO FULL VERSION