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 파싱이 아닙니다.
더 보기
- Markdown 추출 — 완전한 출력 사양
- 구조화 IR — 인용 인식 청킹용 스키마
- 배치 처리 — 병렬화 패턴