Skip to content

Office 文档用于 RAG

检索增强生成(RAG)流水线的成败取决于摄取质量。Office Oxide 给你三种输出(纯文本、Markdown、结构化 IR),它们恰好对应 RAG 流水线需要的三件事:用于嵌入的正文、用于分块的结构、用于引用的元数据。

选择输出

目标 使用
嵌入最便宜、token 成本最低 plain_text()
结构保留分块(检索质量最好) to_markdown()
按 section/幻灯片/单元格分块并引用 to_ir()

对大多数项目来说 to_markdown() 是甜蜜点:保留标题(自然分块边界)、表格保持可查询、足够小到能在不爆 token 的情况下嵌入。

基于 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。每个 section 都带有天然定位符:

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

要按 section 出节点,使用上面的 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 需要结构时

百万级文档语料单线程 ~25 分钟,8 核 ~3 分钟 抽取完成。RAG 流水线里真正主导成本的是嵌入 API 调用,不是 Office 解析。

相关链接