Skip to content

Office-документи для RAG

Retrieval-augmented generation-пайплайни живуть і вмирають за якістю інжесту. Office Oxide дає три виходи (текст, Markdown, структурований IR), що рівно лягають на три потреби RAG-пайплайна: тіло для embeddings, структура для чанкінгу, метадані для цитат.

Вибір виходу

Мета Використовуйте
Найдешевші embeddings, мінімальна вартість токенів plain_text()
Чанки зі збереженням структури (найкраща якість retrieval) to_markdown()
Чанк + цитата за section/слайдом/клітинкою to_ir()

Для більшості проєктів золота середина — to_markdown(): зберігає заголовки (природні межі чанків), таблиці лишаються queryable, розмір достатньо малий, щоб embed’ити без вибуху токенів.

Чанкінг за заголовками з Markdown

Markdown-вивід використовує # / ## / ### для початкових заголовків. Різання за ними дає семантично узгоджені чанки «безкоштовно».

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", " "))

Чанкінг через IR для точності цитат

Якщо у знайденому контексті треба цитувати слайд 3 або лист “Q4 Forecast” — ходіть по IR. Кожна секція несе нативний локатор:

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"Слайд {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),
    })

Тепер ваші retrieved-чанки мають точний локатор (slide:3 / sheet:Q4 Forecast / section:2) для цитат.

Інтеграція з 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")

Кидайте у Chroma.from_documents(docs, embedder) (або будь-який vectorstore) як завжди.

Інтеграція з 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})]

Для нод по секціях використовуйте патерн із IR вище і передавайте кожен чанк окремим LIDoc.

Таблиці — складна частина

LLM добре справляються з невеликими таблицями у форматі Markdown. Великі таблиці (50+ рядків) краще резюмувати або пагінувати:

def summarize_table(rows: list[list[str]]) -> str:
    headers = rows[0]
    body = rows[1:]
    return f"Таблиця з колонками {headers} та {len(body)} рядків. Зразок: {body[:3]}"

Для дашбордів (XLSX) подумайте про зведення на лист замість повного дампа клітинок — LLM більше виграє від «Лист ‘Q4’ сумує дохід $4.2M по 12 регіонах», ніж від 5 000 значень клітинок.

Продуктивність і вартість

Op Час на файл (DOCX, медіана) Примітки
plain_text() 0,8 мс найдешевше
to_markdown() ~1,5 мс рекомендовано для RAG
to_ir() ~1,2 мс коли потрібна структура

Корпус з мільйона документів видобувається за ~25 хвилин single-thread, ~3 хвилини на 8 ядрах. Домінуюча вартість у вашому RAG-пайплайні буде за виклики embedding API, а не за парсинг Office.

Дивіться також