Skip to content

Documentos Office para RAG

Pipelines de Retrieval-augmented Generation vivem e morrem pela qualidade da ingestão. Office Oxide te dá três saídas (texto, Markdown, IR estruturado) que mapeiam direto nas três coisas que um pipeline de RAG precisa: corpo para embeddings, estrutura para chunking, metadados para citações.

Escolha sua saída

Objetivo Use
Embeddings mais baratos, menor custo de token plain_text()
Chunks que preservam estrutura (melhor qualidade de retrieval) to_markdown()
Chunk + citação por section/slide/célula to_ir()

Para a maioria dos projetos, to_markdown() é o ponto doce: preserva títulos (limites naturais de chunk), mantém tabelas consultáveis e é pequeno o bastante para embedar sem explodir tokens.

Chunking ciente de título a partir do Markdown

A saída Markdown usa # / ## / ### para títulos da fonte. Cortar por aí dá chunks semanticamente coesos “de graça”.

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 baseado em IR para precisão de citação

Se você precisa citar slide 3 ou planilha “Q4 Forecast” no contexto recuperado, percorra o IR. Cada section traz o locator 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"Slide {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),
    })

Agora seus chunks recuperados têm um locator preciso para citação (slide:3 / sheet:Q4 Forecast / section:2).

Integração com 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")

Joga em Chroma.from_documents(docs, embedder) (ou qualquer vectorstore) como sempre.

Integração com 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 nodes por section, use o padrão IR acima e passe cada chunk como um LIDoc separado.

Tabelas — a parte difícil

LLMs lidam bem com tabelas pequenas em Markdown. Tabelas grandes (50+ linhas) ficam melhor resumidas ou paginadas:

def summarize_table(rows: list[list[str]]) -> str:
    headers = rows[0]
    body = rows[1:]
    return f"Tabela com colunas {headers} e {len(body)} linhas. Amostra: {body[:3]}"

Para dashboards (XLSX), considere extrair sumários por planilha em vez de dumps de células — o LLM tira mais proveito de “Planilha ‘Q4’ totaliza receita de $4.2M em 12 regiões” do que de 5.000 valores de célula.

Performance e custo

Op Tempo por arquivo (DOCX, mediana) Notas
plain_text() 0,8 ms mais barato
to_markdown() ~1,5 ms recomendado para RAG
to_ir() ~1,2 ms quando você precisa de estrutura

Um corpus de um milhão de documentos extrai em ~25 minutos single-thread, ~3 minutos em 8 cores. O custo dominante no seu pipeline RAG vai ser as chamadas à API de embedding, não o parsing do Office.

Veja também