.NET
- RAG com Ollama e Kernel Memory
![]() |
Que tal criar um sistema usando o RAG com Ollama e o Microsoft Kernel Memory ? |
O termo RAG é muito popular na inteligência artificial ele significa Retrieval-Augmented Generation e é uma arquitetura híbrida que combina um sistema de recuperação de informação com um modelo de geração de linguagem natural (como o GPT).
Em termos simples, o RAG é uma técnica que permite a uma IA:
- Buscar
informações relevantes e atualizadas em uma base de dados externa (como
documentos, sites internos, PDFs, etc.).
- Inserir essas informações no
"contexto" ou no "prompt" do modelo de linguagem.
- Gerar uma resposta
precisa, contextualizada e com base nas informações recuperadas.
Como o RAG Funciona? (O Processo em 3 Etapas)
O processo
do RAG pode ser dividido em três etapas principais:
1- Recuperação
(Retrieval)
Quando o usuário faz uma pergunta, o sistema primeiro a
converte em uma representação numérica (um "vetor" ou "embedding").
Em
seguida, ele busca em uma base de dados pré-processada (também convertida em
vetores) os trechos de texto, documentos ou pedaços de informação mais
semelhantes à pergunta. É como um sistema de busca super eficiente.
2-
Aumento (Augmentation)
As informações mais relevantes encontradas na
etapa anterior são combinadas com a pergunta original do usuário.
Tudo
isso é montado em um "prompt" único e contextualizado, que é enviado para o
modelo de linguagem grande (LLM). Algo como: "Com base nestes documentos
[informações recuperadas], responda à seguinte pergunta: [pergunta do usuário]".
3- Geração (Generation)
O LLM (como o GPT-4, Llama, etc.) recebe esse
prompt enriquecido com o contexto.
Ele, então, gera uma resposta final
que não se baseia apenas no seu conhecimento interno (que pode estar
desatualizado ou ser genérico), mas sim nas informações específicas e confiáveis
que lhe foram fornecidas.
Embeddings : O coração do RAG
O RAG depende dos embeddings para a etapa mais crítica: a Recuperação (Retrieval).
Um embedding é uma representação numérica (vetorial) de dados como texto,
imagens ou áudio. No contexto do RAG e do processamento de linguagem, falamos
principalmente de embeddings de texto.
Pense no embedding como uma forma
de traduzir palavras, frases ou documentos inteiros para uma linguagem que o
computador entende — a linguagem da matemática e da geometria.
Veja como isso se encaixa no processo:
Fase 1: Pré-processamento da Base
de Conhecimento (Fora de Linha)
Dividir: A base de dados (manuais, PDFs,
sites) é dividida em pedaços menores, como parágrafos ou seções.
Embedding: Cada um desses pedaços de texto é convertido em um embedding
por um modelo especializado nessa tarefa (como o text-embedding-ada-002 da
OpenAI ou modelos open-source como os da SentenceTransformers).
Armazenar: Todos esses embeddings são armazenados em um banco de dados
especializado, chamado Vector Database (Banco de Dados Vetorial).
Fase 2:
Busca em Tempo Real (Quando o Usuário Pergunta)
Embedding da
Pergunta: Quando você faz uma pergunta (ex: "Quais são os benefícios do
RAG?"), essa pergunta é convertida em um embedding usando o mesmo modelo da Fase
1.
Busca por Similaridade: O sistema compara o embedding da sua pergunta
com todos os embeddings dos documentos armazenados no banco de dados vetorial.
Ele busca os pedaços de texto cujos embeddings são mais próximos (ou seja, mais
similares em significado) ao da sua pergunta.
Recuperação do Texto
Original: Os textos originais correspondentes aos embeddings mais similares são
recuperados.
Fase 3: Geração da Resposta
Aumento: Os textos
recuperados (que são os mais relevantes para sua pergunta) são injetados no
"prompt" do modelo de linguagem (LLM).
Geração: O LLM (como o GPT-4)
sintetiza aquelas informações e gera uma resposta final, precisa e
contextualizada.

Entendendo o nomic-embed-text:latest
È muito importante entender o que é e como funciona o modelo de embeddings usado no exemplo.
O nomic-embed-text:latest é um modelo de embeddings (um
modelo especializado em transformar textos em vetores numéricos).
Ele é mantido pela Nomic AI e está disponível no Ollama —
portanto, roda localmente (sem precisar da nuvem da OpenAI) sendo responsável
por converter textos em números que representam o significado do texto (não
só as palavras).
Texto Representação vetorial (simplificada)
“gato preto” [0.12, -0.87, 0.33, 0.91,
...]
“felino
de cor escura” [0.14, -0.83, 0.30, 0.89, ...]
Esses vetores
ficam próximos entre si no espaço multidimensional, porque têm significados
semelhantes — ambos falam sobre o mesmo assunto “gato preto” e “felino
de cor escura” mesmo usando palavras diferentes.
Assim,
o Kernel Memory consegue buscar por similaridade semântica, mesmo que as
palavras sejam diferentes. A seguir temos um resumo do fluxo interno que ocorre
você faz uma pergunta:
![]() |
Nesta etapa, o texto da pergunta é transformado em
um vetor numérico que
representa o seu significado. O sistema então compara esse vetor com os vetores
dos documentos indexados.
Assim, mesmo que
as palavras não coincidam, o sistema entende o
sentido da pergunta e encontra os
trechos mais relevantes.”
Criando o projeto exemplo
Para ilustrar o uso deste recurso na prática vamos criar uma aplicação Console no VS 2022 usando o Ollama e Microsoft.Kernel Memory.
Vamos usar o Ollama instalado localmente e em execução na porta 11434, o LLM mistral:latest baixado localmente e o modelo de embeddings : nomic-embed-text:latest

Crie um novo projeto Console no VS 2022 chamado Ollama_Rag e neste projeto crie a pasta Data, a pasta Models e a pasta Utils.
Adicione os seguintes pacotes Nuget no projeto:
Microsoft.KernelMemory.Core
Microsoft.KernelMemory.AI.Ollama
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.Json
Dentro da pasta Models crie o arquivo AppConfig.cs com o seguinte conteúdo:
public class AppConfig { public OllamaSettings Ollama { get; set; } = new(); public List<string> DocumentPaths { get; set; } = new(); public MemorySettings Memory { get; set; } = new(); } public class OllamaSettings { public string Endpoint { get; set; } = "http://localhost:11434/"; public ModelSettings TextModel { get; set; } = new(); public ModelSettings EmbeddingModel { get; set; } = new(); } public class ModelSettings { public string Name { get; set; } = ""; public int MaxTokenTotal { get; set; } = 2048; public int Seed { get; set; } = 42; public int TopK { get; set; } = 7; } public class MemorySettings { public int ChunkSize { get; set; } = 1024; public int ChunkOverlap { get; set; } = 200; public int MaxResults { get; set; } = 5; public double MinRelevance { get; set; } = 0.3; public int MaxChatHistory { get; set; } = 10; } |
AppConfig - É a classe raiz que reúne tudo e que será retornada pelo método LoadConfiguration() na inicialização da aplicação console.
OllaSettings - Configura o endereço do Ollama e os dois
modelos usados:
– um para gerar texto (TextModel)
– outro
para gerar embeddings (EmbeddingMode)
ModelSettings - Define parâmetros internos do modelo (nome, limite de tokens, aleatoriedade, profundidade de busca Top-K).
MemorySettings - Controla como o Kernel Memory corta, sobrepõe e recupera os textos.
A seguir crie na raiz do projeto o arquivo appsettings.json com este conteúdo:
{ "Ollama": { "Endpoint": "http://localhost:11434/", "TextModel": { "Name": "mistral:latest", "MaxTokenTotal": 125000, "Seed": 42, "TopK": 7 }, "EmbeddingModel": { "Name": "nomic-embed-text:latest", "MaxTokenTotal": 2048 } }, "DocumentPaths": [ "Data/AdrianStellar.txt" ], "Memory": { "ChunkSize": 1024, "ChunkOverlap": 200, "MaxResults": 5, "MinRelevance": 0.3, "MaxChatHistory": 10 } } |
A classe AppConfig serve para mapear o conteúdo do
appsettings.json para as classes definidas
permitindo que todo o
seu código use configurações fortemente tipadas, organizadas e fáceis de
ajustar,
sem precisar alterar o código principal. O JSON de appsettings.json
é mapeado recursivamente para
dentro das classes, como se você estivesse montando um objeto complexo a partir
do arquivo.
Dentro da pasta Models crie a classe ChatMessage:
public class ChatMessage { public string Role { get; set; } public string Content { get; set; } public DateTime Timestamp { get; set; } public ChatMessage(string role, string content) { Role = role; Content = content; Timestamp = DateTime.UtcNow; } } |
Essa classe não faz parte da
configuração, e portanto não
é preenchida pelo appsettings.json.
Ela é usada durante a execução do
programa — mais especificamente, para armazenar
as mensagens trocadas na conversa.
Na pasta Data vamos incluir um arquivo texto chamado AdrianStellar.txt com o seguinte conteúdo:
| Adrian Stellar was a young inventor from the
city of Lumina Prime. He built the first solar-powered airship capable of crossing the Frozen Sea. People called him “The Engineer of Light” because his inventions used sun crystals. In 2095, he discovered a lost library hidden beneath the desert of Arion. There, he found ancient blueprints for machines that could store human memories. Adrian devoted his life to building a device called the Mind Mirror. The Mind Mirror allowed people to relive their happiest moments in perfect detail. One day, a malfunction trapped Adrian inside his own memories forever. His final message was a note saying, “Light remembers what time forgets.” To this day, the city of Lumina Prime celebrates the Festival of Light in his honor. |
Este arquivo contém uma história fictícia que um personagem fictício que é uma história exclusiva, impossível de o modelo conhecer de antemão. Assim, qualquer resposta correta será prova de que o contexto do arquivo foi realmente usado.
Nosso objetivo é implementar o RAG e vamos oriendar o LLM para responder apenas com base no conteúdo deste arquivo. Assim podemos fazer perguntas do tipo:
| Who was Adrian Stellar? How people called Adrian Stellar ? What did Adrian Stellar invent? What is the Mind Mirror? What was his final message? Where was the lost library discovered? What the city of Lumina Prime celebrates ? |
Se o RAG estiver funcionando, ele responderá com trechos da história (por
exemplo: “Adrian Stellar was a young inventor from the city of Lumina
Prime.”).
Se o modelo responder algo genérico (“I don’t
know” ou inventar algo incoerente), é sinal de que o RAG não recuperou
contexto corretamente.
Nota: O Texto esta em inglês, e, as perguntas devem ser feitas em inglês, porque tanto o LLM usado como o modelo de embedding foram treinados em inglês e funcionam em conjunto muito bem. (Não funcionariam corretamente para textos em português)
Na pasta Utils crie a classe KernelMemoryHelper:
using Microsoft.Extensions.Configuration; using Microsoft.KernelMemory; using Microsoft.KernelMemory.AI.Ollama; using Ollama_RAG.Models; using System.Text; namespace Ollama_RAG.Utils; public static class KernelMemoryHelper { // 🧩 Lê e vincula o appsettings.json nas classes AppConfig public static AppConfig LoadConfiguration() { var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build(); var cfg = new AppConfig(); configuration.Bind(cfg); return cfg; } // ⚙️ Inicializa o Kernel Memory configurando os modelos e o Ollama public static Task<IKernelMemory> InitializeKernelMemoryAsync(AppConfig cfg) { var ollamaConfig = new OllamaConfig { Endpoint = cfg.Ollama.Endpoint, TextModel = new OllamaModelConfig(cfg.Ollama.TextModel.Name) { MaxTokenTotal = cfg.Ollama.TextModel.MaxTokenTotal, Seed = cfg.Ollama.TextModel.Seed, TopK = cfg.Ollama.TextModel.TopK }, EmbeddingModel = new OllamaModelConfig(cfg.Ollama.EmbeddingModel.Name) { MaxTokenTotal = cfg.Ollama.EmbeddingModel.MaxTokenTotal } }; var memory = new KernelMemoryBuilder() .WithOllamaTextGeneration(ollamaConfig) .WithOllamaTextEmbeddingGeneration(ollamaConfig) .WithSimpleVectorDb() .WithSimpleFileStorage() .Build<MemoryServerless>(); return Task.FromResult<IKernelMemory>(memory); } // 📁 Indexa os documentos (lê, gera embeddings e salva no banco vetorial) public static async Task IndexDocumentsAsync(IKernelMemory mem, IEnumerable<string> paths) { var tasks = paths .Where(File.Exists) .Select(async path => { Console.WriteLine($"Indexando: {path}"); var name = Path.GetFileNameWithoutExtension(path); await mem.ImportTextAsync( await File.ReadAllTextAsync(path), documentId: name, tags: new TagCollection { { "sourceName", name } } // ✅ salva como tag ); }); await Task.WhenAll(tasks); Console.WriteLine("Indexação de documentos concluída."); } // 💬 Constrói a query contextual com base no histórico de conversa public static string ComposeQuery(List<ChatMessage> history, string input) { var sb = new StringBuilder("Conversa anterior:\n"); foreach (var msg in history.TakeLast(6)) sb.AppendLine($"{msg.Role}: {msg.Content}"); sb.AppendLine($"\nQuestão atual: {input}");
return sb.ToString(); } // 🤖 Gera a resposta com base nos resultados do RAG public static async Task<string> GenerateResponseAsync(IKernelMemory mem, string question, SearchResult results) { var sb = new StringBuilder(); sb.AppendLine("Você é um assistente. Use apenas o contexto fornecido."); sb.AppendLine("\nCONTEXTO:"); foreach (var res in results.Results) foreach (var part in res.Partitions) sb.AppendLine(part.Text + "\n---"); sb.AppendLine($"\nQUESTÃO: {question}\nRESPOSTA:");
try { var answer = await mem.AskAsync(sb.ToString()); return answer.Result; } catch (Exception ex) { return $"Erro a gerar resposta: {ex.Message}"; } } } |
Esta classe atua da seguinte forma:
- Centraliza toda a lógica de inicialização e configuração do Kernel Memory e do Ollama. - Lê o arquivo appsettings.json e converte suas configurações nas classes C# (AppConfig). - Cria e configura o Kernel Memory com os modelos de texto e de embeddings. - Realiza a indexação dos documentos, gerando e armazenando os embeddings. - Monta consultas contextuais combinando a pergunta e o histórico do chat. - Gera respostas usando o RAG, com base nos trechos recuperados dos documentos. - Atua como uma camada utilitária reutilizável, separando a lógica de infraestrutura do código principal |
Finalmente na classe Program vamos incluir o código a seguir:
using Ollama_RAG.Models; using Ollama_RAG.Utils; // 👈 Importa a classe estática Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("=== RAG com Ollama ===\n"); // 🧩 Usa os métodos estáticos da nova classe var config = KernelMemoryHelper.LoadConfiguration(); Console.WriteLine("Inicializando o Kernel Memory..."); var memory = await KernelMemoryHelper.InitializeKernelMemoryAsync(config); Console.WriteLine("Indexando documentos..."); await KernelMemoryHelper.IndexDocumentsAsync(memory, config.DocumentPaths); Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine("\nRAG está pronto! Faça perguntas sobre o documento indexado."); Console.WriteLine("Digite 'sair' para encerrar.\n"); Console.ResetColor(); var chatHistory = new List<ChatMessage>(); while (true) { Console.ForegroundColor = ConsoleColor.Gray; Console.Write("\nVocê: "); Console.ResetColor(); var userInput = Console.ReadLine(); if (string.IsNullOrWhiteSpace(userInput)) continue; if (userInput.Equals("sair", StringComparison.OrdinalIgnoreCase)) break; chatHistory.Add(new ChatMessage("User", userInput)); var contextualQuery = KernelMemoryHelper.ComposeQuery(chatHistory, userInput); var searchResults = await memory.SearchAsync( query: contextualQuery, limit: config.Memory.MaxResults, minRelevance: config.Memory.MinRelevance ); if (!searchResults.Results.Any()) { Console.WriteLine("Modelo: Nenhuma informação relevante encontrada."); continue; } var response = await KernelMemoryHelper.GenerateResponseAsync(memory, userInput, searchResults); Console.ForegroundColor = ConsoleColor.Cyan; Console.Write("Modelo: "); Console.ForegroundColor = ConsoleColor.White; Console.WriteLine(response); Console.ResetColor(); chatHistory.Add(new ChatMessage("Modelo", response)); if (chatHistory.Count > config.Memory.MaxChatHistory * 2) chatHistory.RemoveRange(0, config.Memory.MaxChatHistory); } |
Esta classe coordena a execução do projeto RAG (Retrieval-Augmented Generation) integrando o usuário, o Kernel Memory e o Ollama.
| 1- Inicializa o console e exibe mensagens
iniciais → Mostra o título e prepara o ambiente para interação do usuário. 2- Carrega as configurações do sistema → Usa KernelMemoryHelper.LoadConfiguration() para ler e vincular o appsettings.json nas classes C# (AppConfig). 3- Inicializa o Kernel Memory e os modelos do Ollama → Chama KernelMemoryHelper.InitializeKernelMemoryAsync() para configurar o modelo de texto (mistral) e o modelo de embeddings (nomic-embed-text). 4- Indexa os documentos → Executa KernelMemoryHelper.IndexDocumentsAsync() para converter os arquivos em embeddings e armazená-los para busca semântica. 5- Entra no loop principal de interação → Lê as perguntas do usuário no console e controla o fluxo do chat. 6- Monta e executa consultas RAG → Usa ComposeQuery e SearchAsync para buscar trechos relevantes nos documentos e gerar respostas contextualizadas. 7- Exibe a resposta e mantém o histórico da conversa → Mostra a resposta no console, armazena as mensagens no chatHistory e garante que o modelo mantenha o contexto entre as perguntas. |
Importante destacar que na linha de código:
Console.WriteLine("\nRAG está pronto! Faça perguntas sobre o documento indexado.");
Estamos limitando o LLM a consultar o documento AdrianStellar.txt
Executando o projeto e fazendo uma pergunta teremos o seguinte resultado:

