Hoje em dia, o RAG às vezes tem má fama, seja porque agora as pessoas acham que ele é totalmente trivial (é fácil para começar, mas nem tanto para escalar), seja porque acreditam que foi superado por "sistemas com agentes de IA" (que, em muitos casos, quando examinados mais de perto, rapidamente começam a se parecer muito com RAG...).
Este artigo apresenta alguns exemplos práticos para mostrar como lidamos com desafios comuns, como:
Como lidar com dados que combinam texto e números e por que eles prejudicam abordagens ingênuas de RAG: há colisões entre palavras-chave, e números não carregam significado semântico.
Por que criar embeddings a partir de resumos ajuda: gere um breve resumo descritivo para cada trecho e use esse resumo para gerar embeddings e fazer consultas.
Como gerar resumos contextuais: inclua o contexto do documento principal para diferenciar estatísticas com formatos semelhantes.
Quando recorrer a código e modelos Pydantic: quando o conteúdo literal for importante, combine código personalizado e/ou um modelo Pydantic com chamadas a LLMs para garantir a confiabilidade.
O básico
Sistemas RAG viabilizam desde bots de suporte até assistentes internos de conhecimento.
Nos bastidores, normalmente você:
Divide os documentos de origem em trechos
Converte cada trecho em um embedding em um espaço vetorial
Recupera os K trechos mais relevantes no momento da consulta
Gera uma resposta condicionada a esses trechos
Ferramentas populares — LangChain, LlamaIndex e Filestore da OpenAI — tornam essas etapas quase triviais. Mas, em pipelines reais, você encontrará dados que não são apenas texto denso, e abordagens básicas de RAG podem ter dificuldades. Nas próximas seções, mostraremos exemplos concretos de desafios relacionados aos dados e desenvolveremos a solução gradualmente à medida que a complexidade aumentar.
Quando seus dados não são apenas texto (algo bastante comum)
Considere o seguinte trecho de dados no contexto de um jogo:
JSON
Os embeddings funcionam graças às relações aprendidas entre palavras por meio do significado semântico e da gramática. Nos dados acima, temos uma combinação de texto e números. Fora desse contexto específico, os números não têm relação com as palavras. Portanto, podemos dizer que esse trecho de dados é basicamente uma combinação de palavras relativamente descritivas seguidas de alguns números aleatórios.
Isso não seria um problema se esse fosse o único tipo de dado disponível, pois ainda poderíamos fazer a recuperação usando os embeddings das poucas palavras descritivas existentes (ou simplesmente usar text-to-SQL). Mas e se esse trecho estiver perdido entre muitos outros trechos repletos de texto, nos quais essas palavras também aparecem? Por exemplo:
JSON
Agora imagine que queremos recuperar "What is the attack range with Draconic Ascension?"É bem provável que não consigamos recuperar o trecho relevante, pois ele estará perdido no ruído de outros trechos que contêm as mesmas palavras-chave.
O problema fundamental é que não conseguimos diferenciar bem esses trechos de dados, embora eles contenham tipos distintos de informação sobre o mesmo assunto. Será que podemos enriquecer ou aprimorar esses dados de alguma forma? Claro que sim :smile:
Enriqueça seus dados resumindo-os — sim, é isso mesmo
Em vez de gerar diretamente o embedding do próprio trecho, primeiro podemos criar um resumo que descreva o conteúdo dos dados e depois usar esse resumo para gerar o embedding e fazer a recuperação. Na etapa de geração, ainda usaríamos os dados originais vinculados ao resumo.
Assim, para os dois exemplos de trechos acima, geraríamos resumos como:
Estatísticas de alcance, velocidade e dano do ataque (padrão e com Draconic Ascension).
Descrição e detalhes de Draconic Ascension, incluindo condições de ativação, efeitos visuais e história.
Depois, também aprimoramos a consulta para que ela fique "alinhada" ao resumo. Por exemplo, transformaríamos "What is the attack range with Draconic Ascension?"em "What is the statistics of attack range with Draconic Ascension?"Isso é especialmente importante quando a consulta de recuperação vem de usuários que não são da área técnica e perguntam em uma linguagem ~~"livre"~~ humana e normal. Afinal, eles não sabem — nem precisam saber — como um RAG funciona para maximizar precisão e revocação.


