Procesamiento de Documentos
El Desafío de la Ingestión
Antes de poder embeber y buscar documentos, necesitas extraer texto limpio de ellos. Esto suena simple pero es una de las partes más subestimadas de construir un sistema RAG. Los documentos del mundo real vienen en formatos desordenados: PDFs multi-columna con encabezados y pies de página, páginas HTML saturadas con navegación y anuncios, imágenes escaneadas incrustadas en documentos Word, archivos de código con anidamiento complejo.
La calidad de tu texto extraído determina directamente la calidad de tus embeddings y, en última instancia, la calidad de tus respuestas. Basura entra, basura sale -- esto aplica en ningún lugar con más fuerza que en RAG.
Procesamiento de PDFs
Los PDFs son el formato de documento más común en sistemas RAG empresariales, y también son los más difíciles de procesar bien. Un PDF es fundamentalmente un formato visual -- describe dónde dibujar caracteres en una página, no la estructura lógica del texto.
PyPDF (Simple y Rápido)
PyPDF es una biblioteca pura de Python que extrae texto de la mayoría de los PDFs estándar. Funciona bien para PDFs nativos de texto (documentos creados digitalmente) pero tiene problemas con documentos escaneados y diseños complejos.
from langchain_community.document_loaders import PyPDFLoader
# Cargar un PDF y dividir por paginas
loader = PyPDFLoader("manual_empresa.pdf")
pages = loader.load()
for page in pages:
print(f"Pagina {page.metadata['page']}: {len(page.page_content)} caracteres")
print(page.page_content[:200])
print("---")
Unstructured (Robusto y Consciente del Diseño)
La biblioteca unstructured maneja documentos complejos entendiendo elementos de diseño como títulos, encabezados, tablas y listas. Puede procesar PDFs, DOCX, HTML, imágenes (vía OCR) y más.
from langchain_community.document_loaders import UnstructuredPDFLoader
# El modo de alta resolucion detecta elementos de diseno
loader = UnstructuredPDFLoader(
"informe_anual.pdf",
mode="elements", # Dividir en elementos (titulos, texto, tablas)
strategy="hi_res" # Usar modelo de deteccion de diseno
)
elements = loader.load()
for elem in elements[:5]:
print(f"Tipo: {elem.metadata.get('category', 'desconocido')}")
print(f"Texto: {elem.page_content[:100]}")
print("---")
Manejo de PDFs Escaneados
Los PDFs escaneados contienen imágenes, no texto. Necesitas OCR (Reconocimiento Óptico de Caracteres) para extraer texto. La biblioteca unstructured se integra con Tesseract OCR, y opciones más avanzadas incluyen Azure Document Intelligence y AWS Textract.
# Usando unstructured con OCR para documentos escaneados
from langchain_community.document_loaders import UnstructuredPDFLoader
loader = UnstructuredPDFLoader(
"contrato_escaneado.pdf",
strategy="ocr_only" # Forzar procesamiento OCR
)
docs = loader.load()
Consejo: Para sistemas en producción que procesan muchos PDFs, considera Azure Document Intelligence o Amazon Textract. Manejan tablas, formularios y escritura a mano significativamente mejor que el OCR de código abierto.
Procesamiento de HTML
Las páginas web son una fuente común para sistemas RAG -- sitios de documentación, bases de conocimiento y publicaciones de blog viven en la web. El desafío es separar el contenido real de la navegación, barras laterales, pies de página y anuncios.
Enfoque con BeautifulSoup
from langchain_community.document_loaders import WebBaseLoader
import bs4
# Cargar una pagina web y extraer solo contenido del articulo
loader = WebBaseLoader(
web_paths=["https://docs.example.com/guia"],
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(
class_=("post-content", "article-body", "main-content")
)
),
)
docs = loader.load()
# Limpiar el texto extraido
for doc in docs:
# Eliminar espacios en blanco excesivos
doc.page_content = " ".join(doc.page_content.split())
print(doc.page_content[:300])
Procesamiento de Múltiples Páginas
Para sitios de documentación con muchas páginas, usa un sitemap o rastreador recursivo:
from langchain_community.document_loaders import SitemapLoader
# Cargar todas las paginas desde un sitemap
loader = SitemapLoader(
"https://docs.example.com/sitemap.xml",
filter_urls=["https://docs.example.com/guia/"], # Solo paginas de la guia
)
docs = loader.load()
print(f"Cargadas {len(docs)} paginas")
Procesamiento de Markdown
Los archivos Markdown son el formato más fácil de procesar porque ya son texto plano con estructura ligera. Son comunes en repositorios de documentación, wikis y bases de conocimiento.
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from pathlib import Path
# Cargar todos los archivos Markdown de un directorio
loader = DirectoryLoader(
"./docs",
glob="**/*.md",
loader_cls=TextLoader,
loader_kwargs={"encoding": "utf-8"}
)
docs = loader.load()
# Cada documento incluye la ruta de origen en metadatos
for doc in docs:
print(f"Origen: {doc.metadata['source']}")
print(f"Longitud: {len(doc.page_content)} caracteres")
Procesamiento de Archivos de Código
Los archivos de código necesitan manejo especial porque su estructura porta significado. La indentación, los límites de funciones y las declaraciones de importación son importantes para entender el código.
from langchain_community.document_loaders import DirectoryLoader, TextLoader
# Cargar archivos Python de un proyecto
loader = DirectoryLoader(
"./src",
glob="**/*.py",
loader_cls=TextLoader,
loader_kwargs={"encoding": "utf-8"}
)
code_docs = loader.load()
# Agregar metadatos de lenguaje
for doc in code_docs:
doc.metadata["language"] = "python"
doc.metadata["file_type"] = "code"
Cubriremos el procesamiento de código en profundidad en la Lección 9.
Extracción de Metadatos
Buenos metadatos hacen la recuperación dramáticamente mejor. Permiten filtrado (buscar solo documentos de marketing), proporcionan contexto al LLM (esta información proviene del informe anual 2024) y mejoran la citación de fuentes.
Campos de Metadatos Esenciales
Cada documento en tu sistema RAG debería llevar como mínimo:
- source -- de dónde proviene el documento (ruta de archivo, URL, ID de base de datos)
- title -- el título del documento o sección
- date -- cuándo se creó o actualizó el documento por última vez
- content_type -- qué tipo de contenido es (política, guía, código, FAQ)
Extracción Automática de Metadatos
from datetime import datetime
from pathlib import Path
def enrich_metadata(doc, base_path: str = ""):
"""Agregar metadatos utiles a un documento cargado."""
source = doc.metadata.get("source", "")
path = Path(source)
# Extraer de la ruta del archivo
doc.metadata["filename"] = path.name
doc.metadata["extension"] = path.suffix
doc.metadata["directory"] = str(path.parent)
# Extraer titulo del primer encabezado
lines = doc.page_content.split("\n")
for line in lines:
if line.startswith("# "):
doc.metadata["title"] = line.lstrip("# ").strip()
break
# Agregar marca de tiempo de procesamiento
doc.metadata["indexed_at"] = datetime.now().isoformat()
# Estimar tipo de contenido
if path.suffix in [".py", ".js", ".ts", ".go"]:
doc.metadata["content_type"] = "code"
elif path.suffix in [".md", ".mdx"]:
doc.metadata["content_type"] = "documentation"
elif path.suffix == ".pdf":
doc.metadata["content_type"] = "document"
return doc
Construyendo un Cargador Multi-Fuente
En producción, raramente cargas documentos de una sola fuente. Un sistema RAG típico ingesta de múltiples formatos y ubicaciones. Aquí hay un patrón para construir un cargador unificado:
from langchain_community.document_loaders import (
PyPDFLoader,
TextLoader,
DirectoryLoader,
WebBaseLoader,
)
def load_all_documents(config: dict) -> list:
"""Cargar documentos de multiples fuentes."""
all_docs = []
# Cargar PDFs
if "pdf_dir" in config:
loader = DirectoryLoader(
config["pdf_dir"],
glob="**/*.pdf",
loader_cls=PyPDFLoader
)
pdf_docs = loader.load()
for doc in pdf_docs:
doc.metadata["source_type"] = "pdf"
all_docs.extend(pdf_docs)
# Cargar documentos Markdown
if "docs_dir" in config:
loader = DirectoryLoader(
config["docs_dir"],
glob="**/*.md",
loader_cls=TextLoader,
loader_kwargs={"encoding": "utf-8"}
)
md_docs = loader.load()
for doc in md_docs:
doc.metadata["source_type"] = "markdown"
all_docs.extend(md_docs)
# Cargar paginas web
if "urls" in config:
loader = WebBaseLoader(web_paths=config["urls"])
web_docs = loader.load()
for doc in web_docs:
doc.metadata["source_type"] = "web"
all_docs.extend(web_docs)
# Enriquecer todos los metadatos
all_docs = [enrich_metadata(doc) for doc in all_docs]
print(f"Cargados {len(all_docs)} documentos de {len(config)} fuentes")
return all_docs
# Uso
docs = load_all_documents({
"pdf_dir": "./data/pdfs",
"docs_dir": "./data/docs",
"urls": [
"https://docs.example.com/faq",
"https://docs.example.com/inicio-rapido",
]
})
Manejo de Tablas y Datos Estructurados
Las tablas en PDFs y HTML son uno de los desafíos más complicados. Cuando la extracción de texto aplana una tabla, las relaciones fila-columna se pierden y el LLM no puede interpretar los datos correctamente.
Estrategias para tablas:
- Preservar estructura. Usa
unstructuredcon estrategiahi_respara detectar y extraer tablas en formato HTML o Markdown. - Describir tablas. Convierte tablas a descripciones en lenguaje natural: "En Q3 2024, los ingresos fueron $12M, un aumento del 15% respecto a Q2."
- Separar fragmentos de tablas. Almacena tablas como fragmentos propios con metadatos indicando que son datos tabulares.
- Usar modelos de visión. Para tablas complejas, captura la tabla y usa un LLM de visión para describir su contenido.
Consejos para Procesamiento de Documentos en Producción
- Deduplica. Verifica documentos duplicados antes de indexar. El mismo contenido de diferentes fuentes contaminará tus resultados de búsqueda.
- Limpia agresivamente. Elimina encabezados, pies de página, números de página y texto boilerplate. Agregan ruido sin agregar información.
- Preserva los títulos. Los títulos de sección proporcionan contexto crucial. Inclúyelos en cada fragmento o almacénalos como metadatos.
- Rastrea versiones. Cuando los documentos se actualizan, re-indéxalos y elimina entradas obsoletas. Un sistema RAG respondiendo con documentos desactualizados es peor que no responder en absoluto.
- Registra todo. Registra cuántos documentos se cargaron, cuántos fallaron y por qué. Los fallos de parsing son inevitables y necesitas visibilidad sobre ellos.
El procesamiento de documentos no es glamoroso, pero es donde muchos sistemas RAG triunfan o fracasan. En la siguiente lección, aprenderás cómo dividir estos documentos procesados en fragmentos optimizados para la recuperación.