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 fully local personal assistant using Ollama, LangChain, and voice tools, running entirely on a laptop without cloud API calls. The project revealed significant challenges in tool calling with smaller local LLMs, including unreliable structured output and models writing scripts instead of executing actions. Key lessons included avoiding forced tool calling for long-form content and pre-processing certain requests to bypass model decision-making.

read6 min views1 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 ·