{"slug": "construindo-um-assistente-pessoal-100-local-com-ollama-langchain-e-voz-e-as-que", "title": "Construindo um assistente pessoal 100% local com Ollama, LangChain e voz — e as armadilhas que ninguém conta", "summary": "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.", "body_md": "Nos últimos dias montei um assistente pessoal do zero rodando 100% na minha\n\nmáquina — sem chamadas pra API paga, sem dados saindo do notebook. Ele\n\npesquisa na internet, escreve e salva código, entende voz, responde falando,\n\ntem memória de longo prazo e roda um LLM local via [Ollama](https://ollama.com).\n\nO resultado funciona bem, mas o caminho até aqui expôs um monte de conceitos\n\nque todo mundo que trabalha com LLMs locais/agentes acaba esbarrando —\n\nprincipalmente a diferença brutal entre \"um LLM de 70B+ na nuvem\" e \"um LLM\n\nde 8-14B rodando numa GPU de laptop\". Esse artigo é sobre esses conceitos.\n\n```\nVoz do usuário\n    │\n    ▼\nVAD (detecção de fala) ──► Whisper (fala → texto)\n    │\n    ▼\nAgente LangChain/LangGraph ──► Ollama (LLM local)\n    │                              │\n    │                              ├─► Tools: busca web, clima, memória...\n    ▼                              │\nPiper (texto → fala) ◄─────────────┘\n    │\n    ▼\nÁudio de resposta\n```\n\nTudo roda localmente: o LLM via Ollama, a transcrição via\n\n[faster-whisper](https://github.com/SYSTRAN/faster-whisper), a síntese de\n\nvoz via [Piper](https://github.com/OHF-Voice/piper1-gpl), e um SQLite\n\nguardando memória. Nada trafega pra fora da máquina, exceto as buscas na\n\nweb em si.\n\nUm \"agente\" de IA não é magia — é um LLM rodando dentro de um loop que pode,\n\na cada passo, decidir entre responder em texto ou chamar uma função\n\n(\"tool\"), observar o resultado, e decidir de novo. O\n\n[LangChain 1.0](https://python.langchain.com) formaliza isso com\n\n`create_agent`\n\n, construído em cima do LangGraph, que modela esse loop como\n\numa máquina de estados.\n\nCada \"tool\" é só uma função Python decorada, com uma docstring que vira a\n\ndescrição que o modelo lê pra decidir quando usá-la:\n\n``` php\n@tool\ndef get_weather(city: str) -> str:\n    \"\"\"Retorna o clima atual de uma cidade...\"\"\"\n    ...\n```\n\nO modelo nunca executa a função diretamente — ele gera uma estrutura\n\n(\"tool call\") dizendo \"quero chamar `get_weather`\n\ncom `city='São Paulo'`\n\n\", o\n\nframework executa de verdade e devolve o resultado pro modelo continuar.\n\nIsso foi a maior fonte de dor de cabeça do projeto. Tool calling depende do\n\nmodelo ter sido *treinado* pra gerar uma estrutura específica (geralmente\n\nJSON) num formato que o runtime (Ollama, no meu caso) sabe interpretar. Só\n\nque:\n\n`<tool_call>{\"name\": \"web_search\", ...}</tool_call>`\n\ncomo se fosse a\nresposta final, em vez de o runtime interceptar isso como uma chamada de\nfunção de verdade.`save_code_file`\n\n, e\nele escreveu `# código do jogo da cobrinha...`\n\ncomo se fosse suficiente.\nO mesmo modelo, escrevendo código como texto solto numa resposta normal,\nescreveu o jogo inteiro e funcional.A lição prática: **não force tudo a passar por tool calling estruturado**.\n\nPra geração de conteúdo longo (código, relatórios), deixar o modelo escrever\n\nnaturalmente e fazer o *parsing* do resultado depois — com um comentário\n\nconvencional tipo `# arquivo: nome.py`\n\nque o próprio código extrai via regex\n\n— foi muito mais confiável do que depender do modelo preencher um campo\n\nJSON com cuidado.\n\nOutro padrão que precisei adotar: pedir \"pesquise sobre X e salve um\n\nresumo\" fazia o modelo, em vez de pesquisar de verdade, escrever um *script*\n\nque abriria o Google depois — tecnicamente relacionado ao pedido, mas inútil\n\nna prática.\n\nA correção não foi \"melhorar o prompt\" (tentei, várias vezes). Foi tirar a\n\ndecisão das mãos do modelo: detectar esse padrão de pedido por palavras-chave\n\ne **executar a busca antes de chamar o LLM**, injetando os resultados reais\n\njá prontos no contexto. O modelo só precisa sintetizar o que já está na mão\n\ndele — não decide mais se pesquisa ou não.\n\n``` php\ndef is_research_and_save_request(user_text: str) -> bool:\n    lower = user_text.lower()\n    has_research = any(w in lower for w in _RESEARCH_WORDS)\n    has_save = any(w in lower for w in _SAVE_WORDS)\n    return has_research and has_save\n```\n\nIsso é um lembrete geral trabalhando com LLMs: **quanto mais determinística\numa decisão pode ser feita fora do modelo, mais confiável o sistema fica**.\n\n\"Memória\" em um agente na verdade são pelo menos três coisas diferentes:\n\n`checkpointer`\n\n.`thread_id`\n\nfixa. Troquei de\n`InMemorySaver`\n\n(que zera a cada reinício) para `SqliteSaver`\n\n.`remember_fact`\n\n/`recall_facts`\n\n) que\no modelo aciona quando julga relevante.Um detalhe que pegou muita gente de surpresa (inclusive eu): memória de\n\nconversa persistente **cresce sem limite**. Depois de várias interações, o\n\nhistórico inteiro sendo reenviado a cada turno começou a consumir o espaço\n\nde contexto disponível, cortando respostas longas (como código) no meio.\n\nA solução é resumir automaticamente o histórico antigo quando ele passa de\n\num limite de tokens, mantendo as mensagens recentes intactas — no LangChain\n\n1.0 isso já vem pronto como `SummarizationMiddleware`\n\n.\n\nPra detectar quando alguém está falando (e não gravar silêncio pra sempre),\n\na abordagem ingênua é medir a amplitude do áudio e comparar com um\n\nthreshold. Funciona mal — ruído de fundo, cliques do teclado e a respiração\n\njá bastam pra confundir.\n\nVAD de verdade (usei [webrtcvad](https://github.com/wiseman/py-webrtcvad),\n\noriginalmente do WebRTC do Google) analisa o *espectro* do áudio em janelas\n\ncurtas (10-30ms) pra decidir se aquilo tem características de fala humana,\n\nnão só volume. É ordens de magnitude mais confiável.\n\nQueria que o assistente pudesse ser interrompido no meio da fala — falar\n\npor cima dele deveria pará-lo. A implementação em si é simples: uma thread\n\nmonitorando o microfone com VAD enquanto o áudio da resposta toca, e um\n\nevento compartilhado que cancela a reprodução.\n\nSó que num laptop sem fone de ouvido, o alto-falante e o microfone estão a\n\npoucos centímetros um do outro — a própria voz sintetizada do assistente\n\n\"vaza\" pro microfone, o VAD interpreta isso como o usuário falando, e ele se\n\nautointerrompe imediatamente. Isso é o problema clássico de **eco acústico**\n\nque qualquer sistema de videoconferência precisa resolver com cancelamento\n\nde eco (AEC) — e não existe solução puramente em nível de VAD pra isso. A\n\nsaída prática foi deixar o barge-in desligado por padrão, funcional só com\n\nfone de ouvido.\n\nUm erro recorrente no início do projeto: o modelo inventava a data atual, e\n\naté \"explicações\" plausíveis pra números que ele mesmo não sabia (tipo\n\njustificar a variação de uma cotação sem ter buscado o motivo). LLMs não têm\n\nnoção de tempo real — a \"data\" que eles \"sabem\" é qualquer coisa do período\n\nde treinamento.\n\nA correção foi sempre injetar a data/hora real do sistema operacional em\n\ntodo turno, como parte do contexto, deixando explícito que essa informação\n\nsobrepõe qualquer suposição do modelo. Combinado com a regra explícita de\n\n\"nunca invente uma explicação que não veio literalmente da busca\", reduziu\n\nbastante (não elimina 100%) a alucinação.\n\nTrabalhar com LLMs locais pequenos é um exercício constante de **não confiar\ndemais no modelo**. As técnicas que funcionam bem com modelos de fronteira\n\nNão é menos interessante por isso — é, na real, onde a engenharia em cima\n\ndo modelo importa mais.\n\n*O projeto completo (agente, pipeline de voz, memória persistente) está\ndisponível no meu repositório — comenta aí se quiser que eu detalhe alguma\nparte específica num próximo post.*", "url": "https://wpnews.pro/news/construindo-um-assistente-pessoal-100-local-com-ollama-langchain-e-voz-e-as-que", "canonical_source": "https://dev.to/lincoln_romais/construindo-um-assistente-pessoal-100-local-com-ollama-langchain-e-voz-e-as-armadilhas-que-df4", "published_at": "2026-07-17 17:05:46+00:00", "updated_at": "2026-07-17 17:29:07.756701+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Ollama", "LangChain", "LangGraph", "Whisper", "Piper", "SQLite"], "alternates": {"html": "https://wpnews.pro/news/construindo-um-assistente-pessoal-100-local-com-ollama-langchain-e-voz-e-as-que", "markdown": "https://wpnews.pro/news/construindo-um-assistente-pessoal-100-local-com-ollama-langchain-e-voz-e-as-que.md", "text": "https://wpnews.pro/news/construindo-um-assistente-pessoal-100-local-com-ollama-langchain-e-voz-e-as-que.txt", "jsonld": "https://wpnews.pro/news/construindo-um-assistente-pessoal-100-local-com-ollama-langchain-e-voz-e-as-que.jsonld"}}