Otimizando RAG e Redução de Tokens com Markdown Chunking e Cache no .NET
Como reduzi o consumo de tokens sem precisar de um vector db
Um desafio que encontrei (tanto de latência quanto de custo) integrando um agente de IA na plataforma da Nuvemshop, é a injeção de conttexto.
As lojas possuem páginastextensas de termos de uso, políticas de troca, devolução e regras de frete. A API da Nuvemshop entrega todo esse conteúdo em HTML bruto.
Se eu simplesmente enviasse o HTML retornado da API para a tool de busca do agente, consumiria milhares de tokens desnecessários com tags como <div>, <p>, <span> e estilos embutidos, além de poluir a atenção do modelo.
Hoje, reestruturei essa funcionalidade na minha API para resolver o problema de uma vez.
O Desafio Arquitetural e Clean Architecture
Seguindo os princípios de Clean Architecture e DDD:
- O formato HTML é um detalhe de entrega específico da APItexterna da Nuvemshop.
- A camada de Application não deve saber se a política veio em HTML, JSON ou ttexto puro.
- Portanto, a biblioteca de conversão (ReverseMarkdown) foi isolada na camada de Infra, entregando para a aplicação um modelo limpo em Markdown.
// Infrastructure: Conversão isolada no Adaptertexterno
public async Task<StorePolicies> GetPoliciesAsync(string storeId, CancellationToken ct = default)
{
var rawHtmlPolicies = await _httpClient.GetFromJsonAsync<NuvemshopRawPolicies>($"/v1/{storeId}/policies", ct);
var converter = new ReverseMarkdown.Converter();
return new StorePolicies(
RefundPolicy: converter.Convert(rawHtmlPolicies.RefundHtml),
ShippingPolicy: converter.Convert(rawHtmlPolicies.ShippingHtml)
);
}
Chunking Estrutural sem Banco Vetorial
Adicionar um banco vetorial (PgVector, Qdrant, Pinecone) para fazer RAG neste estágio adicionaria complexidade operacional e custo desnecessários para o meu projeto.
Implementei uma abordagem baseada em regras léxicas e hierarquia estrutural:
- Quebra por Cabeçalhos: O PolicyChunkingService divide o Markdown em seções baseadas em títulos (#, ##, ###).
- Normalização em Português: O PortugueseTtextNormalizer remove acentuações e pontuações, permitindo matching flexível em português.
- Scoring Ponderado: O termo pesquisado pontua mais alto se coincidir com o título da seção (peso 3x) do que com o corpo do ttexto (peso 1x).
- Retorno Enxuto: Apenas a seção relevante com limite de caracteres é retornada para o agente.
Gerenciamento de Memória com Sliding Expiration
Como estamos falando de uma aplicação stateless, não tem como saber com precisão o segundo exato em que o comprador fecha a aba do chat na loja.
Para evitar chamadas HTTP repetitivas à API da Nuvemshop a cada mensagem do mesmo comprador, utilizei o IMemoryCache do .NET com Sliding Expiration:
public async Task<string> SearchPolicyAsync(string storeId, string query, CancellationToken ct = default)
{
var cacheKey = $"policies_{storeId}";
var policies = await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
// Renova o cache por mais 15 minutos se houver novas mensagens na loja
entry.SetSlidingExpiration(TimeSpan.FromMinutes(15));
// Garante que o cache expire totalmente após 2 horas de inatividade
entry.SetAbsoluteExpiration(TimeSpan.FromHours(2));
return await _nuvemshopClient.GetPoliciesAsync(storeId, ct);
});
return _searchEngine.Search(policies, query);
}
Essa estratégia garante que:
- Enquanto houver compradores conversando na mesma loja, as políticas permanecem na memória.
- Quando o tráfego da loja zera, o IMemoryCache expira as entradas automaticamente e libera a memória, mantendo o consumo de RAM sob controle na minha humilde VPS.
Resultados Práticos
- Redução de Tokens: As respostas das tools caíram de uma média de ~2.400 tokens (HTML completo) para ~280 tokens (chunk em Markdown relevante), gerando uma economia de mais de 85% em tokens.
- Latência: Redução no tempo de resposta do modelo, que agora processa prompts mais concisos.