cd /news/artificial-intelligence/construindo-um-assistente-pessoal-10… · home topics artificial-intelligence article
[ARTICLE · art-63922] src=dev.to ↗ pub= topic=artificial-intelligence verified=true sentiment=· neutral

Construindo um assistente pessoal 100% local com Ollama, LangChain e voz — e as armadilhas que ninguém conta

A developer built a 100% local personal assistant using Ollama, LangChain, and voice, running entirely on a laptop GPU with no API calls. The project exposed key differences between cloud-based 70B+ models and local 8-14B models, and revealed that structured tool calling often fails for local LLMs, leading the developer to adopt hybrid patterns like pre-executing searches and parsing free-form output.

read6 min views54 publishedJul 17, 2026

Nos últimos dias montei um assistente pessoal do zero rodando 100% na minha

máquina — sem chamadas pra API paga, sem dados saindo do notebook. Ele

pesquisa na internet, escreve e salva código, entende voz, responde falando,

tem memória de longo prazo e roda um LLM local via Ollama.

O resultado funciona bem, mas o caminho até aqui expôs um monte de conceitos

que todo mundo que trabalha com LLMs locais/agentes acaba esbarrando —

principalmente a diferença brutal entre "um LLM de 70B+ na nuvem" e "um LLM

de 8-14B rodando numa GPU de laptop". Esse artigo é sobre esses conceitos.

Voz do usuário
    │
    ▼
VAD (detecção de fala) ──► Whisper (fala → texto)
    │
    ▼
Agente LangChain/LangGraph ──► Ollama (LLM local)
    │                              │
    │                              ├─► Tools: busca web, clima, memória...
    ▼                              │
Piper (texto → fala) ◄─────────────┘
    │
    ▼
Áudio de resposta

Tudo roda localmente: o LLM via Ollama, a transcrição via

faster-whisper, a síntese de

voz via Piper, e um SQLite

guardando memória. Nada trafega pra fora da máquina, exceto as buscas na

web em si.

Um "agente" de IA não é magia — é um LLM rodando dentro de um loop que pode,

a cada passo, decidir entre responder em texto ou chamar uma função

("tool"), observar o resultado, e decidir de novo. O

LangChain 1.0 formaliza isso com

create_agent

, construído em cima do LangGraph, que modela esse loop como

uma máquina de estados.

Cada "tool" é só uma função Python decorada, com uma docstring que vira a

descrição que o modelo lê pra decidir quando usá-la:

@tool
def get_weather(city: str) -> str:
    """Retorna o clima atual de uma cidade..."""
    ...

O modelo nunca executa a função diretamente — ele gera uma estrutura

("tool call") dizendo "quero chamar get_weather

com city='São Paulo'

", o

framework executa de verdade e devolve o resultado pro modelo continuar.

Isso foi a maior fonte de dor de cabeça do projeto. Tool calling depende do

modelo ter sido treinado pra gerar uma estrutura específica (geralmente

JSON) num formato que o runtime (Ollama, no meu caso) sabe interpretar. Só

que:

<tool_call>{"name": "web_search", ...}</tool_call>

como se fosse a resposta final, em vez de o runtime interceptar isso como uma chamada de função de verdade.save_code_file

, e ele escreveu # código do jogo da cobrinha...

como se fosse suficiente. O mesmo modelo, escrevendo código como texto solto numa resposta normal, escreveu o jogo inteiro e funcional.A lição prática: não force tudo a passar por tool calling estruturado.

Pra geração de conteúdo longo (código, relatórios), deixar o modelo escrever

naturalmente e fazer o parsing do resultado depois — com um comentário

convencional tipo # arquivo: nome.py

que o próprio código extrai via regex

— foi muito mais confiável do que depender do modelo preencher um campo

JSON com cuidado.

Outro padrão que precisei adotar: pedir "pesquise sobre X e salve um

resumo" fazia o modelo, em vez de pesquisar de verdade, escrever um script

que abriria o Google depois — tecnicamente relacionado ao pedido, mas inútil