O projeto RAG foi configurado para indexar
um único documento (AdrianStellar.txt),
contendo uma história fictícia.
Esse
documento é a única fonte de
conhecimento do modelo. Portanto, tudo que o modelo souber deve vir
deste arquivo, não do seu
conhecimento prévio.
Pergunta : Who was Adrian Stellar ?
O que aconteceu internamente:
O sistema converteu sua pergunta em
embeddings usando o modelo nomic-embed-text:latest.
O Kernel Memory comparou
esses embeddings com os vetores criados a partir do texto do arquivo.
Como o
documento contém várias informações sobre Adrian Stellar, a busca encontrou alta
similaridade semântica e retornou os trechos correspondentes.
Esses trechos
foram enviados como contexto ao modelo mistral:latest, com a instrução:
“Use
only the provided context.”
Resposta do modelo:
“Adrian Stellar was a young
inventor from the city of Lumina Prime...”
O modelo respondeu
exatamente com base nas informações do texto, sem inventar nada.
Ele
citou corretamente que Adrian era “a young inventor from the city of Lumina
Prime” e mencionou os elementos da história (Mind Mirror, sun crystals,
Festival of Light).
Isso prova que o RAG funcionou corretamente,
pois ele buscou e gerou a resposta com base no conteúdo indexado.
Vamos fazer uma pergunta que não esta no contexto do documento :

