A pergunta final sobre pergunta_pdf = "Qual foi a receita com laptops?" não veio muito satisfatório, eu sei que o melhor seria enriquecer um prompt para um LLM inferir melhor, porém resolvi experimentar alterações em parametros, tamnanho de chunk e overlap, e explorar como estava a busca de K e fazendo um rerank. Também tive de alterar algum libs que estão defasadas. O resultado legal ficou com o UnstructuredPDFLoader, o docling foi mais lento em chunks muito maiores.
from langchain_community.document_loaders import UnstructuredPDFLoader
loader = UnstructuredPDFLoader("relatorio_vendas.pdf", mode='elements') # mode define o nível de granularidade com que um doc é dividido ao ser carregado para a memória
docs_unstructured = loader.load()
print(f"Total de elementos extraídos: {len(docs_unstructured)}\n")
for doc in docs_unstructured:
print(f"---- TIPO DE ELEMENTO: {doc.metadata.get('category')} ---")
print(doc.page_content)
print("\n")
print(set(doc.metadata.get("category") for doc in docs_unstructured)) # Mostrar quais categorias unicas existem no pdf
from langchain_core.documents import Document
# Lista para armazenar os documentos com metadados
docs_com_metados = []
for doc in docs_unstructured:
novos_metadados = doc.metadata.copy()
novos_metadados['source'] = 'relatorio_vendas.pdf'
novos_metadados['ingestion_date'] = datetime.now().strftime('%Y-%m-%d')
novos_metadados['data_owner'] = 'Departamento de Vendas'
docs_com_metados.append(
Document(page_content=doc.page_content, metadata=novos_metadados)
)
print(f"Total de documentos com metadados: {len(docs_com_metados)}\n")
print(docs_com_metados[-1])
import json
ultimo_doc = docs_com_metados[-1]
print(f"Total de documentos com metadados: {len(docs_com_metados)}\n")
# Converte o objeto Document para Dicionário
doc_dict = {
"page_content": ultimo_doc.page_content,
"metadata": ultimo_doc.metadata
}
# Exibe formatado com identação
print(json.dumps(doc_dict, indent=2, ensure_ascii=False))
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size = 1200,
chunk_overlap = 200,
)
chunks = text_splitter.split_documents(docs_com_metados)
print(f"Numero de documentos original: {len(docs_com_metados)}")
print(f"Numero de chunks gerados: {len(chunks)}\n")
print(chunks[2])
# Faça a busca pedindo mais documentos (ex: k=5)
resultados = vector_store.similarity_search("Qual foi a receita com laptops?", k=10)
for i, doc in enumerate(resultados, 1):
print(f"=== CHUNK {i} (Categoria: {doc.metadata.get('category')}) ===")
# Exibe a string EXATA que o embedding leu
print(repr(doc.page_content))
print("\n" + "="*40 + "\n")
from flashrank import Ranker, RerankRequest
# 1. Carrega o modelo super leve do FlashRank (baixa rápido no Colab)
ranker = Ranker()
pergunta_pdf = "Qual foi a receita com laptops?"
# 2. Busca os TOP 10 no ChromaDB (a tabela estará lá na posição 9 como você viu!)
resultados_brutos = vector_store.similarity_search(pergunta_pdf, k=10)
# 3. Formata os chunks para o Flashrank processar
passagens = [
{"id": idx, "text": doc.page_content, "meta": doc.metadata}
for idx, doc in enumerate(resultados_brutos)
]
# 4. Rerank: O FlashRank avalia a pergunta x conteúdo e reorganiza a lista
requisicao = RerankRequest(query=pergunta_pdf, passages=passagens)
resultados_reordenados = ranker.rerank(requisicao)
# 5. Exibe os 3 primeiros após o Re-Ranking
print(f"Pergunta: '{pergunta_pdf}'\n")
print("=== RESULTADOS APÓS RE-RANKING ===\n")
for i, item in enumerate(resultados_reordenados[:3], 1):
score = item.get('score', 0)
texto = item.get('text')
categoria = item.get('meta', {}).get('category', 'N/A')
print(f"--- [POSIÇÃO {i}] (Score de Relevância: {score:.4f}) | Categoria: {categoria} ---")
print(f"{texto}")
print("-" * 60)
acredito que fazendo o rerank para este caso embora tenha mais tokens ajudaria o llm no prompt final ao agregar o contexto dar uma resposta mais assertiva. Também observo que existem outras formas de fazer a leitura do PDF com outras libs que convertem em markdown, ou como estamos usando gemini, fazer isso de um prompt com gemini vision, bom mas daí gera mais custos.