Back

Cómo Crear Agentes de IA desde Cero: Function Calling y Patrones que Funcionan

El mundo de la IA cambió por completo. Ya no estamos en la era del "pregunta y responde" — ahora hablamos de agentes de IA: sistemas que piensan solos, planifican y ejecutan tareas complejas paso a paso.

¿Ya intentaste crear uno? Si es así, probablemente descubriste que no es tan sencillo como "llamar a la API y listo". 😅 Hay que manejar loops, definir herramientas, gestionar errores, tomar decisiones de arquitectura... y la lista sigue.

En este artículo vamos a construir un agente de IA desde cero, uno que realmente puedas usar en producción. Iremos paso a paso: primero los conceptos, después el código en TypeScript, y al final tendrás algo funcional — no solo un demo.

¿Cuál es la diferencia entre un chatbot y un agente?

Simple:

Un chatbot responde. Un agente actúa.

La clave está en el loop agéntico — la capacidad de:

  1. Ver qué está pasando — ¿Qué me están pidiendo?
  2. Pensar — ¿Cómo lo resuelvo?
  3. Actuar — Llamar a herramientas o APIs
  4. Evaluar — ¿Funcionó? ¿Qué sigue?
  5. Repetir — Hasta terminar

Este loop permite que un agente haga cosas como "investiga a la competencia y hazme un reporte" o "encuentra vuelos baratos y reserva el más económico". No solo genera texto — orquesta acciones para lograr un objetivo.

┌─────────────────────────────────────────────────────┐
│                    BUCLE AGÉNTICO                   │
├─────────────────────────────────────────────────────┤
│    ┌──────────┐                                     │
│    │ OBSERVAR │ ◄── Solicitud / Entorno            │
│    └────┬─────┘                                     │
│         ▼                                           │
│    ┌──────────┐                                     │
│    │  PENSAR  │ ◄── Razonamiento LLM               │
│    └────┬─────┘                                     │
│         ▼                                           │
│    ┌──────────┐                                     │
│    │  ACTUAR  │ ◄── Ejecución de Herramientas      │
│    └────┬─────┘                                     │
│         ▼                                           │
│    ┌──────────┐                                     │
│    │REFLEXIONAR│ ◄── Evaluar Resultado             │
│    └────┬─────┘                                     │
│         ▼                                           │
│    ┌────────────────┐                               │
│    │ ¿Tarea Lista?  │── Sí ──► Devolver Resultado  │
│    └──────┬─────────┘                               │
│           │ No                                      │
│           └─────────► Volver a OBSERVAR             │
└─────────────────────────────────────────────────────┘

Entendiendo Function Calling

Function Calling es lo que permite que un LLM diga "ey, necesito ejecutar esta función". En vez de darte texto, te devuelve una petición estructurada: "llama a esta función con estos parámetros".

¿Cómo funciona?

Cuando le pasas definiciones de funciones al LLM, puede elegir entre:

  1. Responder con texto — modo chatbot normal
  2. Pedir que ejecutes una función — ¡modo agente activado!

Así se ve una definición de función para OpenAI:

const tools = [ { type: "function", function: { name: "get_weather", description: "Obtiene el clima actual para una ubicación específica", parameters: { type: "object", properties: { location: { type: "string", description: "Nombre de la ciudad, ej., 'Madrid, España'" }, unit: { type: "string", enum: ["celsius", "fahrenheit"], description: "Unidad de temperatura" } }, required: ["location"] } } } ];

El campo description es clave. El LLM lee esto para decidir cuándo usar cada función.

¿Y con Claude?

Claude usa algo muy similar, solo cambia la estructura:

const tools = [ { name: "get_weather", description: "Obtiene el clima actual para una ubicación específica", input_schema: { type: "object", properties: { location: { type: "string", description: "Nombre de la ciudad, ej., 'Madrid, España'" }, unit: { type: "string", enum: ["celsius", "fahrenheit"] } }, required: ["location"] } } ];

La diferencia: usa input_schema en vez de parameters. El concepto es el mismo.

Construyendo tu Primer Agente

Vamos a crear un agente que puede buscar en la web, leer documentos y hacer cálculos. Usaremos TypeScript + OpenAI, pero estos patrones funcionan con cualquier LLM.

Paso 1: Definir la interfaz de herramientas

interface Tool { name: string; description: string; parameters: { type: "object"; properties: Record<string, { type: string; description: string; enum?: string[]; }>; required: string[]; }; execute: (args: Record<string, unknown>) => Promise<string>; }

Paso 2: Implementa Tus Herramientas

