Criando Agentes de IA do Zero: Function Calling e Padrões que Funcionam
O mundo da IA mudou completamente. Saímos da era "pergunta e resposta" pra era dos agentes de IA — sistemas que pensam sozinhos, planejam e executam tarefas complexas.
Já tentou criar um? Provavelmente descobriu que não é só "chamar a API e pronto". 😅 Tem que gerenciar loops, definir ferramentas, tratar erros... a lista não acaba.
Nesse artigo vamos construir um agente de IA do zero — um que dá pra usar em produção de verdade.
Chatbot vs Agente: qual a diferença?
Chatbot responde. Agente age.
A chave é o loop agêntico:
- Observar — O que tão pedindo?
- Pensar — Como resolvo?
- Agir — Chamar ferramentas/APIs
- Avaliar — Deu certo? Próximo passo?
- Repetir — Até terminar
Isso permite tarefas tipo "pesquisa concorrentes e faz relatório" ou "acha voo barato e reserva". Não só gera texto — orquestra ações.
┌─────────────────────────────────────────────────────┐
│ LOOP AGÊNTICO │
├─────────────────────────────────────────────────────┤
│ ┌──────────┐ │
│ │ OBSERVAR │ ◄── Solicitação / Ambiente │
│ └────┬─────┘ │
│ ▼ │
│ ┌──────────┐ │
│ │ PENSAR │ ◄── Raciocínio do LLM │
│ └────┬─────┘ │
│ ▼ │
│ ┌──────────┐ │
│ │ AGIR │ ◄── Execução de Ferramentas │
│ └────┬─────┘ │
│ ▼ │
│ ┌──────────┐ │
│ │ REFLETIR │ ◄── Avaliar Resultado │
│ └────┬─────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Tarefa Pronta? │── Sim ──► Retornar Resultado │
│ └──────┬─────────┘ │
│ │ Não │
│ └─────────► Voltar a OBSERVAR │
└─────────────────────────────────────────────────────┘
Entendendo Function Calling: A Base do Uso de Ferramentas
Function calling é o mecanismo que permite aos LLMs solicitar a execução de funções externas. Em vez de gerar texto, o modelo produz uma solicitação estruturada para chamar uma função específica com argumentos específicos.
Como Function Calling Funciona
Quando você envia uma solicitação a um LLM com definições de funções, o modelo pode escolher:
- Responder com texto (comportamento de chatbot tradicional)
- Solicitar uma chamada de função (comportamento agêntico)
Aqui está a anatomia de uma definição de função para OpenAI:
const tools = [ { type: "function", function: { name: "get_weather", description: "Obtém o clima atual para um local específico", parameters: { type: "object", properties: { location: { type: "string", description: "Nome da cidade, ex., 'São Paulo, Brasil'" }, unit: { type: "string", enum: ["celsius", "fahrenheit"], description: "Unidade de temperatura" } }, required: ["location"] } } } ];
Os campos description são críticos—são o que o LLM usa para decidir quando e como chamar a função.
Function Calling com Claude (Anthropic)
Claude usa uma estrutura similar, mas ligeiramente diferente:
const tools = [ { name: "get_weather", description: "Obtém o clima atual para um local específico", input_schema: { type: "object", properties: { location: { type: "string", description: "Nome da cidade, ex., 'São Paulo, Brasil'" }, unit: { type: "string", enum: ["celsius", "fahrenheit"] } }, required: ["location"] } } ];
A diferença chave é input_schema em vez de parameters, mas o conceito é idêntico.
Construindo Seu Primeiro Agente: Implementação Completa
Vamos construir um agente prático que pode pesquisar na web, ler documentos e realizar cálculos.
Passo 1: Defina Sua Interface de Ferramentas
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>; }
Passo 2: Implemente Suas Ferramentas
const webSearchTool: Tool = { name: "web_search", description: "Pesquisa na web por informações atuais. Use para eventos recentes ou dados não presentes no treinamento.", parameters: { type: "object", properties: { query: { type: "string", description: "A consulta de pesquisa" } }, 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. Suporta aritmética básica e porcentagens.", parameters: { type: "object", properties: { expression: { type: "string", description: "Expressão matemática, ex., '(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 `Erro: Expressão inválida`; } } }; const readUrlTool: Tool = { name: "read_url", description: "Lê e extrai conteúdo de texto de uma URL.", parameters: { type: "object", properties: { url: { type: "string", description: "A URL para ler" } }, 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 `Erro ao ler URL: ${error}`; } } };
Passo 3: O Loop Agêntico
Aqui é onde a mágica acontece:
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: `Você é um assistente de IA útil com acesso a ferramentas. Use as ferramentas quando necessário para responder perguntas com precisão. Sempre explique seu raciocínio antes de usar uma ferramenta.` }, { 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 || "Não consegui gerar uma resposta."; } 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: `Erro: Ferramenta '${toolCall.function.name}' não 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: `Erro ao executar ferramenta: ${error}` }); } } } return "Número máximo de iterações atingido."; }
Passo 4: Usando Seu Agente
const tools = [webSearchTool, calculatorTool, readUrlTool]; const result = await runAgent( "Qual é a população atual de Tóquio e que porcentagem isso representa da população total do Japão?", tools ); console.log(result);
O Padrão ReAct: Raciocínio e Ação
O padrão ReAct (Reasoning + Acting) torna os agentes mais confiáveis. O agente raciocina explicitamente sobre cada passo:
const REACT_SYSTEM_PROMPT = `Você é um assistente que segue o padrão ReAct. Para cada passo: 1. THOUGHT: Raciocine sobre a situação atual 2. ACTION: Escolha uma ação (usar ferramenta ou resposta final) 3. OBSERVATION: Analise o resultado Formato: THOUGHT: [Seu raciocínio] ACTION: [nome_ferramenta com argumentos OU "FINAL_ANSWER"] OBSERVATION: [Resultado da ferramenta] Quando estiver pronto para responder: THOUGHT: Tenho informação suficiente. ACTION: FINAL_ANSWER [Sua resposta completa]`;
Tratamento de Erros e Padrões de Confiabilidade
1. Retentativas com Backoff Exponencial
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("Retentativa falhou"); }
2. Timeout na Execução
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. Degradação Elegante
async function safeToolExecute(tool: Tool, args: Record<string, unknown>): Promise<string> { try { return await executeWithTimeout( () => executeWithRetry(() => tool.execute(args)), 30000 ); } catch (error) { return `Ferramenta "${tool.name}" falhou: ${error}. Tente outra abordagem.`; } }
Padrão Avançado: Orquestração Multi-Ferramenta
interface ToolResult { toolName: string; input: Record<string, unknown>; output: string; timestamp: Date; } class AgentContext { private history: ToolResult[] = []; private cache: Map<string, string> = new Map(); async executeToolWithCache(tool: Tool, args: Record<string, unknown>): Promise<string> { const cacheKey = `${tool.name}:${JSON.stringify(args)}`; if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey)!; } const result = await tool.execute(args); this.cache.set(cacheKey, result); this.history.push({ toolName: tool.name, input: args, output: result, timestamp: new Date() }); return result; } getHistory(): ToolResult[] { return [...this.history]; } }
Execução Paralela de Ferramentas
Quando as ferramentas são independentes, execute-as em paralelo:
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; }
Monitoramento e Observabilidade
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"; } }; }
Erros Comuns e Como Evitá-los
1. Loops 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. Estouro 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: "Resuma esta conversa de forma concisa." }, { role: "user", content: JSON.stringify(toSummarize) } ] }); return [ messages[0], { role: "assistant", content: `Contexto anterior: ${summary.choices[0].message.content}` }, ...messages.slice(-5) ]; }
3. Descrições Ambíguas
// ❌ Ruim description: "API de clima" // ✅ Bom description: "Obtém condições climáticas de uma cidade. Use quando perguntarem sobre clima ou temperatura. Funciona apenas para cidades, não países."
Considerações de Segurança
1. Validação 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. Execução em 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; } }
Conclusão
Agentes de IA = dominar o loop: observar, pensar, agir, avaliar, repetir.
- Começa simples — Function calling básico primeiro
- Descrições claras — LLM só usa o que entende
- Erros vão rolar — Se prepara
- Logs são vida — Sem eles, debugging é inferno
- Coloca limites — Nada de loop infinito
- Segurança primeiro — Não confia cegamente no LLM
Próximo nível: multi-agente, memória persistente, agentes que aprendem... tudo em cima disso. Bora codar! 🚀
Explore ferramentas relacionadas
Experimente estas ferramentas gratuitas do Pockit