11. RAG Conversacional: Adicionando Memória à Recuperação
No Capítulo 10, melhoramos a qualidade de recuperação do nosso sistema RAG. Mas ainda há uma limitação: cada pergunta é tratada individualmente. Quando um usuário pergunta "Qual é a sua política de reembolso?", nosso sistema encontra o conteúdo relevante nos documentos e responde. Quando a próxima pergunta chega, o sistema a responde sem nenhuma memória da conversa anterior.
Vamos ver por que isso é um problema em uma conversa real. Um usuário pergunta "Qual é a sua política de reembolso?" e em seguida acrescenta "Isso também se aplica a produtos digitais?" Esta pergunta de acompanhamento pressupõe o contexto de "política de reembolso" do turno anterior, mas o texto da pergunta em si não contém nenhuma informação desse tipo. Se usarmos "Isso também se aplica a produtos digitais?" diretamente como consulta de busca, o retriever puxará informações irrelevantes relacionadas a "produtos digitais" (por exemplo, preços ou especificações), e o sistema RAG gerará uma resposta que não corresponde à intenção do usuário.
Neste capítulo, aprenderemos como resolver esse problema. Vamos aprender a reescrever perguntas de acompanhamento ambíguas em perguntas completas, usar essa técnica para construir um sistema de RAG conversacional e abordar como gerenciar o histórico de conversa à medida que as conversas ficam mais longas.
11.1) Reescrevendo Perguntas de Acompanhamento em Perguntas Completas
Como vimos na introdução, perguntas de acompanhamento se baseiam no contexto da conversa anterior, então as pessoas tendem a omitir muita informação. Como resultado, uma pergunta de acompanhamento muitas vezes fica incompleta por si só. Como podemos resolver isso?
No Capítulo 8, aprendemos como ajudar um LLM a entender o contexto da conversa passando o histórico de conversa junto com cada mensagem. Podemos aplicar a mesma abordagem aqui. Passamos a pergunta de acompanhamento junto com o histórico de conversa para o LLM, e pedimos que ele a reescreva em uma pergunta completa que reflita o contexto. Por exemplo, a pergunta de acompanhamento "Isso também se aplica a produtos digitais?" é reescrita, junto com o histórico de conversa, em "Produtos digitais são elegíveis para reembolso?" Com essa pergunta reescrita, a busca consegue encontrar os documentos certos sobre políticas de reembolso para produtos digitais.
Essa técnica é chamada de reescrita de consulta (query rewriting). Vamos criar um system prompt para isso.
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
llm = ChatOpenAI(model="gpt-5-mini")
system_prompt = (
"Given a chat history and the latest user question "
"which might reference context in the chat history, "
"formulate a standalone question "
"which can be understood without the chat history. "
"Do NOT answer the question, just reformulate it if needed "
"and otherwise return it as is."
)A instrução central neste system prompt é "reescreva a pergunta de acompanhamento em uma pergunta completa usando o histórico de chat". Duas diretrizes específicas são importantes.
Primeiro, "Do NOT answer the question, just reformulate it." Isso diz ao LLM para apenas reescrever a pergunta, não respondê-la. Sem essa diretriz, o LLM tende a responder a pergunta em vez de reescrevê-la. O que queremos aqui não é uma resposta, mas uma pergunta completa que possa ser entendida sem o histórico de chat.
Segundo, "otherwise return it as is." Isso diz ao LLM para deixar a pergunta inalterada se ela não precisar de reescrita. Sem isso, o LLM pode reformular desnecessariamente a pergunta, potencialmente alterando seu significado ou escopo original.
Agora vamos usar este system prompt para reescrever de fato uma pergunta de acompanhamento.
messages = [
SystemMessage(content=system_prompt),
# Histórico de chat
HumanMessage(content="What is your refund policy?"),
AIMessage(content="All physical products may be returned within 30 days of purchase for a full refund."),
# Pergunta de acompanhamento
HumanMessage(content="Does that apply to digital products too?"),
]
response = llm.invoke(messages)
print(response.content)Saída:
Are digital products eligible for a refund?O LLM leu o histórico de conversa, reconheceu que a pergunta era sobre "política de reembolso" e a reescreveu em uma pergunta completa. Buscar com essa pergunta reescrita retornará documentos que correspondem à intenção do usuário.
Na próxima seção, integraremos essa etapa de reescrita ao pipeline de RAG para que reescrita, recuperação e geração de resposta aconteçam todas em uma única chamada.
11.2) Construindo o RAG Conversacional
Na seção anterior, aprendemos como reescrever perguntas de acompanhamento em perguntas completas passando o histórico de conversa para o LLM. Agora vamos integrar essa etapa de reescrita ao pipeline de RAG para construir um RAG conversacional onde reescrita → recuperação → geração de resposta acontecem todas em uma única chamada.
O LangChain fornece utilitários de cadeia para construir RAG conversacional (create_history_aware_retriever, create_retrieval_chain, etc.), mas essas funções estão no pacote langchain-classic, que chega ao fim do suporte em dezembro de 2026. A documentação oficial do LangChain agora recomenda usar agentes em vez disso.
Portanto, neste capítulo, usaremos agentes para implementar o RAG conversacional. Agentes são abordados em detalhes na Parte V (Capítulos 15–17), então aqui apresentaremos apenas o que é necessário para a nossa implementação de RAG conversacional.
11.2.1) Componentes de Agente que Usaremos Aqui
No Capítulo 5, tivemos uma breve visão do conceito central de agentes. Quando o LLM analisa a solicitação de um usuário e decide qual ferramenta usar, o sistema executa essa decisão. Naquela época, implementamos esse processo manualmente, mas o LangChain fornece APIs que o tornam muito mais simples. Aqui está uma breve introdução aos três componentes que usaremos.
@tool: Um decorator que converte uma função Python comum em uma ferramenta que o agente pode usar. O agente seleciona e chama autonomamente a ferramenta apropriada dentre suas ferramentas registradas com base na solicitação do usuário.
create_agent: Uma função que recebe um LLM, uma lista de ferramentas e um system prompt para criar um agente. Ela lida internamente com o fluxo de decisão-execução do agente.
InMemorySaver: Um checkpointer que gerencia automaticamente o histórico de conversa. Ele organiza as conversas por thread_id, então quando o agente é invocado com o mesmo thread_id, ele carrega automaticamente o histórico de conversa anterior.
11.2.2) Criando a Ferramenta de Recuperação
Primeiro, vamos transformar a busca no vector store que construímos no Capítulo 10 em uma ferramenta que o agente pode usar.
from langchain.tools import tool
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
# Conecta ao vector store construído no Capítulo 10
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")
vector_store = Chroma(
persist_directory="data/chroma_db",
collection_name="company_docs",
embedding_function=embedding_model,
)
@tool
def retrieve_context(query: str):
"""Busca em documentos por conteúdo relevante para a consulta."""
retrieved_docs = vector_store.similarity_search(query, k=3)
serialized = "\n\n".join(
f"Source: {doc.metadata['source']}\nContent: {doc.page_content}"
for doc in retrieved_docs
)
return serializedO decorator @tool converte a função retrieve_context em uma ferramenta que o agente pode usar. O agente decide autonomamente se deve chamar essa ferramenta com base na pergunta do usuário.
11.2.3) Criando o Agente
Passamos a ferramenta de recuperação, um system prompt e um checkpointer para create_agent para criar o agente.
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver # Instalado automaticamente com langchain
agent = create_agent(
model="gpt-5-mini",
tools=[retrieve_context],
system_prompt=(
"You are a helpful assistant that answers questions about company policies. "
"Use the retrieve_context tool to search for relevant information. "
"If the retrieved context does not contain relevant information, "
"say that you don't know. "
"Keep the answer concise, three sentences maximum."
),
checkpointer=InMemorySaver(),
)model: O LLM que o agente usará.tools: A lista de ferramentas disponíveis para o agente. Registramos a ferramenta de recuperação de documentos (retrieve_context) que criamos acima.system_prompt: As instruções de comportamento do agente. Ele diz ao agente para usar a ferramenta de recuperação para responder perguntas sobre políticas da empresa, e para dizer que não sabe quando o contexto recuperado não contém informações relevantes.checkpointer: Gerencia automaticamente o histórico de conversa.InMemorySaver()armazena as conversas na memória, lidando automaticamente com o histórico de conversa que gerenciávamos manualmente no Capítulo 8.
Quando o agente recebe uma pergunta do usuário, ele consulta o histórico de conversa e decide se é necessária uma busca de documentos no vector store. Se for, ele chama a ferramenta retrieve_context para buscar documentos relevantes e gera uma resposta através do LLM. O histórico de conversa é gerenciado automaticamente pelo InMemorySaver.
11.2.4) Executando uma Conversa de Múltiplos Turnos
Vamos executar uma conversa real de dois turnos para verificar se ela lida corretamente com perguntas de acompanhamento.
# thread_id é um identificador que distingue conversas
# Usar o mesmo thread_id continua a mesma conversa
thread_config = {"configurable": {"thread_id": "1"}}
# --- Turno 1: Uma pergunta completa ---
response1 = agent.invoke(
{"messages": [{"role": "user", "content": "What is your refund policy?"}]},
thread_config,
)
print("Q: What is your refund policy?")
print("A:", response1["messages"][-1].content)
# --- Turno 2: Um acompanhamento que depende do Turno 1 ---
response2 = agent.invoke(
{"messages": [{"role": "user", "content": "Does that apply to digital products too?"}]},
thread_config,
)
print("\nQ: Does that apply to digital products too?")
print("A:", response2["messages"][-1].content)Saída:
Q: What is your refund policy?
A: All physical products may be returned within 30 days of purchase for a full refund.
The original receipt or order confirmation email is required, and items must be in
their original packaging and unused condition.
After 30 days, returns are accepted for store credit only.
Q: Does that apply to digital products too?
A: Digital products (software licenses, e-books, online courses) are non-refundable
once the download or access link has been activated.
However, if you experience technical issues preventing access, you can contact support
within 7 days for a replacement or refund.No segundo turno, passamos "Isso também se aplica a produtos digitais?" mas o agente reconheceu, a partir do histórico de conversa, que isso era um acompanhamento sobre a política de reembolso, e recuperou com precisão a seção de produtos digitais dos documentos da política de reembolso.
Espere — para este agente, não adicionamos nenhuma etapa de reescrita de consulta como a da Seção 11.1. Então, como a pergunta de acompanhamento foi processada corretamente? Quando o LLM chama uma ferramenta (uma função decorada com
@tool), ele gera os argumentos da ferramenta por conta própria. Isso inclui a consulta do usuário passada aoretrieve_context— como o LLM tem todo o histórico da conversa à sua frente, ele reescreveu a pergunta de acompanhamento em uma pergunta completa e autônoma antes de fazer a chamada. Não configuramos nenhuma etapa de reescrita dedicada, mas mesmo assim a reescrita de consulta aconteceu como parte do processo de chamada de ferramenta.Observe também que não precisamos gerenciar o histórico de conversa manualmente — o
InMemorySavero gerencia automaticamente porthread_id.
A próxima seção aborda o problema que surge à medida que as conversas ficam mais longas e o histórico cresce, junto com como resolvê-lo.
11.3) Gerenciando Conversas Mais Longas
O RAG conversacional que construímos funciona bem no início, mas problemas podem surgir à medida que as conversas ficam mais longas. Como aprendemos no Capítulo 8, os LLMs têm um tamanho máximo de entrada que podem processar em uma única chamada. O system prompt, o histórico de conversa, os documentos recuperados e a pergunta do usuário precisam todos caber dentro desse limite.
À medida que as conversas ficam mais longas, o histórico de conversa ocupa mais tokens, eventualmente excedendo o tamanho máximo de entrada e fazendo com que as chamadas de API falhem. Os custos também aumentam a cada chamada, já que você é cobrado por token. Isso significa que precisamos gerenciar o tamanho do nosso histórico de conversa.
No Capítulo 8, resolvemos esse problema com uma janela deslizante (sliding window): mantendo apenas as N mensagens mais recentes e descartando as mais antigas. O mesmo conceito se aplica em um ambiente de agente. create_agent suporta middleware, que é uma etapa de processamento que pode modificar as mensagens antes de o LLM ser chamado. Podemos usar middleware para cortar o histórico antigo.
11.3.1) Limitando o Histórico com Middleware
O decorator @before_model funciona de forma semelhante ao decorator @tool que vimos na Seção 11.2. Assim como @tool converte uma função em uma ferramenta que o agente pode usar, @before_model converte uma função em um middleware que é executado antes de cada chamada ao LLM. O middleware convertido é ativado registrando-o no parâmetro middleware de create_agent.
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import before_model
from langchain.messages import RemoveMessage
from langgraph.graph.message import REMOVE_ALL_MESSAGES
@before_model
def trim_old_messages(state: AgentState, runtime) -> dict | None:
"""Remove mensagens antigas antes de cada chamada ao LLM."""
messages = state["messages"]
# Se houver poucas mensagens, não faz nada
if len(messages) <= 10:
return None
# Mantém apenas a mensagem de sistema (primeira) e as 10 mensagens mais recentes
return {
"messages": [
RemoveMessage(id=REMOVE_ALL_MESSAGES),
messages[0], # Mensagem de sistema
*messages[-10:], # Últimas 10 mensagens (5 turnos)
]
}AgentState é um objeto que mantém os dados de estado do agente, com state["messages"] contendo a lista de mensagens da conversa até o momento. O valor de retorno do middleware determina como essa lista de conversa é modificada.
- Retornar
Nonedeixa os dados de estado existentes do agente inalterados. - Retornar um dicionário aplica seu conteúdo à lista de mensagens existente. No código acima,
RemoveMessage(id=REMOVE_ALL_MESSAGES)primeiro deleta todas as mensagens existentes, depois adiciona de volta apenas a mensagem de sistema e as 10 mensagens mais recentes. Como resultado, apenas essas mensagens são passadas para o LLM.
Registre este middleware com o agente:
agent = create_agent(
model="gpt-5-mini",
tools=[retrieve_context],
system_prompt=(
"You are a helpful assistant that answers questions about company policies. "
"Use the retrieve_context tool to search for relevant information. "
"If the retrieved context does not contain relevant information, "
"say that you don't know. "
"Keep the answer concise, three sentences maximum."
),
checkpointer=InMemorySaver(),
middleware=[trim_old_messages], # Registra o middleware
)Este é o mesmo agente da Seção 11.2 com middleware=[trim_old_messages] adicionado. Agora, não importa quão longa a conversa fique, apenas as mensagens recentes são passadas para o LLM.
11.3.2) O Trade-off da Janela Deslizante
Quando as mensagens antigas são cortadas, o agente não pode mais referenciar seu conteúdo. Se um usuário mencionar algo que perguntou dez turnos atrás, o agente não tem como conhecer esse contexto. Esta é uma limitação fundamental da abordagem de janela deslizante.
Quando o conteúdo de conversas mais antigas precisa ser preservado, uma alternativa é substituir as mensagens antigas por um resumo gerado pelo LLM, em vez de deletá-las. O LangChain fornece o SummarizationMiddleware para esse propósito, que abordaremos na Parte V (a partir do Capítulo 15) quando mergulharmos nas arquiteturas de agente e grafo.