{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns","title":"Patrones de Function Calling en LLMs para Tool Use Confiable en Agentes de IA","description":"Guía técnica sobre tool use en LLMs, structured output, tool calls paralelos, estrategias de reintento, recuperación de errores, patrones de orquestación y lecciones del desarrollo de agentes de IA en producción.","language":"es","datePublished":"2026-01-17","dateModified":"2026-10-21","contentFormat":"text/markdown","content":"AI/ML\n\n# Patrones de Function Calling en LLMs para Tool Use Confiable en Agentes de IA\n\n2026-01-178 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/llm-function-calling-tool-use-patterns>)\n\n## El Modelo Mental: Las Herramientas Son Output Estructurado con Side Effects\n\nEn su esencia, function calling es simplemente el modelo outputeando JSON en vez de texto. En vez de decir \"Busqué el clima y está a 22 grados\", el modelo outputea:\n\n```json\n{\n \"tool\": \"get_weather\",\n \"arguments\": {\n \"location\": \"Buenos Aires\",\n \"units\": \"celsius\"\n }\n}\n```\n\nTu código ejecuta la función, obtiene el resultado, y se lo devuelve al modelo. Pero aquí está lo que la gente se pierde: el modelo no \"llama\" nada. Genera texto que *parece* un function call. Tu código de orquestación parsea ese texto, ejecuta la función real, y maneja el loop conversacional. El modelo es un motor de generación de JSON con muy buena detección de intenciones. Toda decisión de diseño debería fluir de este entendimiento.\n\n## Diseño de Schema de Herramientas: De Donde Viene el 80% de la Confiabilidad\n\nLa palanca más grande para confiabilidad de tool calls es el schema:\n\n```javascript\n// Malo: demasiada libertad\nconst badTool = {\n name: \"manage_subscription\",\n description: \"Manage a user's subscription\",\n parameters: {\n type: \"object\",\n properties: {\n action: { type: \"string\", description: \"What to do\" },\n details: { type: \"string\", description: \"Additional details\" },\n },\n },\n};\n\n// Bueno: restringido, explícito, con razonamiento\nconst goodTool = {\n name: \"update_subscription\",\n description: \"Cambiar el plan de suscripción de un usuario. Solo llamar cuando el usuario haya solicitado explícitamente un cambio de plan y confirmado el nuevo plan.\",\n parameters: {\n type: \"object\",\n properties: {\n reason: {\n type: \"string\",\n description: \"Por qué se está cambiando esta suscripción (citá el pedido del usuario)\",\n },\n user_id: {\n type: \"string\",\n description: \"El ID del usuario del contexto de la conversación\",\n pattern: \"^usr_[a-zA-Z0-9]{20}$\",\n },\n new_plan: {\n type: \"string\",\n enum: [\"starter\", \"professional\", \"enterprise\"],\n },\n billing_cycle: {\n type: \"string\",\n enum: [\"monthly\", \"annual\"],\n },\n effective_date: {\n type: \"string\",\n enum: [\"immediate\", \"next_billing_cycle\"],\n },\n },\n required: [\"reason\", \"user_id\", \"new_plan\", \"billing_cycle\", \"effective_date\"],\n },\n};\n```\n\nPatrones clave:\n\n- **Enums sobre texto libre**: Todo campo que tenga un set conocido de valores válidos debería ser un enum.\n- **Campo reason**: Fuerza al modelo a articular su razonamiento antes de los parámetros de acción. Funciona como chain-of-thought prompting pero dentro del structured output.\n- **Patrones regex**: Para IDs, emails, fechas, restringí el formato.\n- **Constraints descriptivas**: \"Solo llamar cuando el usuario haya solicitado explícitamente...\" le da al modelo criterio claro.\n\n## El Loop de Orquestación: Más Allá del Single-Shot\n\n```javascript\nasync function runAgent(messages, tools, maxIterations = 10) {\n let iterations = 0;\n\n while (iterations < maxIterations) {\n iterations++;\n\n const response = await callModel({\n messages,\n tools,\n tool_choice: 'auto',\n });\n\n if (response.stop_reason === 'end_turn') {\n return {\n response: response.content,\n toolCalls: messages.filter(m => m.role === 'tool_call'),\n };\n }\n\n if (response.stop_reason === 'tool_use') {\n const toolCalls = response.tool_calls;\n\n messages.push({\n role: 'assistant',\n content: response.content,\n tool_calls: toolCalls,\n });\n\n const toolResults = await executeToolCalls(toolCalls, {\n parallel: canRunInParallel(toolCalls),\n });\n\n for (const result of toolResults) {\n messages.push({\n role: 'tool',\n tool_call_id: result.id,\n content: JSON.stringify(result.output),\n });\n }\n\n continue;\n }\n\n throw new AgentError(`Stop reason inesperado: ${response.stop_reason}`);\n }\n\n throw new AgentError(`El agente superó el máximo de iteraciones (${maxIterations})`);\n}\n```\n\nEl guard de ` maxIterations ` es crítico. Sin él, un modelo confundido puede loopear forever, llamando a una herramienta, recibiendo un error, probando una variación ligeramente distinta, recibiendo el mismo error, ad infinitum. Se han observado agentes quemar miles de API calls en minutos sin este guard.\n\n## Tool Calls Paralelos: Cuando el Modelo se Pone Ambicioso\n\nLos modelos modernos pueden pedir múltiples tool calls en una sola respuesta:\n\n```javascript\nasync function executeToolCalls(toolCalls, options = {}) {\n const { parallel, sequential } = classifyToolCalls(toolCalls);\n const results = [];\n\n if (parallel.length > 0) {\n const parallelResults = await Promise.allSettled(\n parallel.map(call => executeSingleTool(call))\n );\n\n for (let i = 0; i < parallelResults.length; i++) {\n const result = parallelResults[i];\n results.push({\n id: parallel[i].id,\n output: result.status === 'fulfilled'\n ? result.value\n : { error: true, message: result.reason.message, retryable: isRetryable(result.reason) },\n });\n }\n }\n\n for (const call of sequential) {\n try {\n const output = await executeSingleTool(call);\n results.push({ id: call.id, output });\n } catch (err) {\n results.push({ id: call.id, output: { error: true, message: err.message } });\n }\n }\n\n return results;\n}\n\nfunction classifyToolCalls(toolCalls) {\n const writeTools = new Set(['update_subscription', 'create_order', 'send_email']);\n const parallel = [];\n const sequential = [];\n\n for (const call of toolCalls) {\n if (writeTools.has(call.function.name)) {\n sequential.push(call); // Las mutaciones van secuenciales\n } else {\n parallel.push(call); // Las lecturas se pueden paralelizar\n }\n }\n\n return { parallel, sequential };\n}\n```\n\n## Manejo de Errores: Enseñándole al Modelo a Recuperarse\n\nCuando un tool call falla, teners que decirle al modelo qué pasó de una forma que lo ayude a recuperarse:\n\n```javascript\nasync function executeSingleTool(call) {\n const tool = toolRegistry.get(call.function.name);\n\n if (!tool) {\n return {\n error: true,\n message: `Herramienta desconocida \"${call.function.name}\". Herramientas disponibles: ${[...toolRegistry.keys()].join(', ')}`,\n };\n }\n\n try {\n const args = JSON.parse(call.function.arguments);\n const validation = validateArgs(tool.schema, args);\n if (!validation.valid) {\n return {\n error: true,\n message: `Argumentos inválidos: ${validation.errors.join('; ')}`,\n hint: `Schema esperado: ${JSON.stringify(tool.schema.properties)}`,\n };\n }\n\n const result = await tool.execute(args);\n return { success: true, data: result };\n } catch (err) {\n if (err instanceof NotFoundError) {\n return {\n error: true,\n message: `${call.function.arguments.id} no fue encontrado. Puede haber sido eliminado o el ID puede ser incorrecto.`,\n suggestion: 'Intentá buscar el recurso por nombre en vez de ID.',\n };\n }\n logger.error('Fallo en ejecución de herramienta', { tool: call.function.name, error: err });\n return {\n error: true,\n message: 'Ocurrió un error interno. Probar un approach diferente o pedile más info al usuario.',\n };\n }\n}\n```\n\nLa clave: los mensajes de error son prompts. El modelo los lee y decide qué hacer. \"Ocurrió un error\" no le da nada al modelo. \"Usuario no encontrado, intentá buscar por nombre en vez de ID\" le da un camino de recuperación.\n\n## El Patrón de Confirmación: Humanos en el Loop\n\nPara acciones de alto impacto (plata, borrado de datos, comunicaciones externas), nunca dejes que el agente ejecute directamente:\n\n```javascript\nconst DANGEROUS_TOOLS = new Set([\n 'send_email', 'process_refund', 'delete_account',\n 'update_billing', 'create_order',\n]);\n\nasync function executeWithConfirmation(call, context) {\n if (!DANGEROUS_TOOLS.has(call.function.name)) {\n return executeSingleTool(call);\n }\n\n return {\n requires_confirmation: true,\n tool: call.function.name,\n arguments: JSON.parse(call.function.arguments),\n summary: await generateHumanReadableSummary(call),\n confirmation_token: generateToken(call),\n };\n}\n```\n\nEste patrón no es negociable para agentes en producción. No importa qué tan bueno sea el modelo, siempre va a haber situaciones ambiguas donde un humano debería decidir.\n\n## El Patrón de Routing: Escalando Más Allá de 20 Herramientas\n\nCuando teners más herramientas de las que un modelo puede elegir confiablemente, utilizar un approach de dos etapas:\n\n```javascript\nconst TOOL_CATEGORIES = {\n billing: ['get_invoices', 'create_refund', 'update_payment_method'],\n support: ['create_ticket', 'get_ticket_status', 'escalate_ticket'],\n account: ['get_profile', 'update_profile', 'reset_password'],\n product: ['search_products', 'get_product_details', 'check_inventory'],\n};\n\nasync function routedAgent(messages) {\n // Primero, determinamos la categoría\n const routingResponse = await callModel({\n messages,\n tools: routerTools,\n tool_choice: { type: \"function\", function: { name: \"select_tool_category\" } },\n });\n\n const category = routingResponse.tool_calls[0].function.arguments.category;\n const relevantTools = TOOL_CATEGORIES[category].map(name => toolRegistry.get(name));\n\n // Después, corremos el agente solo con las herramientas relevantes\n return runAgent(messages, relevantTools);\n}\n```\n\nEste patrón de dos etapas mantiene cada llamada al modelo enfocada en un set chico de herramientas, mejorando dramáticamente la precisión de selección.\n\n## Lecciones Clave del Desarrollo de Agentes en Producción\n\n1. **El diseño de schema es tu palanca principal.** Gastá más tiempo diseñando schemas de herramientas que escribiendo código de orquestación. Enums restringidos, campos de reason requeridos, y descripciones claras hacen más por la confiabilidad que cualquier lógica de retry.\n2. **El modelo no es tu ejecutor, es tu planificador.** No le des al modelo herramientas que hagan acciones atómicas e irreversibles. Dale herramientas que preparen acciones, y dejar que tu código maneje la ejecución con las salvaguardas adecuadas.\n3. **Los mensajes de error son prompts.** Cada string que tu herramienta devuelve es un prompt para el modelo. Escribir mensajes de error como instrucciones de recuperación, no como output de debugging para devs.\n4. **Los tool calls paralelos son una feature, no un bug.** Diseñá tus herramientas para ser paralelizables por defecto. Las operaciones de lectura siempre deberían ser seguras de paralelizar.\n5. **Logueá todo.** La primera vez que debugguees un problema de agente en producción sin logs de conversación completos, nunca más se va a saltear el logging.\n6. **El guard de iteraciones máximas no es opcional.** Los modelos en loops pueden quemar tu presupuesto de API en minutos. Siempre colocar un cap de iteraciones y siempre tener un circuit breaker de costos.\n\nEl tooling está mejorando rápido. Los modos de structured output hacen que la compliance de schema esté garantizada. Pero el desafío fundamental permanece: se está construyendo un sistema donde un generador de texto probabilístico toma decisiones sobre operaciones determinísticas. Respetá esa brecha, diseñá para ella, y tus agentes van a ser los que realmente funcionan en producción.\n\n## Evaluación reproducible: instrucciones hostiles y permisos\n\n[Agent Injection Evals](<https://lucioduran.com/es/projects/agent-injection-evals>) compara prompts con y sin aislamiento de instrucciones, cruzados con políticas de herramientas permisivas o limitadas a la tarea. Usa el mismo modelo local fijado y conserva las 120 generaciones de evaluación, incluidos los errores de formato y las tareas fallidas.\n\nEl aislamiento permitió cinco escrituras indebidas; la estrategia combinada intentó las mismas cinco y la política las bloqueó. Ninguna estrategia completó las seis tareas limpias. Presentar ese cero de ataques como una defensa ganadora sería esconder el problema: el agente tampoco estaba haciendo su trabajo. La ficha separa capacidad, intentos, efectos, llamadas bloqueadas y recursos consumidos. Son herramientas simuladas y un conjunto sintético pequeño, no una validación de seguridad para cualquier agente.\n\n## Preguntas frecuentes\n\n### ¿Cuál es la diferencia entre function calling y tool use?\n\nEs lo mismo con diferente branding. OpenAI lo llama 'function calling' (ahora 'tool use'), Anthropic lo llama 'tool use'. El mecanismo es idéntico: el modelo outputea un objeto JSON estructurado indicando qué función llamar con qué argumentos, y tu código la ejecuta.\n\n### ¿Puedo confiar en que los LLMs generen JSON válido para tool calls?\n\nCon modos de structured output (modo strict de OpenAI, tool\\_use de Anthropic), el schema JSON está garantizado de ser válido. Los valores dentro del JSON todavía pueden estar mal, el modelo puede llamar la función correcta con un ID alucinado, pero la estructura va a matchear tu schema.\n\n### ¿Cuántas herramientas le puedo dar a un modelo?\n\nEn la práctica, 10-20 herramientas funcionan bien. Más de 30, la precisión empieza a degradarse. Si se necesita más, utilizar una capa de routing, que un modelo elija una categoría de herramienta, después una segunda llamada solo con las herramientas relevantes.\n\n## Tips\n\n1. **Tip:** Definir parámetros de herramientas con restricciones de ` enum ` donde sea posible en lugar de cadenas de texto libre. Un LLM eligiendo de ` [\"buy\", \"sell\", \"hold\"] ` es significativamente más confiable que interpretar intención en lenguaje natural.\n2. **Tip:** Incluir siempre un parámetro ` reason ` en los schemas de herramientas. Forzar al modelo a articular *por qué* está llamando a una herramienta antes de proporcionar los argumentos reduce de forma medible los parámetros alucinados.\n3. **Tip:** Configurar ` tool_choice: 'required' ` solo cuando se tenga certeza de que el mensaje del usuario necesita un tool call. Dejar en ` 'auto' ` por defecto para evitar que el modelo fuerce tool calls innecesarios en mensajes conversacionales.\n4. **Tip:** Registrar cada tool call con la respuesta completa del modelo, no solo los parámetros extraídos. Al depurar problemas en producción, se necesita la cadena de pensamiento del modelo, no solo lo que decidió llamar.\n\n- llm\n- function-calling\n- tool-use\n- ai-agents\n- structured-output\n- openai\n- anthropic\n- langchain\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Patrones de Function Calling en LLMs para Tool Use Confiable en Agentes de IA","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#title"},{"level":2,"text":"El Modelo Mental: Las Herramientas Son Output Estructurado con Side Effects","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#el-modelo-mental-las-herramientas-son-output-estructurado-con-side-effects"},{"level":2,"text":"Diseño de Schema de Herramientas: De Donde Viene el 80% de la Confiabilidad","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#diseo-de-schema-de-herramientas-de-donde-viene-el-80-de-la-confiabilidad"},{"level":2,"text":"El Loop de Orquestación: Más Allá del Single-Shot","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#el-loop-de-orquestacin-ms-all-del-single-shot"},{"level":2,"text":"Tool Calls Paralelos: Cuando el Modelo se Pone Ambicioso","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#tool-calls-paralelos-cuando-el-modelo-se-pone-ambicioso"},{"level":2,"text":"Manejo de Errores: Enseñándole al Modelo a Recuperarse","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#manejo-de-errores-ensendole-al-modelo-a-recuperarse"},{"level":2,"text":"El Patrón de Confirmación: Humanos en el Loop","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#el-patrn-de-confirmacin-humanos-en-el-loop"},{"level":2,"text":"El Patrón de Routing: Escalando Más Allá de 20 Herramientas","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#el-patrn-de-routing-escalando-ms-all-de-20-herramientas"},{"level":2,"text":"Lecciones Clave del Desarrollo de Agentes en Producción","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#lecciones-clave-del-desarrollo-de-agentes-en-produccin"},{"level":2,"text":"Evaluación reproducible: instrucciones hostiles y permisos","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#evaluacin-reproducible-instrucciones-hostiles-y-permisos"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#questions"},{"level":3,"text":"¿Cuál es la diferencia entre function calling y tool use?","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#question-1"},{"level":3,"text":"¿Puedo confiar en que los LLMs generen JSON válido para tool calls?","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#question-2"},{"level":3,"text":"¿Cuántas herramientas le puedo dar a un modelo?","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#article-tips"}],"sections":[{"heading":"Patrones de Function Calling en LLMs para Tool Use Confiable en Agentes de IA","level":1,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns","content":"AI/ML"},{"heading":"Patrones de Function Calling en LLMs para Tool Use Confiable en Agentes de IA","level":1,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#title","content":"2026-01-178 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/llm-function-calling-tool-use-patterns>)"},{"heading":"El Modelo Mental: Las Herramientas Son Output Estructurado con Side Effects","level":2,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#el-modelo-mental-las-herramientas-son-output-estructurado-con-side-effects","content":"En su esencia, function calling es simplemente el modelo outputeando JSON en vez de texto. En vez de decir \"Busqué el clima y está a 22 grados\", el modelo outputea:\n\n```json\n{\n \"tool\": \"get_weather\",\n \"arguments\": {\n \"location\": \"Buenos Aires\",\n \"units\": \"celsius\"\n }\n}\n```\n\nTu código ejecuta la función, obtiene el resultado, y se lo devuelve al modelo. Pero aquí está lo que la gente se pierde: el modelo no \"llama\" nada. Genera texto que *parece* un function call. Tu código de orquestación parsea ese texto, ejecuta la función real, y maneja el loop conversacional. El modelo es un motor de generación de JSON con muy buena detección de intenciones. Toda decisión de diseño debería fluir de este entendimiento."},{"heading":"Diseño de Schema de Herramientas: De Donde Viene el 80% de la Confiabilidad","level":2,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#diseo-de-schema-de-herramientas-de-donde-viene-el-80-de-la-confiabilidad","content":"La palanca más grande para confiabilidad de tool calls es el schema:\n\n```javascript\n// Malo: demasiada libertad\nconst badTool = {\n name: \"manage_subscription\",\n description: \"Manage a user's subscription\",\n parameters: {\n type: \"object\",\n properties: {\n action: { type: \"string\", description: \"What to do\" },\n details: { type: \"string\", description: \"Additional details\" },\n },\n },\n};\n\n// Bueno: restringido, explícito, con razonamiento\nconst goodTool = {\n name: \"update_subscription\",\n description: \"Cambiar el plan de suscripción de un usuario. Solo llamar cuando el usuario haya solicitado explícitamente un cambio de plan y confirmado el nuevo plan.\",\n parameters: {\n type: \"object\",\n properties: {\n reason: {\n type: \"string\",\n description: \"Por qué se está cambiando esta suscripción (citá el pedido del usuario)\",\n },\n user_id: {\n type: \"string\",\n description: \"El ID del usuario del contexto de la conversación\",\n pattern: \"^usr_[a-zA-Z0-9]{20}$\",\n },\n new_plan: {\n type: \"string\",\n enum: [\"starter\", \"professional\", \"enterprise\"],\n },\n billing_cycle: {\n type: \"string\",\n enum: [\"monthly\", \"annual\"],\n },\n effective_date: {\n type: \"string\",\n enum: [\"immediate\", \"next_billing_cycle\"],\n },\n },\n required: [\"reason\", \"user_id\", \"new_plan\", \"billing_cycle\", \"effective_date\"],\n },\n};\n```\n\nPatrones clave:\n\n- **Enums sobre texto libre**: Todo campo que tenga un set conocido de valores válidos debería ser un enum.\n- **Campo reason**: Fuerza al modelo a articular su razonamiento antes de los parámetros de acción. Funciona como chain-of-thought prompting pero dentro del structured output.\n- **Patrones regex**: Para IDs, emails, fechas, restringí el formato.\n- **Constraints descriptivas**: \"Solo llamar cuando el usuario haya solicitado explícitamente...\" le da al modelo criterio claro."},{"heading":"El Loop de Orquestación: Más Allá del Single-Shot","level":2,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#el-loop-de-orquestacin-ms-all-del-single-shot","content":"```javascript\nasync function runAgent(messages, tools, maxIterations = 10) {\n let iterations = 0;\n\n while (iterations < maxIterations) {\n iterations++;\n\n const response = await callModel({\n messages,\n tools,\n tool_choice: 'auto',\n });\n\n if (response.stop_reason === 'end_turn') {\n return {\n response: response.content,\n toolCalls: messages.filter(m => m.role === 'tool_call'),\n };\n }\n\n if (response.stop_reason === 'tool_use') {\n const toolCalls = response.tool_calls;\n\n messages.push({\n role: 'assistant',\n content: response.content,\n tool_calls: toolCalls,\n });\n\n const toolResults = await executeToolCalls(toolCalls, {\n parallel: canRunInParallel(toolCalls),\n });\n\n for (const result of toolResults) {\n messages.push({\n role: 'tool',\n tool_call_id: result.id,\n content: JSON.stringify(result.output),\n });\n }\n\n continue;\n }\n\n throw new AgentError(`Stop reason inesperado: ${response.stop_reason}`);\n }\n\n throw new AgentError(`El agente superó el máximo de iteraciones (${maxIterations})`);\n}\n```\n\nEl guard de ` maxIterations ` es crítico. Sin él, un modelo confundido puede loopear forever, llamando a una herramienta, recibiendo un error, probando una variación ligeramente distinta, recibiendo el mismo error, ad infinitum. Se han observado agentes quemar miles de API calls en minutos sin este guard."},{"heading":"Tool Calls Paralelos: Cuando el Modelo se Pone Ambicioso","level":2,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#tool-calls-paralelos-cuando-el-modelo-se-pone-ambicioso","content":"Los modelos modernos pueden pedir múltiples tool calls en una sola respuesta:\n\n```javascript\nasync function executeToolCalls(toolCalls, options = {}) {\n const { parallel, sequential } = classifyToolCalls(toolCalls);\n const results = [];\n\n if (parallel.length > 0) {\n const parallelResults = await Promise.allSettled(\n parallel.map(call => executeSingleTool(call))\n );\n\n for (let i = 0; i < parallelResults.length; i++) {\n const result = parallelResults[i];\n results.push({\n id: parallel[i].id,\n output: result.status === 'fulfilled'\n ? result.value\n : { error: true, message: result.reason.message, retryable: isRetryable(result.reason) },\n });\n }\n }\n\n for (const call of sequential) {\n try {\n const output = await executeSingleTool(call);\n results.push({ id: call.id, output });\n } catch (err) {\n results.push({ id: call.id, output: { error: true, message: err.message } });\n }\n }\n\n return results;\n}\n\nfunction classifyToolCalls(toolCalls) {\n const writeTools = new Set(['update_subscription', 'create_order', 'send_email']);\n const parallel = [];\n const sequential = [];\n\n for (const call of toolCalls) {\n if (writeTools.has(call.function.name)) {\n sequential.push(call); // Las mutaciones van secuenciales\n } else {\n parallel.push(call); // Las lecturas se pueden paralelizar\n }\n }\n\n return { parallel, sequential };\n}\n```"},{"heading":"Manejo de Errores: Enseñándole al Modelo a Recuperarse","level":2,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#manejo-de-errores-ensendole-al-modelo-a-recuperarse","content":"Cuando un tool call falla, teners que decirle al modelo qué pasó de una forma que lo ayude a recuperarse:\n\n```javascript\nasync function executeSingleTool(call) {\n const tool = toolRegistry.get(call.function.name);\n\n if (!tool) {\n return {\n error: true,\n message: `Herramienta desconocida \"${call.function.name}\". Herramientas disponibles: ${[...toolRegistry.keys()].join(', ')}`,\n };\n }\n\n try {\n const args = JSON.parse(call.function.arguments);\n const validation = validateArgs(tool.schema, args);\n if (!validation.valid) {\n return {\n error: true,\n message: `Argumentos inválidos: ${validation.errors.join('; ')}`,\n hint: `Schema esperado: ${JSON.stringify(tool.schema.properties)}`,\n };\n }\n\n const result = await tool.execute(args);\n return { success: true, data: result };\n } catch (err) {\n if (err instanceof NotFoundError) {\n return {\n error: true,\n message: `${call.function.arguments.id} no fue encontrado. Puede haber sido eliminado o el ID puede ser incorrecto.`,\n suggestion: 'Intentá buscar el recurso por nombre en vez de ID.',\n };\n }\n logger.error('Fallo en ejecución de herramienta', { tool: call.function.name, error: err });\n return {\n error: true,\n message: 'Ocurrió un error interno. Probar un approach diferente o pedile más info al usuario.',\n };\n }\n}\n```\n\nLa clave: los mensajes de error son prompts. El modelo los lee y decide qué hacer. \"Ocurrió un error\" no le da nada al modelo. \"Usuario no encontrado, intentá buscar por nombre en vez de ID\" le da un camino de recuperación."},{"heading":"El Patrón de Confirmación: Humanos en el Loop","level":2,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#el-patrn-de-confirmacin-humanos-en-el-loop","content":"Para acciones de alto impacto (plata, borrado de datos, comunicaciones externas), nunca dejes que el agente ejecute directamente:\n\n```javascript\nconst DANGEROUS_TOOLS = new Set([\n 'send_email', 'process_refund', 'delete_account',\n 'update_billing', 'create_order',\n]);\n\nasync function executeWithConfirmation(call, context) {\n if (!DANGEROUS_TOOLS.has(call.function.name)) {\n return executeSingleTool(call);\n }\n\n return {\n requires_confirmation: true,\n tool: call.function.name,\n arguments: JSON.parse(call.function.arguments),\n summary: await generateHumanReadableSummary(call),\n confirmation_token: generateToken(call),\n };\n}\n```\n\nEste patrón no es negociable para agentes en producción. No importa qué tan bueno sea el modelo, siempre va a haber situaciones ambiguas donde un humano debería decidir."},{"heading":"El Patrón de Routing: Escalando Más Allá de 20 Herramientas","level":2,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#el-patrn-de-routing-escalando-ms-all-de-20-herramientas","content":"Cuando teners más herramientas de las que un modelo puede elegir confiablemente, utilizar un approach de dos etapas:\n\n```javascript\nconst TOOL_CATEGORIES = {\n billing: ['get_invoices', 'create_refund', 'update_payment_method'],\n support: ['create_ticket', 'get_ticket_status', 'escalate_ticket'],\n account: ['get_profile', 'update_profile', 'reset_password'],\n product: ['search_products', 'get_product_details', 'check_inventory'],\n};\n\nasync function routedAgent(messages) {\n // Primero, determinamos la categoría\n const routingResponse = await callModel({\n messages,\n tools: routerTools,\n tool_choice: { type: \"function\", function: { name: \"select_tool_category\" } },\n });\n\n const category = routingResponse.tool_calls[0].function.arguments.category;\n const relevantTools = TOOL_CATEGORIES[category].map(name => toolRegistry.get(name));\n\n // Después, corremos el agente solo con las herramientas relevantes\n return runAgent(messages, relevantTools);\n}\n```\n\nEste patrón de dos etapas mantiene cada llamada al modelo enfocada en un set chico de herramientas, mejorando dramáticamente la precisión de selección."},{"heading":"Lecciones Clave del Desarrollo de Agentes en Producción","level":2,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#lecciones-clave-del-desarrollo-de-agentes-en-produccin","content":"1. **El diseño de schema es tu palanca principal.** Gastá más tiempo diseñando schemas de herramientas que escribiendo código de orquestación. Enums restringidos, campos de reason requeridos, y descripciones claras hacen más por la confiabilidad que cualquier lógica de retry.\n2. **El modelo no es tu ejecutor, es tu planificador.** No le des al modelo herramientas que hagan acciones atómicas e irreversibles. Dale herramientas que preparen acciones, y dejar que tu código maneje la ejecución con las salvaguardas adecuadas.\n3. **Los mensajes de error son prompts.** Cada string que tu herramienta devuelve es un prompt para el modelo. Escribir mensajes de error como instrucciones de recuperación, no como output de debugging para devs.\n4. **Los tool calls paralelos son una feature, no un bug.** Diseñá tus herramientas para ser paralelizables por defecto. Las operaciones de lectura siempre deberían ser seguras de paralelizar.\n5. **Logueá todo.** La primera vez que debugguees un problema de agente en producción sin logs de conversación completos, nunca más se va a saltear el logging.\n6. **El guard de iteraciones máximas no es opcional.** Los modelos en loops pueden quemar tu presupuesto de API en minutos. Siempre colocar un cap de iteraciones y siempre tener un circuit breaker de costos.\n\nEl tooling está mejorando rápido. Los modos de structured output hacen que la compliance de schema esté garantizada. Pero el desafío fundamental permanece: se está construyendo un sistema donde un generador de texto probabilístico toma decisiones sobre operaciones determinísticas. Respetá esa brecha, diseñá para ella, y tus agentes van a ser los que realmente funcionan en producción."},{"heading":"Evaluación reproducible: instrucciones hostiles y permisos","level":2,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#evaluacin-reproducible-instrucciones-hostiles-y-permisos","content":"[Agent Injection Evals](<https://lucioduran.com/es/projects/agent-injection-evals>) compara prompts con y sin aislamiento de instrucciones, cruzados con políticas de herramientas permisivas o limitadas a la tarea. Usa el mismo modelo local fijado y conserva las 120 generaciones de evaluación, incluidos los errores de formato y las tareas fallidas.\n\nEl aislamiento permitió cinco escrituras indebidas; la estrategia combinada intentó las mismas cinco y la política las bloqueó. Ninguna estrategia completó las seis tareas limpias. Presentar ese cero de ataques como una defensa ganadora sería esconder el problema: el agente tampoco estaba haciendo su trabajo. La ficha separa capacidad, intentos, efectos, llamadas bloqueadas y recursos consumidos. Son herramientas simuladas y un conjunto sintético pequeño, no una validación de seguridad para cualquier agente."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#questions","content":""},{"heading":"¿Cuál es la diferencia entre function calling y tool use?","level":3,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#question-1","content":"Es lo mismo con diferente branding. OpenAI lo llama 'function calling' (ahora 'tool use'), Anthropic lo llama 'tool use'. El mecanismo es idéntico: el modelo outputea un objeto JSON estructurado indicando qué función llamar con qué argumentos, y tu código la ejecuta."},{"heading":"¿Puedo confiar en que los LLMs generen JSON válido para tool calls?","level":3,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#question-2","content":"Con modos de structured output (modo strict de OpenAI, tool\\_use de Anthropic), el schema JSON está garantizado de ser válido. Los valores dentro del JSON todavía pueden estar mal, el modelo puede llamar la función correcta con un ID alucinado, pero la estructura va a matchear tu schema."},{"heading":"¿Cuántas herramientas le puedo dar a un modelo?","level":3,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#question-3","content":"En la práctica, 10-20 herramientas funcionan bien. Más de 30, la precisión empieza a degradarse. Si se necesita más, utilizar una capa de routing, que un modelo elija una categoría de herramienta, después una segunda llamada solo con las herramientas relevantes."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#article-tips","content":"1. **Tip:** Definir parámetros de herramientas con restricciones de ` enum ` donde sea posible en lugar de cadenas de texto libre. Un LLM eligiendo de ` [\"buy\", \"sell\", \"hold\"] ` es significativamente más confiable que interpretar intención en lenguaje natural.\n2. **Tip:** Incluir siempre un parámetro ` reason ` en los schemas de herramientas. Forzar al modelo a articular *por qué* está llamando a una herramienta antes de proporcionar los argumentos reduce de forma medible los parámetros alucinados.\n3. **Tip:** Configurar ` tool_choice: 'required' ` solo cuando se tenga certeza de que el mensaje del usuario necesita un tool call. Dejar en ` 'auto' ` por defecto para evitar que el modelo fuerce tool calls innecesarios en mensajes conversacionales.\n4. **Tip:** Registrar cada tool call con la respuesta completa del modelo, no solo los parámetros extraídos. Al depurar problemas en producción, se necesita la cadena de pensamiento del modelo, no solo lo que decidió llamar.\n\n- llm\n- function-calling\n- tool-use\n- ai-agents\n- structured-output\n- openai\n- anthropic\n- langchain\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/llm-function-calling-tool-use-patterns"},{"text":"Agent Injection Evals","url":"https://lucioduran.com/es/projects/agent-injection-evals"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns"},"headline":"Patrones de Function Calling en LLMs para Tool Use Confiable en Agentes de IA","description":"Guía técnica sobre tool use en LLMs, structured output, tool calls paralelos, estrategias de reintento, recuperación de errores, patrones de orquestación y lecciones del desarrollo de agentes de IA en producción.","datePublished":"2026-01-17","dateModified":"2026-10-21","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"AI/ML","keywords":"llm, function-calling, tool-use, ai-agents, structured-output, openai, anthropic, langchain","wordCount":1524,"timeRequired":"PT8M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/llm-function-calling-tool-use-patterns"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"Patrones de Function Calling en LLMs para Tool Use Confiable en Agentes de IA","item":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Cuál es la diferencia entre function calling y tool use?","acceptedAnswer":{"@type":"Answer","text":"Es lo mismo con diferente branding. OpenAI lo llama 'function calling' (ahora 'tool use'), Anthropic lo llama 'tool use'. El mecanismo es idéntico: el modelo outputea un objeto JSON estructurado indicando qué función llamar con qué argumentos, y tu código la ejecuta."}},{"@type":"Question","name":"¿Puedo confiar en que los LLMs generen JSON válido para tool calls?","acceptedAnswer":{"@type":"Answer","text":"Con modos de structured output (modo strict de OpenAI, tool_use de Anthropic), el schema JSON está garantizado de ser válido. Los valores dentro del JSON todavía pueden estar mal, el modelo puede llamar la función correcta con un ID alucinado, pero la estructura va a matchear tu schema."}},{"@type":"Question","name":"¿Cuántas herramientas le puedo dar a un modelo?","acceptedAnswer":{"@type":"Answer","text":"En la práctica, 10-20 herramientas funcionan bien. Más de 30, la precisión empieza a degradarse. Si se necesita más, utilizar una capa de routing, que un modelo elija una categoría de herramienta, después una segunda llamada solo con las herramientas relevantes."}}]}]}],"contentHash":"3310a94f5f280d8b8ed84dec4e00068ae5df873ea5f8ff5244afc0f586681d89","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/llm-function-calling-tool-use-patterns","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":67415,"contentBytes":14292}}
