RAG na prática: como um agente LangChain busca informação e responde de forma estruturada
Paulo Roberto Bolsanello·03 Set 2026·10 min de leitura
Um assistente de vendas que nunca viu o seu catálogo de produtos não serve para muita coisa. Ele pode até escrever bem, mas não sabe o preço do notebook X nem se há monitor 4K em estoque. É esse problema que o RAG (Retrieval-Augmented Generation, ou geração aumentada por recuperação) resolve: ele dá ao modelo acesso aos seus dados antes de gerar uma resposta.
Neste artigo vamos construir, passo a passo, um assistente de vendas com LangChain que busca produtos em um catálogo CSV e responde perguntas de clientes. No final, vamos evoluir esse exemplo para retornar respostas estruturadas, em formato de dados, em vez de texto livre.
O problema que o RAG resolve
Modelos de linguagem são treinados com um recorte de dados até uma certa data. Eles não sabem, e não têm como saber, quais produtos existem no seu catálogo, qual o preço atual ou o que está disponível em estoque. Também seria inviável colar o catálogo inteiro dentro do prompt a cada pergunta: o texto ficaria grande demais e caro de processar.
O RAG contorna esse problema em duas etapas. Primeiro, transforma os documentos em vetores numéricos, chamados de embeddings, e guarda esses vetores em um banco de dados vetorial. Depois, quando chega uma pergunta, busca nesse banco os trechos mais relevantes e os entrega ao modelo junto com a pergunta. O modelo responde com base nesse contexto, não apenas com o que aprendeu durante o treinamento.
Estrutura do projeto
Antes de escrever qualquer código, vale organizar a pasta do projeto. O exemplo usado neste artigo tem poucos arquivos:
langchain/
├── .venv/
├── .env
├── main.py
└── products.csv
main.py: o script com todo o código apresentado neste artigo.
products.csv: o catálogo de produtos usado como base de conhecimento do RAG.
.env: arquivo com variáveis de ambiente, como a chave da API da OpenAI. Não deve ser versionado no Git.
O .venv merece atenção à parte. Ele é um ambiente Python isolado, criado dentro da própria pasta do projeto, onde ficam instaladas as dependências específicas deste código (LangChain, FAISS, e as demais bibliotecas). Sem ele, tudo seria instalado direto no Python do sistema, o que cedo ou tarde gera conflito: um projeto pode precisar de uma versão do LangChain e outro projeto, de uma versão diferente, e as duas não convivem bem instaladas globalmente.
Para criar e ativar o ambiente virtual:
# Criar o ambiente virtual
python -m venv .venv
# Ativar no Linux ou macOS
source .venv/bin/activate
# Ativar no Windows
.venv\Scripts\activate
Com o ambiente ativado, o terminal costuma mostrar (.venv) no início da linha. É nesse estado que as próximas instalações via pip devem ser feitas, para que as dependências fiquem isoladas nesse projeto e não se espalhem pelo restante da máquina.
Instalando as dependências
Com o ambiente virtual ativado, o próximo passo é instalar o LangChain e os pacotes de integração usados no exemplo. O LangChain é modular: o pacote principal não vem com suporte a OpenAI, FAISS ou carregadores de documentos, cada um desses recursos é uma dependência separada.
langchain: núcleo da biblioteca, inclui o create_agent e os componentes de orquestração.
langchain-community: integrações mantidas pela comunidade, como o CSVLoader usado para ler o catálogo.
langchain-openai: integração com os modelos e embeddings da OpenAI.
langchain-text-splitters: os splitters de texto, como o RecursiveCharacterTextSplitter.
faiss-cpu: o banco de dados vetorial usado para indexar e buscar os embeddings. Se o ambiente tiver GPU disponível, faiss-gpu é uma alternativa mais rápida.
python-dotenv: carrega variáveis de ambiente (como a chave da API da OpenAI) a partir de um arquivo .env.
pydantic: define o schema usado nas respostas estruturadas.
Também é necessário criar o arquivo .env na raiz do projeto com a chave da API:
OPENAI_API_KEY=sua-chave-aqui
Com a pasta organizada, o ambiente virtual ativado e as dependências instaladas, o projeto está pronto para receber o código.
Carregando e dividindo os dados
O primeiro passo no código é ler a fonte de dados. No exemplo, um catálogo de produtos em CSV:
Cada linha do CSV vira um documento. Se o catálogo tiver descrições longas, cada documento pode ficar grande demais para ser um bom "pedaço" de busca, então ele é dividido em partes menores:
O chunk_overlap garante que um pedaço de texto não corte uma informação importante bem no meio, repetindo um trecho do fim de um chunk no início do próximo. Para um catálogo com descrições curtas, como no exemplo, provavelmente cada produto já cabe em um único chunk, mas a divisão continua sendo uma boa prática para catálogos maiores ou descrições mais extensas.
Transformando texto em vetores
Com os documentos divididos, cada chunk é convertido em um vetor de embeddings e armazenado em um índice vetorial:
O FAISS é a biblioteca que guarda esses vetores e permite buscar, entre milhares de itens, quais são os mais parecidos com uma consulta. O k=3 define que a busca vai retornar sempre os três produtos mais relevantes para cada pergunta.
Essa é a parte central do RAG: a busca não é por palavras-chave, é por similaridade de significado. Uma busca por "notebook para programar" pode encontrar um produto descrito como "laptop ideal para desenvolvedores", mesmo sem nenhuma palavra em comum entre a pergunta e a descrição.
Dando ao agente uma ferramenta de busca
O agente não acessa o índice vetorial diretamente. Ele usa uma ferramenta (tool), uma função Python decorada e descrita para que o modelo saiba quando e como chamá-la:
@tool
def search_products(query: str) -> str:
"""Busca produtos semanticamente semelhantes à consulta informada."""
docs = retriever.invoke(query)
if not docs:
return "Nenhum produto encontrado."
result = [doc.page_content for doc in docs]
return "\n\n------\n\n".join(result)
O docstring da função não é um detalhe cosmético: é o texto que o modelo lê para decidir se e quando deve chamar essa ferramenta. Uma descrição vaga leva a chamadas erradas ou a chamadas que nunca acontecem.
Montando o agente
Por fim, o agente é criado com um modelo, a lista de ferramentas disponíveis e um prompt de sistema que define seu papel:
Quando o agente recebe uma pergunta, ele decide sozinho se precisa consultar o catálogo, chama a ferramenta search_products quando necessário, e usa o resultado para montar a resposta final. Esse ciclo de decidir, buscar e responder é o que diferencia um agente de uma simples chamada direta ao modelo.
O problema do texto livre
O código até aqui funciona bem para um chat. Mas e se essa resposta precisar alimentar outro sistema? Um painel que lista os produtos recomendados, um CRM que registra quais itens foram sugeridos ao cliente, ou uma API que devolve os dados para um aplicativo?
Nesses casos, texto livre é um problema. A resposta do agente pode vir como um parágrafo corrido, uma lista, ou qualquer outra formatação que o modelo escolher naquele momento. Extrair dados confiáveis desse texto exigiria parsing manual, expressões regulares ou outro modelo só para interpretar a resposta do primeiro. É frágil e desnecessário.
Respostas estruturadas com Pydantic
O LangChain resolve isso com o parâmetro response_format do create_agent. Em vez de receber só texto, o agente valida e retorna a resposta em um formato definido por você, usando um modelo Pydantic:
from typing import List, Optional
from pydantic import BaseModel, Field
class ProductAnswer(BaseModel):
"""Resposta estruturada do assistente de vendas."""
answer: str = Field(description="Resposta direta e objetiva para o cliente")
products_mentioned: List[str] = Field(description="Nomes dos produtos citados na resposta")
notes: Optional[str] = Field(default=None, description="Observações adicionais, como disponibilidade ou alternativas")
Cada campo tem uma descrição, e essa descrição também importa: é o que orienta o modelo sobre o que colocar em cada campo. A mudança no agente é de uma linha:
E a resposta passa a vir pronta para uso, dentro da chave structured_response:
for question in questions:
response = agent.invoke({"messages": [{"role": "user", "content": question}]})
structured = response["structured_response"]
print(f"Pergunta: {question}")
print(f"Resposta: {structured.answer}")
print(f"Produtos citados: {', '.join(structured.products_mentioned)}")
if structured.notes:
print(f"Observações: {structured.notes}")
print()
Não há mais a necessidade de interpretar texto. structured.answer é sempre uma string, structured.products_mentioned é sempre uma lista, e se o modelo tentar devolver algo fora do schema, o LangChain sinaliza o erro em vez de deixar passar um dado inválido.
O que muda na prática
O pipeline de busca continua exatamente o mesmo: carregar dados, dividir em chunks, gerar embeddings, indexar no FAISS e disponibilizar como ferramenta. O que muda é o contrato de saída do agente. Antes, a resposta era para ser lida por uma pessoa. Depois, ela também pode ser lida por código, com garantia de formato.
Essa diferença é o que separa um protótipo de chat de um sistema que outras partes de uma aplicação conseguem consumir com confiança.
Código completo
Juntando a estrutura do projeto, o pipeline de RAG e as respostas estruturadas, o código final do main.py fica assim:
from pprint import pprint
from typing import List, Optional
from dotenv import load_dotenv
from pydantic import BaseModel, Field
from langchain_community.document_loaders import CSVLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain.tools import tool
from langchain_community.vectorstores import FAISS
from langchain.agents import create_agent
load_dotenv()
# Carregamento dos dados
loader = CSVLoader(file_path="products.csv", encoding="utf-8", csv_args={"delimiter": ";"})
documents = loader.load()
pprint(f"Total de documentos carregados: {len(documents)}")
print(f"Primeiro documento: {documents[0].page_content}")
# Divisão em chunks
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(documents)
print(f"Total de chunks gerados: {len(chunks)}")
# Embeddings e indexação vetorial
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = FAISS.from_documents(chunks, embeddings)
# Retriever
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
@tool
def search_products(query: str) -> str:
"""Busca produtos semanticamente semelhantes à consulta informada.
Args:
query: Texto usado para localizar produtos no índice vetorial.
Returns:
Os produtos encontrados, separados por linhas divisórias, ou uma
mensagem indicando que nenhum produto foi encontrado.
"""
docs = retriever.invoke(query)
if not docs:
return "Nenhum produto encontrado."
result = [doc.page_content for doc in docs]
return "\n\n------\n\n".join(result)
system_prompt = (
"Você é um assistente de vendas especializado em produtos de tecnologia. "
"Seu objetivo é ajudar os clientes a encontrar produtos que atendam às suas necessidades. "
"Use a ferramenta 'search_products' para buscar produtos no índice vetorial. "
"Forneça respostas claras e concisas, destacando os recursos e benefícios dos produtos encontrados."
)
# Schema de resposta estruturada
class ProductAnswer(BaseModel):
"""Resposta estruturada do assistente de vendas."""
answer: str = Field(description="Resposta direta e objetiva para o cliente")
products_mentioned: List[str] = Field(description="Nomes dos produtos citados na resposta")
notes: Optional[str] = Field(default=None, description="Observações adicionais, como disponibilidade ou alternativas")
agent = create_agent(
model="gpt-4.1-mini",
tools=[search_products],
system_prompt=system_prompt,
response_format=ProductAnswer,
)
questions = [
"Estou procurando um laptop com boa duração de bateria e desempenho para programação.",
"Quais são os melhores fones de ouvido com cancelamento de ruído disponíveis?",
"Preciso de um monitor 4K para edição de vídeo. Alguma recomendação?",
"Preciso de um setup gamer, tem algo para recomendar?",
"Qual o produto mais barato e o mais caro do catálogo?",
]
for question in questions:
response = agent.invoke({"messages": [{"role": "user", "content": question}]})
structured = response["structured_response"]
print(f"Pergunta: {question}")
print(f"Resposta: {structured.answer}")
print(f"Produtos citados: {', '.join(structured.products_mentioned)}")
if structured.notes:
print(f"Observações: {structured.notes}")
print()
À medida que um sistema cresce, a lógica de negócio costuma se tornar mais complexa. Um dia você está apenas filtrando dados. No outro, está classificando, ordenando, eliminando, aplicando exceções e resolvendo empates.…
Neste post, você aprenderá a fazer o deploy de uma aplicação Laravel em uma VPS rodando Ubuntu com o servidor web Nginx e configurar HTTPS utilizando o Certbot. Além disso, será abordado como configurar um banco de…