🎭 Personalidade (soul.md)
A soul.md é um arquivo Markdown que define quem o agente é: tom, regras, formato de resposta, o que ele recusa fazer. Ela é injetada como system prompt em toda chamada do LLM. Versionar como Markdown deixa você editar como qualquer outro arquivo do projeto e ver diff no git.
📋 O que sempre vai na soul
- Identidade: nome do agente, propósito, pra quem trabalha (você).
- Tom: formal? casual? direto? bem-humorado? exemplos curtos do estilo.
- Regras: sempre responder em PT-BR, mensagens curtas no Telegram, código em bloco, etc.
- Recusas: o que não fazer (mandar dados sensíveis em texto puro, ações destrutivas sem confirmar).
- Fatos sobre você: nome, fuso, profissão, contexto familiar mínimo.
💡 Como evoluir a soul
Comece com 30 linhas. Use o agente uma semana. Toda vez que ele errar tom ou estilo, abra a soul e adicione a regra ("nunca usar emoji no início da resposta", "preferir bullets sobre parágrafo longo"). Em duas semanas ela está afiada e o agente "soa como você espera".
⚙️ Configuração centralizada
Em vez de espalhar process.env.ALGUMA_COISA por todo o código, você cria um único módulo que lê o .env, valida tipos com Zod, e exporta um objeto config tipado. Todo o resto do código importa de lá.
🎯 Por que vale a pena
- • Validação na inicialização: se faltou .env ou tem valor inválido, falha logo no boot — não 3 horas depois numa request real.
- • Refactor sem dor: renomeou uma variável? Muda em um lugar, TypeScript te aponta todas as referências.
- • Defaults seguros:
MAX_ITERdefault 15,TZdefault America/Sao_Paulo — sem precisar lembrar.
📊 Anatomia mínima
import { z } from 'zod';
import 'dotenv/config';
const Schema = z.object({
TELEGRAM_BOT_TOKEN: z.string().min(20),
ALLOWED_CHAT_IDS: z.string().transform(s => s.split(',').map(Number)),
LLM_PROVIDER: z.enum(['openrouter','anthropic','openai','ollama']),
LLM_MODEL: z.string(),
LLM_API_KEY: z.string().min(10),
DATABASE_PATH: z.string().default('./data/agente.db'),
TIMEZONE: z.string().default('America/Sao_Paulo'),
MAX_ITER: z.coerce.number().default(15),
});
export const config = Schema.parse(process.env);
export type Config = typeof config;
💭 Memória de curto prazo
É o histórico recente da conversa: as últimas N mensagens guardadas em SQLite e re-enviadas a cada chamada do LLM. Sem isso, cada mensagem sua é uma conversa nova — ele esquece o que vocês acabaram de falar.
📐 Como dimensionar
- Buffer de 40-60 mensagens serve a maioria dos casos — equivale a uma conversa de uma hora.
- Mensagens antigas não somem: ficam no banco e podem ser recuperadas pela memória semântica (tópico 5).
- Quando o buffer estoura, descarta o mais velho. Em produção, antes de descartar, summariza num "highlight" de uma linha.
💡 Dica
Use better-sqlite3 em vez do sqlite3 antigo. É síncrono (mais simples), mais rápido, e funciona com TypeScript sem dor. Crie índice em (chat_id, created_at) — buscar últimas N fica instantâneo mesmo com 10k mensagens.
📌 Memória central de fatos
Uma tabela chave-valor com fatos centrais sobre você, que sempre entram no system prompt. Diferente do buffer (que rola e descarta), esses fatos são permanentes até você mudar.
🔑 Exemplos de fatos centrais
- • nome: "Nei"
- • fuso: "America/Sao_Paulo"
- • profissão: "consultor de tecnologia, foco em automação"
- • família: "esposa Ana, filhos Pedro (8) e Mariana (5)"
- • meta_q2: "lançar curso de agentes em junho"
- • preferência_resposta: "bullets curtos, sem emoji no início"
✓ Bons candidatos
- • Fatos estáveis (nome, fuso)
- • Preferências de longo prazo
- • Contexto de família/trabalho
- • Metas trimestrais
✗ Não é pra cá
- • Detalhe de uma conversa única
- • Estado temporário ("estou no aeroporto")
- • Histórico transacional (vai pra buffer)
- • Documentos longos (vai pra semântica)
💡 Budget
Mantenha total da memória central abaixo de ~2.000 caracteres (~500 tokens). Tudo isso vai a cada mensagem — se inflar demais, queima tokens à toa e dilui o foco do agente. Quando passar do limite, é hora de mover fatos antigos pra arquivo histórico.
🔍 Memória semântica
Cada mensagem que vocês trocam é convertida em um embedding (vetor numérico que representa o significado) e indexada num banco vetorial. Quando você pergunta algo, o agente busca os trechos mais parecidos em significado — não em palavras exatas.
🎯 Por que muda o jogo
Você diz: "Lembra daquele cliente que reclamou de prazo lá no início do ano?"
Sem memória semântica: ele só vê as últimas 40 mensagens, não acha nada, responde "qual?".
Com memória semântica: busca por similaridade (não pela palavra "prazo"), encontra a conversa de fevereiro com o cliente Marcelo, e responde com o contexto certo.
🔧 Opções de banco vetorial
- Pinecone: serverless, free tier de 100k vetores, configura em 2 minutos. Bom pra começar.
- sqlite-vec: extensão SQLite, roda local, sem custo, sem internet. Bom se já usa SQLite.
- Qdrant: open-source, roda em Docker, escalável. Bom pra depois de 100k vetores.
- Modelo de embedding: text-embedding-3-small (OpenAI, $0.02/M tokens) ou nomic-embed via Ollama (grátis local).
🪜 Estratégia das 3 camadas
A regra prática é simples: cada camada tem um custo e um benefício. Você usa camadas 1 e 2 sempre, e camada 3 sob demanda via tool call.
🎯 Resumo decisório
Sempre vai. ~500 tokens. Fatos sobre você. Mudança rara.
Sempre vai. ~2.000-5.000 tokens. Últimas 40-60 mensagens. Continuidade.
Só quando o agente decide chamar a tool buscar_memoria(query). Pode trazer 5-10 trechos relevantes.
💰 Impacto no custo
Mandar tudo a cada mensagem (sem essa hierarquia) facilmente triplica seu gasto mensal de tokens. A diferença entre $5/mês e $50/mês vem de onde você corta — e cortar inteligentemente é exatamente isso aqui.
✅ Resumo do módulo
Próximo módulo:
1.3 — Cérebro e Ferramentas (camada de LLM, sistema de tools, e o loop que junta tudo)