<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <docs>https://blogs.law.harvard.edu/tech/rss</docs>
    <title>Chromadb on FXShell - DevOps &amp; Sec</title>
    <link>https://fxshell.com.br/tags/chromadb/</link>
    <description>Recent content in Chromadb on FXShell - DevOps &amp; Sec</description>
    <image>
      <title>Chromadb on FXShell - DevOps &amp; Sec</title>
      <link>https://fxshell.com.br/tags/chromadb/</link>
      <url>fxshell.png</url>
    </image>
    <ttl>1440</ttl>
    <generator>Hugo 0.152.2</generator>
    <language>pt-br</language>
    <lastBuildDate>Fri, 04 Sep 2026 13:52:38 UT</lastBuildDate>
    <atom:link href="https://fxshell.com.br/tags/chromadb/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>RAG Local com Ollama: um assistente que responde sobre seus runbooks sem sair da máquina</title>
      <link>https://fxshell.com.br/posts/rag-local-ollama-runbooks/</link>
      <pubDate>Sun, 30 Aug 2026 00:00:00 UT</pubDate>
      <dc:creator>Felipe da Matta</dc:creator>
      <guid>https://fxshell.com.br/posts/rag-local-ollama-runbooks/</guid>
      <description>No meio de um incidente, ninguém tem paciência para caçar num wiki qual é o procedimento de rollback. A informação existe, está escrita, alguém documentou com carinho seis meses atrás, mas está enterrada em algum runbook que ninguém lembra onde fica. O conhecimento existe e mesmo assim não está disponível na hora que importa.