const webSearchTool: Tool = { name: "web_search", description: "Busca en la web información actual. Usa para eventos recientes o datos no en el entrenamiento.", parameters: { type: "object", properties: { query: { type: "string", description: "La consulta de búsqueda" } }, required: ["query"] }, execute: async (args) => { const { query } = args as { query: string }; const response = await fetch(`https://api.search.example/search?q=${encodeURIComponent(query)}`); const data = await response.json(); return JSON.stringify(data.results.slice(0, 5)); } }; const calculatorTool: Tool = { name: "calculator", description: "Realiza cálculos matemáticos. Soporta aritmética básica y porcentajes.", parameters: { type: "object", properties: { expression: { type: "string", description: "Expresión matemática, ej., '(100 * 1.15) + 50'" } }, required: ["expression"] }, execute: async (args) => { const { expression } = args as { expression: string }; try { const result = Function(`"use strict"; return (${expression})`)(); return `Resultado: ${result}`; } catch { return `Error: Expresión inválida`; } } }; const readUrlTool: Tool = { name: "read_url", description: "Lee y extrae contenido de texto de una URL.", parameters: { type: "object", properties: { url: { type: "string", description: "La URL a leer" } }, required: ["url"] }, execute: async (args) => { const { url } = args as { url: string }; try { const response = await fetch(url); const html = await response.text(); const text = html.replace(/<[^>]*>/g, ' ').slice(0, 5000); return text; } catch (error) { return `Error leyendo URL: ${error}`; } } };

Paso 3: El Bucle Agéntico

Aquí es donde ocurre la magia:

import OpenAI from 'openai'; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); interface Message { role: "system" | "user" | "assistant" | "tool"; content: string; tool_calls?: Array<{ id: string; type: "function"; function: { name: string; arguments: string }; }>; tool_call_id?: string; } async function runAgent( userMessage: string, tools: Tool[], maxIterations: number = 10 ): Promise<string> { const toolDefinitions = tools.map(tool => ({ type: "function" as const, function: { name: tool.name, description: tool.description, parameters: tool.parameters } })); const messages: Message[] = [ { role: "system", content: `Eres un asistente de IA útil con acceso a herramientas. Usa las herramientas cuando sea necesario para responder preguntas con precisión. Siempre explica tu razonamiento antes de usar una herramienta.` }, { role: "user", content: userMessage } ]; for (let i = 0; i < maxIterations; i++) { const response = await openai.chat.completions.create({ model: "gpt-4-turbo-preview", messages: messages, tools: toolDefinitions, tool_choice: "auto" }); const assistantMessage = response.choices[0].message; messages.push(assistantMessage as Message); if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) { return assistantMessage.content || "No pude generar una respuesta."; } for (const toolCall of assistantMessage.tool_calls) { const tool = tools.find(t => t.name === toolCall.function.name); if (!tool) { messages.push({ role: "tool", tool_call_id: toolCall.id, content: `Error: Herramienta '${toolCall.function.name}' no encontrada` }); continue; } try { const args = JSON.parse(toolCall.function.arguments); const result = await tool.execute(args); messages.push({ role: "tool", tool_call_id: toolCall.id, content: result }); } catch (error) { messages.push({ role: "tool", tool_call_id: toolCall.id, content: `Error ejecutando herramienta: ${error}` }); } } } return "Se alcanzó el máximo de iteraciones."; }

Paso 4: Usando Tu Agente

const tools = [webSearchTool, calculatorTool, readUrlTool]; const result = await runAgent( "¿Cuál es la población actual de Tokio y qué porcentaje es de la población total de Japón?", tools ); console.log(result);

El Patrón ReAct: Razonamiento y Acción

Este enfoque hace el debugging mucho más fácil.

Manejo de Errores (Esto es Crítico)

Si vas a producción, necesitas manejo de errores sólido. Aquí van los patrones esenciales.

1. Reintentos con backoff

async function executeWithRetry<T>( fn: () => Promise<T>, maxRetries: number = 3, baseDelay: number = 1000 ): Promise<T> { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await fn(); } catch (error) { if (attempt === maxRetries - 1) throw error; const delay = baseDelay * Math.pow(2, attempt); await new Promise(resolve => setTimeout(resolve, delay)); } } throw new Error("Reintento fallido"); }

2. Timeouts

async function executeWithTimeout<T>( fn: () => Promise<T>, timeoutMs: number = 30000 ): Promise<T> { return Promise.race([ fn(), new Promise<T>((_, reject) => setTimeout(() => reject(new Error("Timeout")), timeoutMs) ) ]); }

3. Que no explote todo

