Skip to content

Documentos Office para RAG

Las pipelines de Retrieval-Augmented Generation viven y mueren por la calidad de la ingesta. Office Oxide te da tres salidas (texto, Markdown, IR estructurado) que mapean directamente a las tres cosas que una pipeline RAG necesita: cuerpo para embeddings, estructura para chunking, metadatos para citas.

Elige tu salida

Objetivo Usa
Embeddings más baratos, mínimo coste de token plain_text()
Chunks con estructura preservada (mejor calidad de retrieval) to_markdown()
Chunk + cita por section/diapositiva/celda to_ir()

Para la mayoría de proyectos, to_markdown() es el sweet spot: preserva títulos (límites naturales de chunk), las tablas siguen siendo consultables y es lo bastante pequeño como para embeber sin disparar los tokens.

Chunking consciente de títulos desde Markdown

La salida Markdown usa # / ## / ### para los títulos de origen. Cortar por ahí da chunks semánticamente coherentes “gratis”.

from office_oxide import Document

def chunk_by_heading(md: str, level: int = 2):
    chunks, current = [], []
    for line in md.splitlines():
        if line.startswith("#" * level + " "):
            if current:
                chunks.append("\n".join(current))
            current = [line]
        else:
            current.append(line)
    if current:
        chunks.append("\n".join(current))
    return chunks

with Document.open("report.docx") as doc:
    md = doc.to_markdown()

chunks = chunk_by_heading(md, level=2)
for c in chunks:
    print(len(c), c[:60].replace("\n", " "))

Chunking basado en IR para precisión de cita

Si necesitas citar diapositiva 3 o hoja “Q4 Forecast” en el contexto recuperado, recorre el IR. Cada section trae el localizador natural:

from office_oxide import Document

with Document.open("deck.pptx") as doc:
    ir = doc.to_ir()

chunks = []
for i, section in enumerate(ir["sections"], 1):
    title = section.get("title") or f"Diapositiva {i}"
    body = []
    for el in section["elements"]:
        if el["kind"] == "Heading":
            body.append("# " + el["text"])
        elif el["kind"] == "Paragraph":
            body.append(" ".join(r["text"] for r in el["runs"]))
        elif el["kind"] == "Table":
            for row in el["rows"]:
                body.append(" | ".join(row))
    chunks.append({
        "source": "deck.pptx",
        "locator": f"slide:{i}",
        "title": title,
        "text": "\n".join(body),
    })

Ahora tus chunks recuperados llevan un localizador preciso para citas (slide:3 / sheet:Q4 Forecast / section:2).

Integración con LangChain

from langchain_core.documents import Document as LCDoc
from office_oxide import Document

def load_office(path: str) -> list[LCDoc]:
    with Document.open(path) as doc:
        ir = doc.to_ir()
    out = []
    for i, section in enumerate(ir["sections"], 1):
        body_lines = []
        for el in section["elements"]:
            if el["kind"] == "Paragraph":
                body_lines.append(" ".join(r["text"] for r in el["runs"]))
            elif el["kind"] == "Heading":
                body_lines.append(el["text"])
        if not body_lines:
            continue
        out.append(LCDoc(
            page_content="\n".join(body_lines),
            metadata={
                "source": path,
                "section_index": i,
                "section_title": section.get("title"),
            },
        ))
    return out

docs = load_office("report.docx")

Tíralo en Chroma.from_documents(docs, embedder) (o cualquier vectorstore) como siempre.

Integración con LlamaIndex

from llama_index.core import Document as LIDoc
from office_oxide import Document

def load_office(path: str) -> list[LIDoc]:
    with Document.open(path) as doc:
        md = doc.to_markdown()
    return [LIDoc(text=md, metadata={"source": path})]

Para nodos por section, usa el patrón IR de arriba y pasa cada chunk como un LIDoc separado.

Tablas — la parte difícil

Los LLM manejan bien tablas pequeñas en formato Markdown. Las tablas grandes (50+ filas) van mejor resumidas o paginadas:

def summarize_table(rows: list[list[str]]) -> str:
    headers = rows[0]
    body = rows[1:]
    return f"Tabla con columnas {headers} y {len(body)} filas. Muestra: {body[:3]}"

Para dashboards (XLSX), considera extraer resúmenes por hoja en vez de volcados completos de celdas — el LLM saca más provecho de “La hoja ‘Q4’ totaliza ingresos de $4,2M en 12 regiones” que de 5.000 valores de celda.

Rendimiento y coste

Op Tiempo por archivo (DOCX, mediana) Notas
plain_text() 0,8 ms lo más barato
to_markdown() ~1,5 ms recomendado para RAG
to_ir() ~1,2 ms cuando necesitas estructura

Un corpus de un millón de documentos se extrae en ~25 minutos single-thread, ~3 minutos en 8 núcleos. El coste dominante en tu pipeline RAG serán las llamadas a la API de embedding, no el parseo de Office.

Véase también