Supongamos que hemos solicitado a un agente LLM que cree una incidencia. La respuesta indica «Se ha creado la incidencia», pero en el proyecto no aparece ninguna incidencia nueva. Si insistimos, el agente explica que utilizó herramientas.
Antes de cambiar de modelo o añadir un prompt como «nunca mientas», hay una cuestión que conviene verificar: ¿hasta qué fase del proceso de ejecución se llegó realmente?
Las frases del agente por sí solas no nos dan esa respuesta. Debemos diferenciar si se generó una solicitud de llamada a una herramienta, si el ejecutor procesó dicha solicitud y si el resultado quedó registrado en el servicio externo.
1. «Realicé la llamada» amalgama eventos distintos
El proceso de trabajo de un agente que utiliza herramientas sigue, aproximadamente, los siguientes pasos:
- El modelo selecciona las herramientas y los argumentos que va a usar.
- El entorno de ejecución interpreta la solicitud de llamada y verifica los permisos.
- El ejecutor invoca la función real o la API externa.
- El resultado de la herramienta se transmite al modelo.
- Se evalúa si se alcanzó el estado solicitado por el usuario.
Cada una de estas fases puede fallar de forma independiente.
Aunque el modelo haya generado correctamente la solicitud de llamada, puede producirse una excepción en el ejecutor. Incluso si la API acepta la solicitud, la tarea asincrónica puede estar todavía en curso. La ejecución podría haber finalizado, pero la incidencia se creó en un proyecto erróneo.
Por ello, al investigar fallos, es preferible no agrupar todas las fases bajo un único estado de «llamada a herramienta con éxito».
2. Diferenciar entre texto normal y eventos de llamada
Consideremos, a modo de ejemplo, las llamadas a funciones personalizadas en la API Responses de OpenAI. A continuación se muestra un ejemplo simplificado de los campos principales del elemento de llamada.
{
"type": "function_call",
"call_id": "call_42",
"name": "create_ticket",
"arguments": "{\"title\":\"로그인 오류 점검\"}"
}La aplicación recibe este elemento, ejecuta la función correspondiente y asocia el resultado al mismo call_id.
{
"type": "function_call_output",
"call_id": "call_42",
"output": "{\"status\":\"created\",\"ticket_id\":\"T-731\"}"
}En este caso, status e ticket_id son formatos de resultado definidos por la aplicación de ejemplo. No son campos estándar mediante los cuales OpenAI garantice la creación de incidencias.
La diferencia entre esta estructura y la siguiente frase, que podría aparecer en una respuesta normal, es clara:
Se ha creado la incidencia al llamar a
create_ticket.
Esta frase es texto generado por el modelo. Aunque escribamos un nombre de herramienta o un formato de llamada dentro de un bloque de código, la ejecución no se producirá a menos que la implementación haga que el ejecutor la procese como una solicitud de llamada.
Las funciones personalizadas las ejecuta la aplicación, pero las herramientas en el lado del servidor gestionadas por el proveedor pueden tener una ubicación de ejecución distinta. Por tanto, primero debemos verificar dónde se ejecutan las herramientas que utilizamos y qué eventos generan. La documentación de llamadas a funciones de OpenAI y la documentación sobre el uso de herramientas de Claude explican estos límites de ejecución.
3. Síntomas similares, causas distintas
Se genera una frase de finalización sin solicitud de llamada
Si la salida del modelo contiene únicamente mensajes normales y no hay eventos de llamada, verificaremos si el modelo afirma haber completado la acción sin evidencia de ejecución.
Las posibles causas incluyen que la herramienta no esté expuesta para esa solicitud, descripciones ambiguas de la herramienta o la no selección de la llamada. Primero debemos comparar las definiciones de herramientas incluidas en la solicitud real con la salida en bruto del modelo.
Sin embargo, no debemos concluir que no se ejecutó nada solo porque no aparezcan registros en la pantalla de trazas. También hay que revisar omisiones en la recolección de datos, el muestreo y las rutas de ejecución independientes.
Existe una solicitud de llamada, pero el ejecutor la omitió
En este caso, es necesario examinar el flujo de control de la aplicación más que el modelo.
- ¿Se leyó solo el texto de la respuesta y se descartaron los elementos de llamada?
- ¿Se procesó únicamente la primera de varias llamadas?
- ¿Coinciden el nombre de la herramienta y el nombre real del controlador?
- ¿Finalizó el bucle de ejecución mientras estaba pendiente de aprobación?
El streaming (flujo continuo) también es objeto de revisión. Si se tratan fragmentos de argumentos como solicitudes completas o se pierden los eventos de finalización de la llamada, la ejecución puede omitirse. response.function_call_arguments.done es una señal de que la generación de argumentos ha terminado, no de que la ejecución de la función externa haya concluido.
El fallo de ejecución se transmite como un éxito
El código de manejo de excepciones puede borrar el fallo por completo.
try:
result = execute_tool(arguments)
except Exception:
result = {"message": "처리했습니다"}Este código elimina incluso las pistas para investigar la causa raíz. El resultado que se transmite al modelo debe mantener la evidencia del fallo y su ámbito.
En MCP también se diferencia el hecho de haber recibido una respuesta del éxito de la tarea de la herramienta. Por ejemplo, la siguiente respuesta indica un error en la herramienta.
{
"jsonrpc": "2.0",
"id": 17,
"result": {
"content": [
{
"type": "text",
"text": "프로젝트에 이슈를 생성할 권한이 없습니다."
}
],
"isError": true
}
}Si el adaptador transmite solo content y descarta la indicación de error, se pierde información crítica. La especificación de Tools de MCP trata los errores de protocolo y los errores de ejecución de herramientas por separado.
Se interpreta la recepción de la solicitud como la finalización del trabajo
Hay que tener especial cuidado con tareas que constan de varias etapas, como despliegues, conversión de archivos o envío de correos electrónicos.
Si la API de despliegue devolvió un ID de tarea, puede que solo se haya confirmado la «recepción de la solicitud de despliegue». Que el servicio de correo acepte un mensaje no confirma que haya llegado a la bandeja de entrada del destinatario.
Debemos definir por herramienta a qué estado denomina el producto «finalizado».
4. Diferenciar el ámbito que resuelven los prompts y los esquemas
Instrucciones como «no digas que has terminado hasta recibir el resultado de la ejecución» pueden ser útiles. Sin embargo, no pueden reparar el manejo de excepciones del ejecutor ni los flujos de trabajo interrumpidos.
Las configuraciones también tienen su ámbito de aplicación.
tool_choice: "required"exige una llamada a una herramienta.strict: trueayuda a generar argumentos que se ajusten a las restricciones de esquema admitidas.
Ninguna de las dos garantiza el éxito de tareas externas. También hay que verificar por separado si el ID de proyecto, aunque tenga un formato válido, existe realmente y si el usuario tiene permisos para escribir en ese proyecto.
Las configuraciones que obligan a realizar llamadas deben aplicarse a los pasos necesarios, y poder desactivarse en los pasos que describen resultados. Forzar incondicionalmente una llamada en cada turno puede generar repeticiones innecesarias.
5. El estado de finalización se deriva de los registros de ejecución
En la práctica, es útil gestionar el estado del trabajo de forma separada de la respuesta del modelo. Por ejemplo, podemos distinguir los siguientes estados:
requested: Se recibió la solicitud de llamada.waiting_for_approval: A la espera de aprobación de la ejecución.running: La ejecución se inició.succeeded: La herramienta devolvió un resultado de éxito.verified: Se verificaron las condiciones de finalización definidas.failed: Se confirmó el fallo.unknown: Aún no es posible juzgar el resultado de la ejecución.
Estos nombres son ejemplos de diseño. Lo importante es no dejar que el modelo declare los estados de forma arbitraria, sino que el ejecutor y el código de verificación los actualicen basándose en eventos reales.
Las condiciones de finalización para la creación de una incidencia podrían definirse así:
- La API de creación devolvió un ID de incidencia.
- La incidencia consultada mediante ese ID existe en el proyecto solicitado.
- Los campos necesarios, como el título, coinciden con la solicitud.
No es necesario realizar consultas adicionales para todas las herramientas. Debemos decidirlo según qué garantiza la respuesta original y según el riesgo y coste del trabajo. Si el servicio experimenta latencia en la consulta inmediatamente después de guardar, esta característica también debe considerarse.
Para interfaces de usuario importantes, como insignias de finalización, es más seguro renderizar directamente desde este estado. Que el modelo explique los resultados verificados, pero sin convertirse en el único ente que decide si algo está completado.
6. Al encontrar un timeout, verifica antes de reintentar
Supongamos que la solicitud a la API externa finalizó por tiempo de espera agotado (timeout). Es posible que el servidor ya haya creado la incidencia, pero la respuesta se haya perdido.
Si tratamos este estado inmediatamente como un fallo y creamos la incidencia de nuevo, se generará una incidencia duplicada. Primero debe registrarse como unknown, y necesitamos una ruta para verificar el resultado mediante consultas o los identificadores de solicitud del proveedor.
Si el servicio admite claves de idempotencia, debemos usar la misma clave para los reintentos de la misma tarea lógica. El ámbito real de garantía y los periodos de conservación deben seguir la documentación del servicio. La documentación de solicitudes idempotentes de Stripe es un ejemplo concreto.
También hay que distinguir entre el call_id del modelo y las claves de idempotencia de los trabajos empresariales. Si el modelo vuelve a solicitar la misma tarea, puede generar un nuevo ID de llamada; por tanto, un diseño que impida ejecuciones duplicadas basándose únicamente en el ID de llamada puede no ser suficiente.
7. Adjuntar evidencia de ejecución a la evaluación de las respuestas
En las pruebas de agentes, debemos verificar conjuntamente las frases de la respuesta y el estado real.
Podemos crear, como mínimo, las siguientes situaciones:
- ¿No afirma que ha completado la acción cuando no llama a ninguna herramienta?
- ¿No describe un rechazo de permisos como un éxito?
- ¿Se transmite el error de la herramienta al modelo tal cual?
- ¿Marca las tareas asincrónicas como «en curso»?
- ¿Evita ejecuciones duplicadas tras un timeout sin verificar el resultado?
- ¿Asocia los resultados de varias llamadas con las solicitudes correctas?
- ¿No considera finalizados resultados generados en un destino incorrecto?
Es conveniente que las trazas incluyan identificadores que vinculen las llamadas al modelo, las solicitudes de herramientas, el inicio y fin de la ejecución, los errores y los resultados de la verificación. Podemos aprovechar funciones de recolección como las de trazas en OpenAI Agents SDK.
No obstante, no debemos registrar incondicionalmente valores secretos o datos personales en los registros operativos. Registre primero los identificadores y estados necesarios para rastrear el problema, y aplique enmascaramiento y control de acceso a los argumentos y resultados.
Conclusión
Cuando el agente afirma «realicé la llamada», verifiquemos estas tres cuestiones:
- ¿Hubo una solicitud de llamada real?
- ¿Qué resultado registró el ejecutor?
- ¿Ese resultado cumple la condición de finalización solicitada por el usuario?
Distinguir estas tres preguntas permite diagnosticar con mucha mayor precisión las afirmaciones incorrectas del modelo y los fallos de ejecución del entorno de ejecución.
Un agente fiable no se construye solo con frases fluidas de finalización. Diseñe la evidencia de ejecución y las rutas de verificación que respaldan esas frases.