Não tire as coisas do contexto (um conselho que vale para a vida)
Agora, considere outro cenário: lidar com inúmeros trechos de dados muito parecidos, como os mostrados abaixo:
Plain Text
Mantendo a mesma abordagem, imagine perguntar: "What is character X's attack range?"Com os resumos que acabamos de gerar, estaríamos fazendo uma aposta baseada na sorte, pois eles também seriam muito parecidos. Então, como poderíamos diferenciá-los?
A resposta é simples: fornecendo contexto. Poderíamos simplesmente incluir no trecho uma referência ao documento principal, como {"character": "X"}, neste caso. Assim, conseguiríamos recuperar com precisão os dados corretos do personagem X, mesmo tendo os mesmos dados para os personagens Y e Z.
No entanto, uma abordagem melhor e mais generalizável seria gerar um resumo contextual do trecho. Ou seja, em vez de gerar um resumo apenas do próprio trecho de dados, poderíamos fornecer tanto o documento principal quanto o trecho para gerar um resumo contextual geral. Nele, explicaríamos como o trecho se encaixa no documento principal, por exemplo:
Este trecho apresenta estatísticas detalhadas de... para o personagem X. O trecho se encaixa no documento completo ao mostrar a força de X na velocidade de ataque...
Este trecho apresenta estatísticas detalhadas de... para o personagem Y. O trecho se encaixa no documento completo ao mostrar os atributos aprimorados de Y quando ela usa sua habilidade especial...
Este trecho apresenta estatísticas detalhadas de... para o personagem Z. O trecho se encaixa no documento completo ao mostrar que os atributos de Z são adequados para atuar como tanque em partidas em equipe...
Esse método (parcialmente inspirado pela Anthropic) pode parecer um exagero para o exemplo acima, mas é muito eficaz para trechos que poderiam ser interpretados incorretamente "fora de contexto". Além disso, oferece uma abordagem unificada que funciona para todos os trechos e mantém o pipeline de engenharia organizado.


Quando é preciso ser ~~obcecado por controle~~ rigoroso
Normalmente, recebemos os dados completos e os dividimos em trechos para um sistema RAG. Neste exemplo, mostramos algo um pouco diferente: dados divididos em trechos inadequados. São seções aleatórias de um trecho lógico que, na verdade, precisam ser reunidas. Um trecho lógico é um bloco de conteúdo que naturalmente deveria permanecer unido, como uma subseção de um documento ou um parágrafo coerente.


Nossa primeira tentativa com esses dados foi enviar tudo em uma chamada a um LLM, pedir que agrupasse os trechos como considerasse adequado e devolvesse o conteúdo agrupado. Um LLM deveria fazer isso muito bem, certo? Bem, sim e não.
Também descobrimos em várias outras ocasiões que os LLMs tendem a agir de forma preguiçosa e não são confiáveis quando precisamos do conteúdo completo e exato, especialmente com contextos longos. O que faz todo o sentido. Mas isso inviabilizava este caso de uso específico, pois precisávamos do conteúdo exato, palavra por palavra — sem resumos e sem omitir nenhuma parte do original. Não podíamos perder nenhum detalhe.
E, claro, a parte do "sim" era que o LLM compreendia muito bem a semântica e as estruturas dos trechos fragmentados. Desde que não se recusasse a reproduzir o conteúdo exato. Droga :/
Como poderíamos aproveitar aquilo que um LLM faz bem e evitar aquilo em que ele não é confiável? Recorremos ao nosso bom e velho amigo, o código (leia-se: uma função personalizada em Python). E a um modelo Pydantic extremamente simples. Esta é a solução:
Percorra as seções mantendo um trecho lógico atual
Em cada seção, pergunte ao LLM: esta seção pertence ao trecho lógico atual? Responda sim ou não (seguindo o modelo Pydantic).
Se sim, anexe a seção ao trecho; se não, finalize o trecho lógico atual, pois ele está completo, e inicie outro com essa seção.


É claro que usamos um pouco mais de tokens do que usaríamos em uma única passagem por todo o conteúdo. Porém, neste caso de uso específico, em que preservar o conteúdo exato era a principal prioridade, o pequeno custo adicional valeu muito a pena.
É uma solução muito simples, mas segue um princípio importante: quando precisamos de rigor, não convém depender apenas de LLMs, pois, afinal, eles são probabilísticos.
Código e funções personalizados, além de modelos Pydantic, podem produzir resultados previsíveis e confiáveis sem deixar de aproveitar os recursos dos LLMs.
Criar uma solução de IA generativa é tanto um desafio de engenharia quanto de IA. Esperamos que estes exemplos tenham inspirado você a enfrentar seus próprios desafios. Para saber mais sobre soluções de IA generativa com foco em engenharia, confira nosso artigo sobre design de sistemas com agentes de IA baseado em roteadores.