intermediate45 minLección 4 de 5

Integración de Base de Conocimiento y Procesamiento de Documentos

Ingiere documentos PDF, HTML y Markdown, divídelos inteligentemente, extrae metadatos y conéctalos a la base de conocimiento del agente.

Integración de Base de Conocimiento y Procesamiento de Documentos

Un agente es tan conocedor como los documentos a los que puede acceder. Construir un pipeline robusto de ingesta de documentos — desde archivos brutos hasta una base de conocimiento buscable — es una habilidad fundamental de ingeniería para agentes de producción.


Ingesta de Documentos (PDF, HTML, Markdown)

Diferentes formatos requieren diferentes cargadores. LangChain proporciona cargadores para cada formato principal.

python
from langchain_community.document_loaders import ( PyPDFLoader, # PDF files BSHTMLLoader, # HTML pages TextLoader, # Plain text / Markdown UnstructuredMarkdownLoader, # Structured Markdown ) # Load a PDF pdf_loader = PyPDFLoader("contract.pdf") pdf_docs = pdf_loader.load() print(f"Loaded {len(pdf_docs)} pages from PDF") # Output: Loaded 12 pages from PDF # Load an HTML file html_loader = BSHTMLLoader("page.html") html_docs = html_loader.load() print(f"Title: {html_docs[0].metadata.get('title', 'N/A')}") # Output: Title: Product Documentation # Load Markdown md_loader = UnstructuredMarkdownLoader("readme.md") md_docs = md_loader.load() print(f"Loaded {len(md_docs)} Markdown documents")
⚠️Warning

La calidad de la extracción de PDF varía enormemente. Los PDFs escaneados requieren OCR (ej: pytesseract o Azure Document Intelligence). Siempre inspecciona el texto extraído antes de indexar.

Pipeline de Ingesta de Documentos

100%

Procesamiento de Documentos: Secuencia Punto a Punto

100%
📌Important

Siempre valida la salida de cada etapa del pipeline antes de pasar a la siguiente. Una falla común es que el cargador de documentos devuelva texto vacío o distorsionado, que luego se incrusta e indexa como si fuera significativo. Agrega verificaciones de validación: longitud del texto > 0, proporción de caracteres y detección de idioma.


OCR para Documentos Escaneados

Los PDFs escaneados contienen imágenes de texto, no texto seleccionable. El OCR extrae el texto de las imágenes:

python
# Example: using pytesseract for OCR on scanned PDFs from pdf2image import convert_from_path import pytesseract from langchain.schema import Document def ocr_pdf(filepath: str) -> list[Document]: """Extract text from a scanned PDF using OCR.""" images = convert_from_path(filepath) documents = [] for page_num, image in enumerate(images, start=1): # Run OCR on the page image text = pytesseract.image_to_string(image, lang="eng") if len(text.strip()) < 20: # Skip pages with insufficient text continue doc = Document( page_content=text, metadata={ "source": filepath, "page": page_num, "ocr_method": "tesseract", }, ) documents.append(doc) return documents # Usage # docs = ocr_pdf("scanned_contract.pdf") # print(f"Extracted {len(docs)} pages via OCR")
ℹ️Note

La precisión del OCR depende en gran medida de la calidad de la imagen. Para mejores resultados, escanea a 300 DPI o más, usa modo blanco y negro, y asegúrate de que el documento esté plano (no enrollado o doblado). Considera servicios comerciales de OCR (Azure Document Intelligence, Google Cloud Vision) para escaneos de baja calidad.


Estrategias de División de Texto

El divisor elegido cambia drásticamente la calidad de la recuperación. A continuación se muestran las estrategias más comunes.

python
from langchain.text_splitter import ( RecursiveCharacterTextSplitter, TokenTextSplitter, MarkdownHeaderTextSplitter, ) # Strategy 1: Recursive character splitting (general purpose) recursive_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=["\n\n", "\n", ". ", " "], ) # Strategy 2: Token-aware splitting (matches LLM tokenizers) token_splitter = TokenTextSplitter( chunk_size=256, # tokens, not characters chunk_overlap=50, ) # Strategy 3: Markdown-aware splitting (preserves headers) headers_to_split_on = [ ("#", "Header 1"), ("##", "Header 2"), ("###", "Header 3"), ] markdown_splitter = MarkdownHeaderTextSplitter( headers_to_split_on=headers_to_split_on, )
EstrategiaUnidadPreserva EstructuraSuperposiciónMejor Para
RecursiveCharacterCaracteresModeradaTexto general
TokenTokensBajaFragmentos alineados al LLM
MarkdownHeaderEncabezadosAltaNoDocumentos, wikis
RecursiveJsonClaves JSONAltaNoDatos JSON estructurados
HTMLHeaderEtiquetas HTMLAltaNoPáginas web
SemánticaLímites de oraciónAltaNoPreservación de pasajes coherentes

Extracción de Metadatos

Los metadatos transforman fragmentos en unidades filtrables y rastreables. Cada fragmento debe llevar suficiente contexto para identificar su origen.

python
from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter loader = PyPDFLoader("annual-report.pdf") docs = loader.load() # Add custom metadata to each page for i, doc in enumerate(docs): doc.metadata.update({ "page_number": i + 1, "source": "annual-report.pdf", "year": "2025", "doc_type": "financial_report", }) # Split and preserve metadata splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=50, ) chunks = splitter.split_documents(docs) print(f"Total chunks: {len(chunks)}") print(f"Sample metadata: {chunks[0].metadata}") # Output: Total chunks: 47 # Sample metadata: {'page_number': 1, 'source': 'annual-report.pdf', # 'year': '2025', 'doc_type': 'financial_report'}
💡Tip

Un esquema de metadatos bien diseñado es tan importante como el propio embedding. Campos de metadatos comunes de alto valor: source (ruta del archivo o URL), page_number, section_title, author, date_published, doc_type, language, content_hash. Estos campos permiten filtrado potente y hacen que tu base de conocimiento sea auditables.

python
def enrich_metadata(doc, source_path: str) -> dict: """Automatically extract metadata from document content.""" import re metadata = { "source": source_path, "ingested_at": datetime.utcnow().isoformat(), } # Try to extract title from first heading lines = doc.page_content.split("\n") for line in lines[:10]: if line.startswith("# "): metadata["title"] = line.strip("# ") break # Detect language (simplified — use langdetect in production) if re.search(r"[¿¡áéíóúñ]", doc.page_content): metadata["language"] = "es" elif re.search(r"[àèìòùç]", doc.page_content): metadata["language"] = "pt" elif re.search(r"[äöüß]", doc.page_content): metadata["language"] = "de" else: metadata["language"] = "en" # Count tokens (approximate) metadata["estimated_tokens"] = len(doc.page_content.split()) * 1.3 return metadata

Indexación Incremental

Las bases de conocimiento reales nunca son estáticas. Llegan nuevos documentos, los antiguos se actualizan y las entradas obsoletas deben eliminarse.

python
import hashlib from datetime import datetime import chromadb client = chromadb.Client() collection = client.get_or_create_collection("knowledge_base") def index_document(filepath: str, content: str, metadata: dict) -> None: # Generate a content hash for deduplication content_hash = hashlib.sha256(content.encode()).hexdigest() # Check if this content already exists existing = collection.get(ids=[content_hash]) if existing["ids"]: print(f"Skipping duplicate: {filepath}") return # Add timestamp for incremental sync metadata["indexed_at"] = datetime.utcnow().isoformat() metadata["content_hash"] = content_hash # Split, embed, and index chunks = splitter.split_text(content) # ... embed and add to collection ... print(f"Indexed {filepath} ({len(chunks)} chunks)")
⚠️Warning

Los hashes de contenido son excelentes para detección exacta de duplicados, pero fallan cuando un documento se actualiza. Un documento con incluso un carácter cambiado tendrá un hash completamente diferente. Para detección de actualizaciones, también rastrea marcas de tiempo de modificación de archivo o números de versión.

Clasificación de Documentos por Estrategia de Procesamiento

Tipo de DocumentoCargadorPreprocesamientoDivisorTratamiento Especial
PDF basado en textoPyPDFLoaderNingunoRecursiveCharacterTexto seleccionable
PDF escaneadoPyPDFLoader + OCRImagen-a-textoRecursiveCharacterVerificación de calidad OCR
Página HTMLBSHTMLLoaderEliminar tags/navHTMLHeaderEliminación de pie de página
Documento MarkdownMarkdownHeaderNingunoMarkdownHeaderPreservación de encabezados
Datos JSONJSONLoaderValidar JSONRecursiveJsonValidación de esquema
CSV/ExcelCSVLoaderAnalizar filasRecursiveCharacterMetadatos de columnas
Repositorio de códigoTextLoaderFiltro .gitignoreTokenDetección de lenguaje

Actualización de Bases de Conocimiento

Los documentos cambian. Tu índice debe reflejar esos cambios sin una reconstrucción completa.

python
def update_document(filepath: str, new_content: str) -> None: # Delete all chunks from this source collection.delete(where={"source": filepath}) # Re-index with fresh content chunks = splitter.split_text(new_content) # ... embed and re-add ... print(f"Updated: {filepath}") def delete_document(filepath: str) -> None: collection.delete(where={"source": filepath}) print(f"Deleted: {filepath}")
ℹ️Note

El patrón eliminar-y-reindexar es simple y correcto, pero tiene una ventana donde el documento no está disponible. Para sistemas de alta disponibilidad, usa un enfoque de dos fases: indexa la nueva versión, luego intercambia atómicamente con la versión antigua.

python
def update_document_atomic(filepath: str, new_content: str) -> None: """Atomic update: index new version, then remove old version.""" # Generate a temporary group ID for the new chunks import uuid new_group_id = str(uuid.uuid4()) # Index new content with temporary group chunks = splitter.split_text(new_content) new_ids = [] for i, chunk in enumerate(chunks): chunk_id = f"{new_group_id}:{i}" new_ids.append(chunk_id) # ... embed and add with chunk_id ... # Now atomically remove old and keep new collection.delete(where={"source": filepath}) print(f"Atomically updated: {filepath}")

Conexión con Agentes

Una vez construida la base de conocimiento, conéctala a un agente a través de una herramienta de recuperación.

python
from langchain.tools import tool from langchain.agents import create_openai_functions_agent, AgentExecutor from langchain_openai import ChatOpenAI import chromadb client = chromadb.Client() collection = client.get_collection("knowledge_base") @tool def search_knowledge_base(query: str, k: int = 3) -> str: """Search the company knowledge base for relevant information.""" results = collection.query(query_texts=[query], n_results=k) return "\n\n".join(results["documents"][0]) # Create an agent with the KB tool llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) agent = create_openai_functions_agent( llm=llm, tools=[search_knowledge_base], prompt=..., # your system prompt here ) agent_executor = AgentExecutor(agent=agent, tools=[search_knowledge_base]) # Now the agent can answer from the knowledge base # result = agent_executor.invoke({"input": "What is the vacation policy?"})

Manejo de PII en la Ingesta de Documentos

⚠️Warning

Los documentos pueden contener información personal identificable (PII) como nombres, correos electrónicos, números de teléfono y números de tarjeta de crédito. Si tu base de conocimiento es utilizada por agentes que atienden a múltiples usuarios, la filtración de PII entre sesiones es un riesgo de privacidad y cumplimiento.

python
import re def sanitize_document(text: str) -> str: """Remove common PII patterns from document text.""" # Email addresses text = re.sub(r'\b[\w\.-]+@[\w\.-]+\.\w+\b', '[EMAIL]', text) # Phone numbers (US format) text = re.sub(r'\b\d{3}[-.]?\d{3}[-.]?\d{4}\b', '[PHONE]', text) # Social Security Numbers text = re.sub(r'\b\d{3}-\d{2}-\d{4}\b', '[SSN]', text) # Credit card numbers (simplified) text = re.sub(r'\b(?:\d{4}[-\s]?){3}\d{4}\b', '[CC]', text) return text # Apply before indexing # cleaned_text = sanitize_document(raw_text)

6 Preguntas de Práctica

Practice Question

¿Qué cargador deberías usar para un PDF escaneado?

Practice Question

¿Cuál es el propósito de la superposición de fragmentos en la división de texto?

Practice Question

¿Por qué deben adjuntarse metadatos a cada fragmento?

Practice Question

En la indexación incremental, ¿cómo evitar entradas duplicadas?

Practice Question

¿Cómo actualizar un documento en la base de conocimiento?

Practice Question

La base de conocimiento de un agente contiene documentos de RRHH con direcciones de correo electrónico de empleados. ¿Qué hacer antes de indexar?


Success

Conclusiones Clave

  • Diferentes formatos (PDF, HTML, Markdown) requieren cargadores específicos.
  • Los PDFs escaneados requieren procesamiento OCR antes de la extracción de texto.
  • La estrategia de división de texto — recursiva, basada en tokens o consciente de estructura — impacta directamente la calidad de la recuperación.
  • Los metadatos (fuente, página, timestamp, hash) hacen que los fragmentos sean filtrables y trazables.
  • La indexación incremental usa hashes de contenido para saltar duplicados y evitar reconstrucciones completas.
  • Actualizar un documento requiere eliminar fragmentos antiguos y re-indexar el nuevo contenido.
  • La base de conocimiento se conecta al agente mediante una herramienta de recuperación que envuelve la búsqueda vectorial.
  • El pipeline de ingesta es lineal: cargar, dividir, enriquecer metadatos, incrustar, indexar.
  • La PII debe sanitizarse antes de la indexación para prevenir fugas de datos.
  • Las actualizaciones atómicas previenen ventanas de indisponibilidad durante la re-indexación.
Progreso80%