Skip to content

RAG용 Office 문서

검색 증강 생성(RAG) 파이프라인은 인제스트 품질에 따라 살고 죽습니다. Office Oxide는 세 가지 출력(텍스트, Markdown, 구조화 IR)을 제공하며, 이들은 RAG 파이프라인이 필요로 하는 세 가지 — 임베딩용 본문, 청킹용 구조, 인용용 메타데이터 — 와 깔끔하게 매핑됩니다.

출력 선택

목표 사용
가장 저렴한 임베딩, 최저 토큰 비용 plain_text()
구조 보존 청크(최고의 검색 품질) to_markdown()
섹션·슬라이드·셀 단위 청크 + 인용 to_ir()

대부분의 프로젝트에서 to_markdown()이 최적의 균형점입니다: 제목 보존(자연스러운 청크 경계), 표는 질의 가능한 형태로 유지, 토큰을 과도하게 쓰지 않고 임베딩하기에 충분히 작음.

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

이제 검색된 청크는 인용용 정확한 로케이터(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)(또는 어떤 벡터스토어든)에 넣으세요.

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’는 12개 지역에서 매출 $4.2M을 합산"이 5,000개 셀 값보다 더 도움이 됩니다.

성능과 비용

작업 파일당 시간(DOCX 중간값) 비고
plain_text() 0.8 ms 가장 쌈
to_markdown() ~1.5 ms RAG에 권장
to_ir() ~1.2 ms 구조가 필요할 때

100만 문서 코퍼스가 단일 스레드로 ~25분, 8코어로 ~3분에 추출됩니다. RAG 파이프라인의 지배적 비용은 임베딩 API 호출이지 Office 파싱이 아닙니다.

더 보기