Pergunta : When Adrian Stellar was born ?
Aqui a pergunta foi novamente transformada em embeddings.
O Kernel Memory
procurou por trechos semanticamente próximos à ideia de “data de
nascimento”.
Nenhum trecho no documento continha essa informação
(não há “born”, “birthday”, “date”, etc.).
O
RAG, portanto, não encontrou contexto relevante. O prompt enviado ao modelo
continha apenas o aviso:
“The context provided does not specify when
Adrian Stellar was born.”
Resposta do modelo:
“The context provided does not
specify when Adrian Stellar was born. Therefore, I cannot provide an answer for
this question based on the information given.”
O modelo não inventou
uma resposta — ele seguiu a instrução do prompt:
“Use only the provided
context.”
Isso mostra que o modelo respeitou a limitação do RAG, e
não usou conhecimento “de fora”.
Essa é uma característica desejada em
sistemas de IA responsáveis: só responder com base em dados confiáveis.
O resultado mostra claramente como o sistema RAG atua em duas fases:
1- Primeiro, ele recupera informação dos documentos (Retrieval),
2-
Depois, gera uma resposta textual baseada nesses dados (Generation).
Se a informação está no documento, ele responde.
Se não está, ele diz
que não sabe — e isso é o comportamento correto de um assistente de IA conectado
a uma base de conhecimento.”
Aqui o RAG mostra seu comportamento ideal: quando a resposta não existe no contexto recuperado, o modelo não ‘chuta’, não 'alucina', apenas informa que a informação não está disponível.”
E estamos conversados...
"Porque pela graça sois salvos, por meio da fé; e
isto não vem de vós, é dom de Deus.
Não vem das obras, para que ninguém se
glorie"
Efésios 2:8,9
Referências:
NET - Unit of Work - Padrão Unidade de ...