na prática.

A correção não foi "melhorar o prompt" (tentei, várias vezes). Foi tirar a

decisão das mãos do modelo: detectar esse padrão de pedido por palavras-chave

e executar a busca antes de chamar o LLM, injetando os resultados reais

já prontos no contexto. O modelo só precisa sintetizar o que já está na mão

dele — não decide mais se pesquisa ou não.

def is_research_and_save_request(user_text: str) -> bool:
    lower = user_text.lower()
    has_research = any(w in lower for w in _RESEARCH_WORDS)
    has_save = any(w in lower for w in _SAVE_WORDS)
    return has_research and has_save

Isso é um lembrete geral trabalhando com LLMs: quanto mais determinística uma decisão pode ser feita fora do modelo, mais confiável o sistema fica.

"Memória" em um agente na verdade são pelo menos três coisas diferentes:

checkpointer

.thread_id

fixa. Troquei de InMemorySaver

(que zera a cada reinício) para SqliteSaver

.remember_fact

/recall_facts

) que o modelo aciona quando julga relevante.Um detalhe que pegou muita gente de surpresa (inclusive eu): memória de

conversa persistente cresce sem limite. Depois de várias interações, o

histórico inteiro sendo reenviado a cada turno começou a consumir o espaço

de contexto disponível, cortando respostas longas (como código) no meio.

A solução é resumir automaticamente o histórico antigo quando ele passa de

um limite de tokens, mantendo as mensagens recentes intactas — no LangChain

1.0 isso já vem pronto como SummarizationMiddleware

.

Pra detectar quando alguém está falando (e não gravar silêncio pra sempre),

a abordagem ingênua é medir a amplitude do áudio e comparar com um

threshold. Funciona mal — ruído de fundo, cliques do teclado e a respiração

já bastam pra confundir.

VAD de verdade (usei webrtcvad,

originalmente do WebRTC do Google) analisa o espectro do áudio em janelas

curtas (10-30ms) pra decidir se aquilo tem características de fala humana,

não só volume. É ordens de magnitude mais confiável.

Queria que o assistente pudesse ser interrompido no meio da fala — falar

por cima dele deveria pará-lo. A implementação em si é simples: uma thread

monitorando o microfone com VAD enquanto o áudio da resposta toca, e um

evento compartilhado que cancela a reprodução.

Só que num laptop sem fone de ouvido, o alto-falante e o microfone estão a

poucos centímetros um do outro — a própria voz sintetizada do assistente

"vaza" pro microfone, o VAD interpreta isso como o usuário falando, e ele se

autointerrompe imediatamente. Isso é o problema clássico de eco acústico

que qualquer sistema de videoconferência precisa resolver com cancelamento

de eco (AEC) — e não existe solução puramente em nível de VAD pra isso. A

saída prática foi deixar o barge-in desligado por padrão, funcional só com

fone de ouvido.

Um erro recorrente no início do projeto: o modelo inventava a data atual, e

até "explicações" plausíveis pra números que ele mesmo não sabia (tipo

justificar a variação de uma cotação sem ter buscado o motivo). LLMs não têm

noção de tempo real — a "data" que eles "sabem" é qualquer coisa do período

de treinamento.

A correção foi sempre injetar a data/hora real do sistema operacional em

todo turno, como parte do contexto, deixando explícito que essa informação

sobrepõe qualquer suposição do modelo. Combinado com a regra explícita de

"nunca invente uma explicação que não veio literalmente da busca", reduziu

bastante (não elimina 100%) a alucinação.

Trabalhar com LLMs locais pequenos é um exercício constante de não confiar demais no modelo. As técnicas que funcionam bem com modelos de fronteira

Não é menos interessante por isso — é, na real, onde a engenharia em cima

do modelo importa mais.

O projeto completo (agente, pipeline de voz, memória persistente) está disponível no meu repositório — comenta aí se quiser que eu detalhe alguma parte específica num próximo post.

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @ollama 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/construindo-um-assis…] indexed:0 read:6min 2026-07-17 ·