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:
- Ver qué está pasando — ¿Qué me están pidiendo?
- Pensar — ¿Cómo lo resuelvo?
- Actuar — Llamar a herramientas o APIs
- Evaluar — ¿Funcionó? ¿Qué sigue?
- 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:
- Responder con texto — modo chatbot normal
- 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:
- Empieza simple — Function calling básico primero, complejidad después.
- Descripciones claras — El LLM solo puede usar lo que entiende.
- Errores van a pasar — Prepárate para ellos.
- Logs son vida — Sin ellos, el debugging es un infierno.
- Pon límites — Nada de loops infinitos ni llamadas sin control.
- 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! 🚀
Explora herramientas relacionadas
Prueba estas herramientas gratuitas de Pockit