Esse lab ataca exatamente esse problema com uma técnica chamada RAG, montando um assistente que lê a sua própria documentação e responde perguntas em linguagem natural, citando de qual arquivo tirou cada resposta. O detalhe que torna tudo isso viável para documentação interna: roda 100% na sua máquina, sem API paga e sem mandar um único byte para a nuvem.
</description>
      <content:encoded><![CDATA[No meio de um incidente, ninguém tem paciência para caçar num wiki qual é o procedimento de rollback. A informação existe, está escrita, alguém documentou com carinho seis meses atrás, mas está enterrada em algum runbook que ninguém lembra onde fica. O conhecimento existe e mesmo assim não está disponível na hora que importa.
Esse lab ataca exatamente esse problema com uma técnica chamada RAG, montando um assistente que lê a sua própria documentação e responde perguntas em linguagem natural, citando de qual arquivo tirou cada resposta. O detalhe que torna tudo isso viável para documentação interna: roda 100% na sua máquina, sem API paga e sem mandar um único byte para a nuvem.
O diagrama animado abaixo mostra o lab inteiro em movimento, do IBM Bob gerando os runbooks até a resposta citando a fonte no terminal.
Objetivo do Lab Construir um assistente de perguntas e respostas sobre uma base de runbooks de DevOps (deploy, incidentes, segurança e observabilidade), usando modelos de IA que rodam localmente via Ollama. Você faz uma pergunta como &ldquo;como faço o rollback de um deploy que deu errado?&rdquo; e o assistente busca os trechos relevantes na documentação, monta o contexto e gera a resposta citando os arquivos de origem.
Tudo sobe com um comando de Docker Compose. Nada é instalado nativamente na máquina, nem o modelo, nem o banco vetorial, nem as bibliotecas Python. Ao final do post você tem o projeto completo, arquivo por arquivo, com a explicação de cada decisão de implementação.
O que é RAG, sem enrolação RAG é a sigla de Retrieval-Augmented Generation, ou geração aumentada por recuperação. A ideia resolve uma limitação real dos modelos de linguagem: eles sabem muito sobre o mundo em geral, mas não sabem nada sobre a sua empresa, seus runbooks, suas convenções internas. Além disso, quando não sabem, tendem a inventar uma resposta plausível.
O RAG contorna isso invertendo a ordem. Em vez de perguntar direto ao modelo e torcer para ele saber, primeiro a gente busca na nossa própria documentação os trechos mais relevantes para a pergunta, e só então entrega esses trechos ao modelo junto com a pergunta, pedindo que ele responda com base naquele material. O modelo deixa de ser a fonte da verdade e passa a ser quem lê e resume a sua fonte da verdade.
O ganho é duplo. A resposta fica ancorada nos seus documentos, então dá para citar a fonte, e o risco de invenção cai bastante, porque o modelo é instruído a responder apenas com o que recebeu e a admitir quando a informação não está lá.
Tecnologias Utilizadas Antes do código, vale entender cada peça e por que ela está aqui.
Ollama é uma ferramenta que roda modelos de linguagem open source localmente, com uma API HTTP simples. É o que permite usar um LLM na própria máquina sem depender de OpenAI ou qualquer serviço externo. No lab ele roda dois modelos: o llama3.2:3b, um modelo pequeno que gera as respostas e roda até em CPU, e o nomic-embed-text, especializado em transformar texto em vetores.
Embeddings são a peça que faz a busca por significado funcionar. Um modelo de embedding converte um pedaço de texto em uma lista de números (um vetor) que representa o significado daquele texto. Textos com sentido parecido geram vetores próximos no espaço. É isso que permite encontrar o trecho certo mesmo quando a pergunta usa palavras diferentes das que estão no documento. Se você pergunta &ldquo;como desfazer um deploy&rdquo; e o runbook fala em &ldquo;rollback&rdquo;, a busca por palavra-chave não acha nada, mas a busca por vetor acha, porque os dois textos têm significado próximo.
ChromaDB é um banco de dados vetorial. Ele guarda os vetores gerados pelos embeddings e responde rápido à pergunta &ldquo;quais são os trechos mais parecidos com este aqui?&rdquo;. É a memória pesquisável do assistente. No lab ele persiste em disco, então a indexação feita uma vez sobrevive a reinícios.
LangChain é a biblioteca que cola tudo: carrega os documentos, quebra em pedaços, chama o modelo de embedding, conversa com o ChromaDB e monta o prompt final. Ela poupa muito código de integração entre as peças.
Streamlit entrega a interface web em pouquíssimo código Python, sem front-end separado. É onde você digita a pergunta e vê a resposta com as fontes.
Docker Compose orquestra os dois contêineres (Ollama e a aplicação), cuida da ordem de subida e persiste os volumes. É o que torna o lab reproduzível em qualquer máquina com Docker.
Arquitetura O fluxo tem duas fases. A ingestão acontece uma vez, quando os documentos são indexados. A consulta acontece a cada pergunta.
Componente Papel no fluxo Onde roda Ollama Serve o LLM e o modelo de embeddings Contêiner ollama Modelo de embeddings Converte texto em vetor Dentro do Ollama ChromaDB Guarda e busca vetores por similaridade Volume no contêiner app LLM (llama3.2) Gera a resposta a partir do contexto Dentro do Ollama App Python Ingestão, CLI e interface web Contêiner app O mesmo pipeline visto como grafo deixa claro quem chama quem. O Ollama aparece nos dois lados, na ingestão gerando os embeddings e na consulta gerando a resposta.
flowchart TB Bob([IBM Bob gera os runbooks]):::bob subgraph Ingestao[Ingestão · uma vez] direction TB D[Documentos.md .pdf .txt] --> C[Chunks] C --> E[Embeddings] E --> DB[(ChromaDB)] end subgraph Consulta[Consulta · cada pergunta] direction TB Q[Pergunta] --> R[Retrievaltop-k similar] R --> P[Prompt + contexto] P --> L[LLM · llama3.2] L --> A[Respostacom fontes] end Bob --> D DB -.busca por similaridade.-> R O{{Ollama}} -.embeddings.-> E O -.geração.-> L classDef bob fill:#1f6feb,stroke:#388bfd,color:#fff; Subindo tudo com Docker Compose O coração da reprodutibilidade é o docker-compose.yml. Ele declara dois serviços. O ollama usa a imagem oficial, expõe a porta padrão e guarda os modelos baixados em um volume nomeado, para não baixar de novo a cada subida. O app é construído a partir do Dockerfile local, só sobe depois que o Ollama passa no healthcheck e recebe toda a configuração por variáveis de ambiente.
services: ollama: image: ollama/ollama:latest container_name: rag-ollama ports: - &#34;11434:11434&#34; volumes: - ollama_models:/root/.ollama healthcheck: test: [&#34;CMD&#34;, &#34;ollama&#34;, &#34;list&#34;] interval: 10s timeout: 5s retries: 10 restart: unless-stopped app: build: . container_name: rag-app depends_on: ollama: condition: service_healthy environment: OLLAMA_HOST: http://ollama:11434 LLM_MODEL: llama3.2:3b EMBED_MODEL: nomic-embed-text CHROMA_DIR: /data/chroma volumes: - ./docs:/app/docs:ro - chroma_data:/data/chroma ports: - &#34;8501:8501&#34; command: streamlit run app.py --server.address=0.0.0.0 --server.port=8501 volumes: ollama_models: chroma_data: Dois detalhes que evitam dor de cabeça. O depends_on com condition: service_healthy garante que a aplicação não tente falar com o Ollama antes dele estar pronto, um erro clássico que gera falha intermitente na primeira subida. E a pasta docs é montada como somente leitura (:ro), porque a aplicação só lê os documentos, nunca escreve neles.
O ponto de configuração único Toda a configuração vive em common.py, lido pela ingestão, pela CLI e pela interface. Centralizar isso evita repetir a criação dos clientes em cada arquivo e mantém um único lugar para trocar o modelo ou o diretório do banco.
import os from langchain_ollama import OllamaEmbeddings, ChatOllama from langchain_chroma import Chroma OLLAMA_HOST = os.getenv(&#34;OLLAMA_HOST&#34;, &#34;http://localhost:11434&#34;) LLM_MODEL = os.getenv(&#34;LLM_MODEL&#34;, &#34;llama3.2:3b&#34;) EMBED_MODEL = os.getenv(&#34;EMBED_MODEL&#34;, &#34;nomic-embed-text&#34;) CHROMA_DIR = os.getenv(&#34;CHROMA_DIR&#34;, &#34;./chroma&#34;) COLLECTION = &#34;runbooks-devops&#34; def get_embeddings(): return OllamaEmbeddings(model=EMBED_MODEL, base_url=OLLAMA_HOST) def get_llm(): return ChatOllama(model=LLM_MODEL, base_url=OLLAMA_HOST, temperature=0.1) def get_vectorstore(): return Chroma( collection_name=COLLECTION, embedding_function=get_embeddings(), persist_directory=CHROMA_DIR, ) A temperature=0.1 no LLM é uma escolha deliberada. Temperatura controla o quão criativo o modelo é, e num assistente de runbook a gente quer o contrário de criatividade: quer a resposta mais previsível e fiel ao contexto possível. Temperatura baixa deixa o modelo mais determinístico e menos propenso a florear.
A fase de ingestão em detalhe A ingestão lê os documentos da pasta docs/, quebra cada um em pedaços menores, gera os embeddings e grava no ChromaDB. O diagrama abaixo mostra esse caminho, com o Ollama alimentando a etapa de embeddings.
O código carrega cada tipo de arquivo com o loader adequado, quebra em chunks e grava:
from langchain_community.document_loaders import ( DirectoryLoader, TextLoader, PyPDFLoader, UnstructuredMarkdownLoader, ) from langchain_text_splitters import RecursiveCharacterTextSplitter from common import get_vectorstore def carregar_documentos(): loaders = [ DirectoryLoader(&#34;docs&#34;, glob=&#34;**/*.md&#34;, loader_cls=UnstructuredMarkdownLoader), DirectoryLoader(&#34;docs&#34;, glob=&#34;**/*.txt&#34;, loader_cls=TextLoader), DirectoryLoader(&#34;docs&#34;, glob=&#34;**/*.pdf&#34;, loader_cls=PyPDFLoader), ] docs = [] for loader in loaders: docs.extend(loader.load()) return docs splitter = RecursiveCharacterTextSplitter( chunk_size=800, chunk_overlap=120, separators=[&#34;\n## &#34;, &#34;\n### &#34;, &#34;\n\n&#34;, &#34;\n&#34;, &#34;. &#34;, &#34; &#34;], ) chunks = splitter.split_documents(carregar_documentos()) store = get_vectorstore() store.add_documents(chunks) A quebra em pedaços (chunking) importa mais do que parece. Um documento inteiro é grande demais para caber no contexto do modelo e, pior, dilui o significado: o vetor de um documento gigante vira uma média borrada de vários assuntos, e a busca perde precisão. Um chunk de tamanho médio mantém cada trecho coeso em torno de um assunto só.
O chunk_overlap=120 faz cada pedaço compartilhar 120 caracteres com o vizinho. Isso resolve o problema da informação que cai bem na fronteira entre dois chunks: sem sobreposição, uma frase importante poderia ser cortada ao meio e não aparecer inteira em nenhum dos dois pedaços. Com sobreposição, ela aparece completa em pelo menos um.
A lista de separators diz ao splitter a ordem de preferência para cortar. Ele tenta cortar primeiro em títulos de seção, depois em parágrafos, depois em linhas, e só corta no meio de uma frase como último recurso. O resultado são chunks que respeitam a estrutura do documento em vez de picotar no caractere 800 na marra.
A fase de consulta em detalhe Na consulta, a pergunta passa pelo mesmo modelo de embedding e vira um vetor. O ChromaDB devolve os trechos mais próximos, que viram contexto de um prompt enviado ao LLM. O diagrama abaixo mostra o caminho completo, da pergunta até a resposta com fonte.
O código da CLI é curto porque o common.py já fez o trabalho pesado:
from common import ( get_llm, get_vectorstore, montar_contexto, fontes_unicas, PROMPT_TEMPLATE, ) def responder(pergunta, k=4): store = get_vectorstore() docs = store.similarity_search(pergunta, k=k) # busca por vetor contexto = montar_contexto(docs) prompt = PROMPT_TEMPLATE.format(contexto=contexto, pergunta=pergunta) resposta = get_llm().invoke(prompt).content print(resposta.strip()) print(&#34;Fontes:&#34;, &#34;, &#34;.join(fontes_unicas(docs))) O similarity_search(pergunta, k=4) é onde a mágica acontece. Ele converte a pergunta em vetor com o mesmo modelo de embedding usado na ingestão (isso é fundamental, os dois lados precisam falar a mesma língua vetorial) e pede ao ChromaDB os quatro trechos cujos vetores estão mais próximos. Quatro é um equilíbrio: contexto suficiente para responder bem, sem encher o prompt de ruído que confunde o modelo.
O prompt que segura a invenção A parte que separa um assistente confiável de um gerador de respostas plausíveis está no prompt. Ele monta o contexto com a origem de cada trecho e dá a regra explícita ao modelo.
PROMPT_TEMPLATE = &#34;&#34;&#34;Você é um assistente técnico de DevOps. Responda à \ pergunta usando somente o contexto abaixo, extraído da documentação interna. \ Se o contexto não contiver a resposta, diga que não encontrou a informação na \ documentação, sem inventar. Responda em português, de forma objetiva, e cite \ os arquivos de origem ao final. Contexto: {contexto} Pergunta: {pergunta} Resposta:&#34;&#34;&#34; def montar_contexto(docs): blocos = [] for d in docs: origem = d.metadata.get(&#34;source&#34;, &#34;desconhecido&#34;) blocos.append(f&#34;[Fonte: {origem}]\n{d.page_content}&#34;) return &#34;\n\n---\n\n&#34;.join(blocos) Duas instruções fazem o trabalho. A primeira manda responder somente com base no contexto. A segunda manda admitir quando a informação não está lá, em vez de preencher a lacuna. Sem a segunda instrução, o modelo tende a inventar uma resposta que parece certa, e num runbook isso é perigoso. Marcar cada trecho com [Fonte: arquivo] no contexto é o que permite o modelo citar de onde tirou a resposta no final.
Ingestão idempotente no entrypoint O entrypoint.sh garante que tudo esteja pronto antes da aplicação subir: baixa os modelos no Ollama e faz a ingestão inicial, mas só se o banco ainda estiver vazio.
#!/usr/bin/env bash set -euo pipefail echo &#34;[entrypoint] Garantindo modelos no Ollama...&#34; curl -s &#34;${OLLAMA_HOST}/api/pull&#34; -d &#34;{\&#34;name\&#34;: \&#34;${LLM_MODEL}\&#34;}&#34; &gt; /dev/null curl -s &#34;${OLLAMA_HOST}/api/pull&#34; -d &#34;{\&#34;name\&#34;: \&#34;${EMBED_MODEL}\&#34;}&#34; &gt; /dev/null if [ ! -f &#34;${CHROMA_DIR}/chroma.sqlite3&#34; ]; then echo &#34;[entrypoint] Banco vazio. Rodando ingestão inicial...&#34; python ingest.py else echo &#34;[entrypoint] Banco já existe. Pulando ingestão.&#34; fi exec &#34;$@&#34; Isso segue a mesma lógica de qualquer automação de infraestrutura bem feita: rodar duas vezes deve levar ao mesmo estado, sem duplicar dados nem quebrar o que já funcionava. O ollama pull é naturalmente idempotente, se o modelo já existe ele apenas confirma. E a ingestão só roda quando o arquivo do ChromaDB ainda não existe, então reiniciar o contêiner não reindexa tudo de novo à toa.
A interface web O app.py monta a interface em Streamlit. Além da resposta, ele mostra num expander os trechos exatos que foram recuperados, o que é ótimo para entender por que o modelo respondeu daquele jeito.
import streamlit as st from common import get_llm, get_vectorstore, montar_contexto, fontes_unicas, PROMPT_TEMPLATE store, llm = get_vectorstore(), get_llm() pergunta = st.text_input(&#34;Pergunte sobre a documentação de infraestrutura:&#34;) if pergunta: docs = store.similarity_search(pergunta, k=4) contexto = montar_contexto(docs) resposta = llm.invoke(PROMPT_TEMPLATE.format(contexto=contexto, pergunta=pergunta)).content st.markdown(resposta) st.info(&#34;Fontes: &#34; + &#34;, &#34;.join(fontes_unicas(docs))) with st.expander(&#34;Ver os trechos recuperados&#34;): for i, d in enumerate(docs, 1): st.markdown(f&#34;**Trecho {i}** · `{d.metadata.get(&#39;source&#39;)}`&#34;) st.text(d.page_content) Mostrar os trechos recuperados não é enfeite. Quando a resposta parece estranha, o expander revela se o problema foi recuperação ruim (trouxe o trecho errado) ou geração ruim (trouxe o certo mas o modelo respondeu mal). Essa distinção é o primeiro passo para melhorar um RAG.
Executando o lab Pré-requisito é só Docker e Docker Compose. Para subir tudo:
docker compose up --build Na primeira vez o Ollama baixa os dois modelos (alguns GB) e a ingestão roda automaticamente. Depois a interface fica em http://localhost:8501.
Antes de confiar na interface, vale um teste rápido pela linha de comando:
docker compose run --rm app python rag.py &#34;como faço o rollback de um deploy?&#34; A resposta aponta o kubectl rollout undo e cita runbook-deploy.md como fonte, exatamente o trecho que está na documentação. Se você perguntar algo que não está em nenhum runbook, o assistente responde que não encontrou a informação, em vez de inventar. Esse é o teste que mais importa: um RAG que inventa é pior que documentação nenhuma, porque dá uma resposta confiante e errada.
Ao trocar os documentos em docs/, reindexe com um comando:
docker compose run --rm app python ingest.py Para que serve no mercado Esse padrão está em todo lugar hoje, com nomes diferentes. Um time de SRE aponta o RAG para a base de runbooks e transforma a documentação de incidente em algo consultável em linguagem natural durante a madrugada. Um time de suporte aponta para a base de conhecimento e reduz o tempo de primeira resposta. Uma área de compliance aponta para políticas internas e responde dúvidas de auditoria citando o documento exato.
A versão local tem um apelo específico: privacidade. Muita documentação interna não pode ser enviada para uma API de terceiros por questão contratual ou regulatória. Rodando o modelo na própria infraestrutura, o dado nunca sai do perímetro. É a diferença entre poder usar IA sobre a documentação sensível e não poder.
Conclusão O que esse lab mostra não é um modelo mais inteligente, é um jeito melhor de usar um modelo que você já tem. A inteligência aqui está na arquitetura: separar a recuperação da geração, ancorar a resposta em fontes reais, instruir o modelo a admitir o que não sabe e baixar a temperatura para priorizar fidelidade sobre criatividade. Isso transforma um gerador de texto genérico em um assistente que responde sobre o seu material específico e mostra de onde tirou cada afirmação.
E o fato de tudo caber em dois contêineres, sem custo de API e sem dado saindo da máquina, tira a IA do território de prova de conceito cara e coloca ela como uma ferramenta que qualquer time pode subir num sábado à tarde e apontar para a própria documentação.
Referências Ollama: https://github.com/ollama/ollama ChromaDB: https://github.com/chroma-core/chroma LangChain: https://python.langchain.com Nomic Embed: https://ollama.com/library/nomic-embed-text ]]></content:encoded>
    </item>
  </channel>
</rss>