async function safeToolExecute(tool: Tool, args: Record<string, unknown>): Promise<string> { try { return await executeWithTimeout( () => executeWithRetry(() => tool.execute(args)), 30000 ); } catch (error) { return `Herramienta "${tool.name}" falló: ${error}. Intenta otro enfoque.`; } }

Ejecutando Herramientas en Paralelo

Si las herramientas no dependen una de otra, ejecútalas al mismo tiempo:

async function executeToolsInParallel( toolCalls: Array<{ tool: Tool; args: Record<string, unknown> }> ): Promise<Map<string, string>> { const results = new Map<string, string>(); const promises = toolCalls.map(async ({ tool, args }) => { const result = await safeToolExecute(tool, args); results.set(tool.name, result); }); await Promise.all(promises); return results; }

Monitoreo y Observabilidad

interface AgentTrace { traceId: string; startTime: Date; endTime?: Date; steps: Array<{ type: "thought" | "action" | "observation"; content: string; timestamp: Date; }>; status: "running" | "completed" | "failed"; } function createTracer() { const trace: AgentTrace = { traceId: crypto.randomUUID(), startTime: new Date(), steps: [], status: "running" }; return { trace, addStep: (type: "thought" | "action" | "observation", content: string) => { trace.steps.push({ type, content, timestamp: new Date() }); }, complete: () => { trace.endTime = new Date(); trace.status = "completed"; } }; }

Errores Comunes y Cómo Evitarlos

1. Bucles Infinitos

function detectLoop(messages: Message[], threshold: number = 3): boolean { const recentToolCalls = messages .filter(m => m.tool_calls) .slice(-threshold) .map(m => JSON.stringify(m.tool_calls)); return new Set(recentToolCalls).size === 1 && recentToolCalls.length === threshold; }

2. Desbordamiento de Contexto

async function summarizeHistory(messages: Message[]): Promise<Message[]> { if (messages.length <= 10) return messages; const toSummarize = messages.slice(1, -5); const summary = await openai.chat.completions.create({ model: "gpt-4-turbo-preview", messages: [ { role: "system", content: "Resume esta conversación concisamente." }, { role: "user", content: JSON.stringify(toSummarize) } ] }); return [ messages[0], { role: "assistant", content: `Contexto previo: ${summary.choices[0].message.content}` }, ...messages.slice(-5) ]; }

3. Descripciones Ambiguas

// ❌ Malo description: "API del clima" // ✅ Bueno description: "Obtiene condiciones climáticas de una ciudad. Usa cuando pregunten sobre clima o temperatura. Solo funciona para ciudades, no países."

Consideraciones de Seguridad

1. Validación de Entrada

function validateToolArgs(schema: Tool['parameters'], args: Record<string, unknown>): boolean { for (const required of schema.required) { if (!(required in args)) return false; } for (const [key, value] of Object.entries(args)) { const propSchema = schema.properties[key]; if (propSchema?.enum && !propSchema.enum.includes(value as string)) { return false; } } return true; }

2. Ejecución en Sandbox

import ivm from 'isolated-vm'; async function safeEval(code: string): Promise<string> { const isolate = new ivm.Isolate({ memoryLimit: 128 }); const context = await isolate.createContext(); try { const result = await context.eval(code, { timeout: 5000 }); return String(result); } finally { isolate.dispose(); } }

3. Rate Limiting

class RateLimiter { private requests: number[] = []; constructor(private limit: number, private windowMs: number) {} async check(): Promise<boolean> { const now = Date.now(); this.requests = this.requests.filter(t => t > now - this.windowMs); if (this.requests.length >= this.limit) return false; this.requests.push(now); return true; } }

Conclusión

Crear agentes de IA se trata de dominar el loop: observar, pensar, actuar, evaluar, repetir. Lo esencial:

  1. Empieza simple — Function calling básico primero, complejidad después.
  2. Descripciones claras — El LLM solo puede usar lo que entiende.
  3. Errores van a pasar — Prepárate para ellos.
  4. Logs son vida — Sin ellos, el debugging es un infierno.
  5. Pon límites — Nada de loops infinitos ni llamadas sin control.
  6. Seguridad primero — Nunca confíes ciegamente en la salida del LLM.

Estos patrones son la base para cualquier agente en producción. Ya sea para automatizar soporte, crear asistentes de investigación o herramientas de código — los principios son los mismos.

¿El siguiente nivel? Sistemas multi-agente, memoria persistente, agentes que aprenden... pero todo se construye sobre lo que vimos hoy. ¡A codear! 🚀

AILLMAgentsFunction CallingTypeScriptOpenAIClaudeAgentic AI

Explora herramientas relacionadas

Prueba estas herramientas gratuitas de Pockit