🔌 Camada de LLM
A camada de LLM é a única peça que sabe qual provider está rodando. Pra ela, você passa mensagens e ferramentas. Ela traduz pro formato do provider, chama a API, traduz a resposta pro formato comum, e retorna. O resto do código nunca sabe se é Claude, GPT ou Ollama.
🎯 Interface comum
type Message = { role: 'system'|'user'|'assistant'|'tool', content: string, tool_calls?: ToolCall[], tool_call_id?: string };
type ToolCall = { id: string, name: string, args: Record<string, any> };
type LLMResponse = { content: string, tool_calls: ToolCall[], usage: { input: number, output: number } };
interface LLM {
chat(messages: Message[], tools: ToolDef[]): Promise<LLMResponse>;
}
💡 Por que isso é estratégico
Em 6 meses, vai sair um modelo melhor. Em um ano, dois ou três. Quem tem essa abstração troca em 5 minutos. Quem não tem, gasta uma semana de refactor — ou simplesmente fica preso no modelo antigo enquanto a concorrência avança.
🔧 Anatomia de uma ferramenta
Uma ferramenta tem três partes: nome (identificador), JSON Schema (descrição que o LLM lê pra saber quando e como chamar) e handler (a função TypeScript que executa de verdade). Esse padrão se repete pra qualquer ferramenta — Notion, Gmail, banco, scraping.
📐 Exemplo: ferramenta read_file
export const readFile = {
name: 'read_file',
description: 'Lê o conteúdo de um arquivo em data/ e retorna como texto',
schema: {
type: 'object',
properties: {
path: { type: 'string', description: 'Caminho relativo dentro de data/' }
},
required: ['path']
},
handler: async ({ path }: { path: string }) => {
const safe = ensureInside('./data', path); // allowlist
const content = await fs.readFile(safe, 'utf8');
return content.slice(0, 8000); // limita tamanho
}
};
📊 O que faz uma boa descrição
- • Diz quando usar a ferramenta, não só o que ela faz
- • Dá um exemplo curto se o uso é ambíguo
- • Avisa de limites ("retorna no máximo 8000 chars")
- • Lista alternativas ("para web, use search_web; para arquivo local, use read_file")
🧰 Ferramentas essenciais
Não comece com 30 ferramentas. Começa com cinco e dá pra cobrir 80% das tarefas reais. Adiciona o resto sob demanda, conforme você usa o agente e percebe o que falta.
🎯 Kit mínimo
- read_file: ler arquivo do disco (anotações, dados, documentos).
- write_file: salvar texto em arquivo (criar nota, exportar resumo).
- search_web: buscar na web (Brave Search ou similar — fonte de fatos atuais).
- fetch_url: ler conteúdo de uma URL específica (artigos, docs).
- remember_fact: atualizar memória central (chave-valor).
💡 Crescer aos poucos
Cada ferramenta nova é mais um item no JSON Schema que vai pro LLM toda chamada. Cento e vinte ferramentas mal escritas confundem o modelo. Quinze bem desenhadas e claramente documentadas ele usa com precisão. Qualidade de descrição importa mais que quantidade.
🔄 O loop do agente
O loop é o coração do agente. Ele: (1) chama o LLM com mensagens + ferramentas; (2) se vier tool_calls, executa cada uma e devolve resultado pro LLM; (3) repete até a resposta vir só em texto. É essa repetição que diferencia um agente de um chatbot.
🎯 Pseudocódigo do loop
async function runAgent(userMessage: string) {
const messages = [systemPrompt(), ...buffer(), { role: 'user', content: userMessage }];
for (let i = 0; i < MAX_ITER; i++) {
const res = await llm.chat(messages, tools);
messages.push({ role: 'assistant', content: res.content, tool_calls: res.tool_calls });
if (res.tool_calls.length === 0) {
return res.content; // resposta final
}
for (const call of res.tool_calls) {
const tool = tools.find(t => t.name === call.name);
const result = await tool.handler(call.args);
messages.push({ role: 'tool', tool_call_id: call.id, content: String(result) });
}
}
throw new Error('MAX_ITER atingido sem resposta final');
}
📊 Cenário típico
Você pergunta "resume o último email do Marcelo". O loop faz: (1) LLM chama search_emails(from='marcelo'); (2) ferramenta retorna 3 emails; (3) LLM chama read_email(id=ultimo); (4) ferramenta retorna corpo; (5) LLM responde com o resumo. Foram 3 idas-e-vindas dentro de uma mensagem sua.
🚧 Limites e segurança
Loop sem freio é desastre garantido. Quatro proteções são obrigatórias antes de soltar o agente: limite de iterações, allowlist de paths, timeout em rede, e kill switch via env.
✓ Sempre faça
- • MAX_ITER de 15-20 — para se entrar em loop
- • Allowlist explícita de pastas onde pode ler/escrever
- • Timeout de 30s em fetch e tool calls
- • Variável KILL_SWITCH que desliga o agente sem deploy
✗ Nunca faça
- • Loop sem MAX_ITER (fatura explode)
- • Permitir paths absolutos no read_file
- • Executar shell command sem confirmação
- • Confiar 100% em path validado pelo LLM
⚠️ Cenário real de prejuízo
Sem MAX_ITER: agente entra em espiral chamando a mesma ferramenta com pequenas variações. Você dorme. De manhã, fatura de $300 numa única conversa. Com MAX_ITER de 15: ele para depois de 15 ações e pede ajuda. Custo: zero a mais de fatura.
✅ Resposta final no Telegram
O loop terminou, você tem um texto. Agora ele precisa virar mensagem no Telegram — e o Telegram tem regras próprias que pegam todo mundo de surpresa: limite de 4096 caracteres por mensagem, parser de Markdown chato (caracteres especiais precisam de escape) e rate limit que pune editar a mesma mensagem rápido demais.
🎯 Receita prática
- 1. Use
parse_mode: 'HTML'em vez de Markdown — escape é mais previsível. - 2. Se a resposta passa de 4000 chars, divide em pedaços por parágrafo ou bloco de código.
- 3. Em retry de 429 (rate limit), espera o tempo que o erro indica.
- 4. Salva a mensagem do agente no buffer antes de enviar — se Telegram falhar, você não perde o histórico.
💡 Ganho da Trilha 2
Por enquanto, a resposta vai inteira de uma vez. Na Trilha 2 você adiciona streaming — a mensagem é editada em tempo real conforme o LLM gera. Faz uma diferença gigante na sensação de "ele está pensando agora".
✅ Resumo do módulo
Fim da Trilha 1 — você tem um agente funcional!
A Trilha 2 (Vida do Agente) adiciona streaming, voz, autonomia 24/7, reflexão noturna e skills auto-geradas.