如果你正在做 RAG(检索增强生成)、知识库问答或者 Agent 应用,大概率会遇到这样一个场景:用户丢过来一份 PDF,说“帮我总结一下”“帮我查一个数据”“把这份合同的关键条款提取出来”。
很多人的第一反应是:直接把 PDF 喂给 LLM。
我见过的项目里,这一步埋下了大量后续问题。有的把 PDF 转成图片后交给多模态模型,解析速度慢、成本高;有的直接把二进制流塞进 prompt,模型输出一堆不可控的内容;还有的用正则硬抠 PDF 内的文本,结果被字体编码和表格布局折磨到怀疑人生。
这里有一个非常关键的认知误区:LLM 不是 PDF 解析器。
把 PDF 文件直接丢给大模型,不等于把 PDF 里的内容真正“读”明白了。真正可靠的工程做法,是先使用专业的数据加载与解析工具把文档内容清洗成结构化文本,再交给 LLM 做语义理解。OpenDataLoader 这一类文档加载工具的价值,恰恰就在这个容易被忽略的环节。
这篇文章会讲清楚:为什么说 LLM 不是 PDF Parser;PDF 解析到底难在哪里;OpenDataLoader 这类工具在 RAG 管道里处于什么位置;以及一个完整的、可落地的最小实践流程。
1. 这篇文章真正要解决的问题
先直接给结论:LLM 擅长的是“语义理解”,而不是“文件格式解析”。
这两件事在技术链路上是完全分开的。
PDF 解析是把 PDF 文件里二进制编码的文本、图片、表格、排版信息提取成纯文本或结构化数据。这一步的工作对象是文件格式和编码规则,跟“语义”没有关系。
LLM 理解是在纯文本之上做总结、抽取、推理。这一步的工作对象是语言,跟“PDF 是什么格式”没有关系。
如果你跳过第一步,直接把 PDF 丢给 LLM,模型的上下文窗口里接收到的是什么呢?
不要想当然地认为,模型“看”到的内容和你用 PDF 阅读器看到的内容一样。模型拿到的是一个原始字节序列。LLM 的 tokenizer 是面向自然语言文本设计的,它不认识 PDF 的二进制编码。如果你强行把原始 PDF 字节放进 prompt,不仅会消耗大量 token,还会得到大量无意义字符,模型的输出质量会断崖式下降。
就算你用 LangChain 或 LlamaIndex 的PDFLoader加载了 PDF,本质上也还是依赖一个底层 PDF 解析库来提取文本。这个解析过程做得够不够好,直接决定 RAG 检索的精度。
所以这篇文章真正要解决的问题是:
- 为什么 PDF 解析是一个和 LLM 无关的独立工程问题?
- PDF 解析的难点具体有哪些?
- OpenDataLoader 这类文档加载工具在其中承担什么角色?
- 如何设计一个“先正确解析,再交给 LLM”的数据管道?
- 常见的解析坑有哪些,怎么排查?
如果你是正在搭建知识库、文档问答系统、合同审核工具、论文阅读助手或者 Agent 应用的后端工程师,这篇文章值得认真读完。
2. 基础概念与核心原理
2.1 LLM 上下文窗口的边界
LLM 上下文窗口决定了模型一次能“看到”多少 token。
GPT-4 级别的模型通常支持 32K 到 200K token 的上下文,Claude、Gemini 也有类似的设计。很多人以为:“只要上下文窗口够大,直接把 PDF 整本塞进去不就行了?”
这里有两个问题:
第一,PDF 里的文本并不是以自然语言形式线性排列的。PDF 文件内部是一组页面对象、字体对象、内容流的组合。一段文本可能被拆散成多个文本块,甚至按字符位置单独绘制。你看到的每一行文字,在 PDF 内部是“被定位”的图形元素。
第二,即便你强行把提取出来的文本全部塞进上下文,模型的推理成本和时间也会随之上升。更重要的是,语境中无关的解析噪声会干扰模型对重点内容的判断。
所以正确的做法是:在把内容送进 LLM 之前,确保内容已经是“干净的结构化文本”。
2.2 PDF 解析的本质:从布局到文本
PDF 解析的目标,是把视觉布局还原成有逻辑顺序的文本流。
听起来简单,实际上难点非常多。
第一个难点是文本提取顺序。PDF 内部存的是绘制指令,不是文档大纲。一段文章在 PDF 里可能按列排版,阅读顺序是“右栏从上到下,再从左栏从上到下”,但解析器提取时可能按对象 ID 顺序输出,文字顺序完全错乱。
第二个难点是字体内嵌与编码。PDF 为了跨平台一致显示,经常内嵌自定义字体。有些字体没有标准 Unicode 映射表,解析器如果不知道怎么把字形映射回字符,提取出来就是乱码或者空白。
第三个难点是表格结构还原。PDF 里的表格是“画”出来的,线条是图形对象,文字是文本对象。把两者重新拼接成行、列、单元格的结构,需要复杂的启发式算法。
第四个难点是扫描版 PDF。这种 PDF 本质是图片,解析器提取不到任何文本层,必须借助 OCR(光学字符识别)技术才能得到内容。
理解这些难点之后,你就明白为什么说“LLM 不是 PDF Parser”了。LLM 根本没有处理 PDF 编码、字体映射、版式重建的能力。它只负责在文本之上做推理。
2.3 文档加载层的价值
在 RAG 架构里,文档加载层位于“原始文件”和“文本切块与向量化”之间。
这一层要做的事包括:
- 解析不同格式的文档(PDF、Word、PPT、Markdown、HTML 等)。
- 抽取正文文本,过滤页眉、页脚、水印、导航噪声。
- 还原表格、标题层级、段落结构。
- 输出标准化的文本或 JSON 数据结构,供后续分块和向量化使用。
OpenDataLoader 这类工具做的,就是把这一层做得更专业、更开箱即用。
你甚至可以这样理解:
LLM 是大脑,负责思考。OpenDataLoader 是消化系统,负责把食物(文档)分解成身体能吸收的营养(结构化文本)。
如果消化系统出问题,大脑再聪明也白搭。
2.4 为什么“先解析,再给 LLM”是 RAG 的事实标准
从 RAG 管道的视角看,完整的链路是:
原始文档 → 文档加载与解析(OpenDataLoader 等) → 文本清洗与切块 → Embedding 向量化 → 向量数据库存储 → 查询时召回 → LLM 生成答案在这个链路里,第一步做不好,后续所有环节都会退化。
假设解析阶段把一段跨页的表格拆散了,切块阶段就会把表格内容切到两个不同的 chunk 里,召回阶段就很难同时命中完整数据,最终 LLM 给出的答案自然残缺。
所以,专业的数据加载工具不是可有可无的装饰品,而是保证 RAG 系统检索精度的地基。
3. OpenDataLoader 的核心能力与适用场景
3.1 它解决什么问题
从材料来看,OpenDataLoader 是一个数据加载工具,尤其在处理多格式文档加载与转换方面做了大量工作。它的定位,正好补上“LLM 不擅长 PDF 解析”的短板。
把它放在 RAG 管道里看,它解决的问题非常明确:
- 从 PDF、XML、DOC、PPT、CSV、图片等各类文件中提取文本内容。
- 把非结构化数据转换成适合 LLM 处理的格式。
- 为 RAG、Agent、文档问答等 LLM 应用提供“干净的数据入口”。
它适合以下几类场景:
场景一:知识库问答。
你有一个包含几百份 PDF 的企业知识库,需要让员工通过自然语言查询内部规范、产品手册、规章制度。这时候必须先批量解析 PDF,提取出正文,再做向量化和索引。
场景二:合同审查助手。
合同通常以 PDF 形式存在,需要抽取关键条款、到期时间、违约责任。解析质量直接决定后续抽取的准确性。如果解析出来的文本是乱序的,模型会把甲方和乙方的责任主体搞反。
场景三:研究报告与论文阅读。
学术 PDF 的排版复杂,双栏、公式、脚注、参考文献混在一起。专业加载工具需要能处理这种复杂版式,保留标题层级和段落逻辑。
场景四:Agent 工具链中的数据准备环节。
Agent 应用经常需要访问外部文件来完成用户任务。如果 Agent 要“读”一个 PDF 文件里的数据,合理的工程实现是:先用文档加载工具解析,再把解析后的结构化文本放入 Agent 的上下文。
3.2 使用它的正确姿势
这里要强调一个容易被误解的点:OpenDataLoader 不是 LLM 的替代品,而是 LLM 上游的数据入口。
它做的事情是“把文档变成模型能理解的形式”,而不是“理解文档内容”。
一个完整的应用流程大概是:
用户上传 PDF → OpenDataLoader 解析 PDF → 得到结构化文本 → 文本切块并向量化 → 存入向量数据库 → 用户提问时召回相关内容 → LLM 基于召回内容生成回答如果你已经跑通过一个简单的 RAG 项目,可以把这一步理解为:把原来用 LangChainPyPDFLoader加载 PDF 的步骤,换成更专业、更可靠的文档加载工具。
3.3 它与直接调用 LLM 的对比
| 维度 | 直接丢给 LLM | 先解析再交给 LLM |
|---|---|---|
| 输入内容 | 原始 PDF 二进制或未清洗文本 | 干净的结构化文本 |
| 理解质量 | 受噪声干扰,容易出错 | 稳定可控 |
| Token 消耗 | 高,含大量无意义内容 | 低,只包含有效文本 |
| 表格/结构还原 | 基本无法保证 | 可由解析层完成 |
| 扫描件支持 | 不支持 | 依赖 OCR 能力 |
| 可控性 | 低,模型输出不稳定 | 高,能定位问题环节 |
这个对比很清楚:把 PDF 解析前置,相当于把“不可控”变成“可控”。
4. 环境准备与前置条件
实战部分会包含一个最小示例,把“解析 PDF → 文本切块 → 向量化检索 → LLM 生成回答”这条链路完整跑通。
环境方面,建议满足以下条件:
- Python 3.9 及以上版本。
- 一个可用的 LLM API(OpenAI、通义、文心、智谱、本地 Ollama 都行)。
- 一个向量数据库(Chroma、FAISS、Milvus 都行,示例里用 FAISS 或 Chroma 最简单)。
- 基础依赖库:
openai、langchain、faiss-cpu(或chromadb)、pdfplumber(用于 PDF 文本提取示例)。
OpenDataLoader 的具体安装方式以项目官方文档为准,因为不同版本和发行渠道安装命令可能不同。下面演示的更多是“解析层 + LLM”配合使用的通用思路,这段逻辑在换成任何文档加载工具时都是成立的。
# 创建虚拟环境(以 venv 为例) python3 -m venv venv source venv/bin/activate # 安装基础依赖,版本请以实际项目要求为准 pip install openai langchain pdfplumber faiss-cpu chromadb安装完成后,检查 Python 版本确认环境正常:
python --version如果你的环境里已经有 Conda,也可以用它创建独立环境:
conda create -n># 文件路径:pdf_extract.py import pdfplumber def extract_text_from_pdf(file_path: str) -> str: """从 PDF 文件中提取全部文本。""" full_text = [] with pdfplumber.open(file_path) as pdf: for page in pdf.pages: text = page.extract_text() if text: full_text.append(text) return "\n\n".join(full_text) if __name__ == "__main__": result = extract_text_from_pdf("demo.pdf") print(result[:2000])如果 PDF 是扫描件,extract_text()往往是空字符串。这时需要接 OCR,实践中可以先用pdfplumber判断页面上是否存在文本层,没有就转入 OCR 分支。
5.2 步骤二:文本切块与清洗
解析出来的文本往往夹杂着页眉、页脚、页码、标题中的多余空行。在交给向量化管道前,需要先做清洗和切块。
切块的核心是:保持语义完整性。最简单可靠的方式是“按段落切”,或者用固定窗口切但尽量保留标题上下文。下面是一个简单的切块示例:
# 文件路径:text_chunk.py import re from typing import List def clean_text(text: str) -> str: """清洗常见 PDF 提取噪声。""" # 去掉多余空行 text = re.sub(r"\n{3,}", "\n\n", text) # 去掉常见页眉页码模式,按需调整 lines = [l for l in text.splitlines() if not re.match(r"^\s*\d+\s*$", l)] return "\n".join(lines) def split_into_chunks(text: str, chunk_size: int = 800) -> List[str]: """按大致字符长度切块,尽量切在段落边界。""" paragraphs = text.split("\n\n") chunks, current = [], "" for para in paragraphs: if len(current) + len(para) <= chunk_size: current += "\n\n" + para else: chunks.append(current) current = para if current: chunks.append(current) return chunks这里的重点不是代码本身,而是让你理解:解析 → 清洗 → 切块,是三个独立步骤,不要混在一起。每一步都应该单独验证输出质量。
5.3 步骤三:向量化与存储
把清洗后的 chunk 转换成向量,存入向量数据库。这里用 FAISS 做演示,因为它简单,不需要额外启动服务。
# 文件路径:vector_store.py from langchain.embeddings import OpenAIEmbeddings from langchain.vectorstores import FAISS from text_chunk import clean_text, split_into_chunks from pdf_extract import extract_text_from_pdf def build_index(pdf_path: str, index_path: str = "faiss_index"): text = extract_text_from_pdf(pdf_path) cleaned = clean_text(text) chunks = split_into_chunks(cleaned) embeddings = OpenAIEmbeddings() vectorstore = FAISS.from_texts(chunks, embeddings) vectorstore.save_local(index_path) print(f"index saved, chunks count: {len(chunks)}") if __name__ == "__main__": build_index("demo.pdf")如果使用的是本地模型,Embedding 模型可以换成HuggingFaceEmbeddings,选择中文效果好的模型即可。
5.4 步骤四:检索与 LLM 生成
最后一步:用户提问,系统召回相关 chunk,把结果拼进 prompt,交给 LLM 生成回答。
# 文件路径:rag_query.py from langchain.vectorstores import FAISS from langchain.embeddings import OpenAIEmbeddings from langchain.chat_models import ChatOpenAI from langchain.chains import RetrievalQA def create_qa_chain(index_path: str = "faiss_index"): embeddings = OpenAIEmbeddings() vectorstore = FAISS.load_local(index_path, embeddings) retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) qa_chain = RetrievalQA.from_chain_type( llm=llm, retriever=retriever, chain_type="stuff", ) return qa_chain if __name__ == "__main__": chain = create_qa_chain() answer = chain.run("这份文档的结论是什么?") print(answer)到这里,一个最小可用的 RAG 链路就完整跑通了。
6. 完整示例与代码实现
为了让你更好地理解“先解析、再让 LLM 处理”的核心思路,这里把完整流程整合成一个可运行的脚本。
6.1 完整脚本结构
pdf_rag_demo/ ├── pdf_extract.py # PDF 解析 ├── text_chunk.py # 文本清洗与切块 ├── vector_store.py # 向量化与存储 ├── rag_query.py # 检索问答 ├── requirements.txt # 依赖清单 └── demo.pdf # 示例 PDF6.2 requirements.txt
# 文件路径:requirements.txt # 版本请根据实际环境调整,这里只列出包名 openai langchain pdfplumber faiss-cpu如果你的 Embedding 和 LLM 都使用 OpenAI,需要设置环境变量:
export OPENAI_API_KEY="your-api-key"如果你是使用国内大模型 API 或本地模型,请把OpenAIEmbeddings和ChatOpenAI换成对应的类。
6.3 运行步骤
第一步,执行 PDF 解析:
python pdf_extract.py预期输出:控制台打印 PDF 提取出的前 2000 个字符。如果输出为空,说明 PDF 没有文本层,需要走 OCR 方案。
第二步,构建向量索引:
python vector_store.py预期输出:
index saved, chunks count: 12这里的chunks count取决于你的 PDF 长度和切块大小。
第三步,发起检索问答:
python rag_query.py预期输出:模型根据召回内容给出的回答。
6.4 关键逻辑解析
这个示例最核心的地方在于:PDF 解析发生在任何 LLM 调用之前。
pdf_extract.py负责把 PDF 变成文本。text_chunk.py负责把文本变成语义相对完整的块。vector_store.py负责把块变成向量并建立索引。rag_query.py负责检索并让 LLM 基于检索结果回答。
每一步的输入输出都是标准文本或向量,不存在“把 PDF 二进制直接塞给模型”的黑色地带。这样出现问题的时候,你只需要检查对应环节,不需要在一个黑盒里盲猜。
6.5 对比实验:直接让 LLM 读 PDF
如果你还不死心,可以做一个简单对比。把 PDF 文件读成 bytes,然后 base64 编码后拼进 prompt,看模型输出什么:
# 文件路径:llm_direct_pdf.py import base64 # 这只是演示错误用法,不推荐实际项目使用 with open("demo.pdf", "rb") as f: data = base64.b64encode(f.read()).decode("utf-8") prompt = f"请总结这份文档的内容:\n{data[:1000]}" print(prompt)你会发现,这段内容里全是不可读的字符。模型在绝大多数情况下无法从这种输入里给出有意义的总结。
这就是“LLM Is Not a PDF Parser”最直观的证明:它的上下文窗口中需要的是自然语言文本,不是文件编码。
7. 运行结果与效果验证
跑通上面的流程之后,怎么判断结果是否正常?
7.1 解析阶段验证
运行python pdf_extract.py后:
- 如果打印的文本可以正常阅读,表达顺序和原文档一致,说明解析成功。
- 如果出现乱码,检查 PDF 是否使用了内嵌非标字体。
- 如果出现空白,检查 PDF 是否是扫描件。
7.2 索引阶段验证
运行vector_store.py后,确认生成的faiss_index/目录存在,并且包含索引文件。
可以打印 chunks 数量和前几个 chunk 的内容:
from text_chunk import clean_text, split_into_chunks from pdf_extract import extract_text_from_pdf text = extract_text_from_pdf("demo.pdf") chunks = split_into_chunks(clean_text(text)) for i, chunk in enumerate(chunks[:3]): print(f"chunk {i}: {chunk[:100]}")这一步能帮你确认切块逻辑是否合理,有没有出现一句话被硬生生切到两个块里的情况。
7.3 检索问答阶段验证
运行rag_query.py后:
- 如果回答内容来自文档,说明链路完整。
- 如果回答内容明显是模型“自由发挥”,没有参考文档,说明检索召回失败,或者 prompt 里没有把召回内容正确拼接进去。
一个简单的验证方法是:直接打印retriever.get_relevant_documents("你的问题"),看召回的文本是否和问题相关。
vectorstore = FAISS.load_local("faiss_index", OpenAIEmbeddings()) docs = vectorstore.similarity_search("这份文档的结论是什么?", k=3) for doc in docs: print(doc.page_content[:200])如果召回到的内容和问题毫无关系,问题大概率出在 PDF 解析阶段,不是 LLM 的问题。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| PDF 提取文本乱码 | 字体映射缺失,或自定义字体无 Unicode 编码 | 用 PDF 阅读器打开检查显示是否正常;抽取部分文本查看十六进制编码 | 换用支持字体映射的解析引擎;必要时转成 Word 或用 OCR |
| 提取出的文本顺序错乱 | PDF 是多栏排版,解析器按对象顺序输出 | 对比原文档布局与提取文本顺序 | 启用解析工具的阅读顺序检测;或按坐标排序 |
| 提取结果为空白 | PDF 是扫描件,无文本层 | 打开 PDF,尝试全选复制 | 接入 OCR 流程,如 PaddleOCR、Tesseract |
| 表格内容缺失或错位 | PDF 中表格由图形绘制,纯文本提取无法还原结构 | 检查表格区域是否被当作单独图形处理 | 使用带表格抽取能力的加载工具,或单独做表格解析 |
| 中文标点变成“□” | 字体缺少标点映射 | 查看提取文本中的具体字符 | 尝试不同解析器;用正则清洗特殊字符 |
| 向量检索召回不准 | 解析文本里混入大量页眉页脚噪声 | 打印 index 里的文本内容 | 强化清洗规则,过滤页眉页脚、页码、水印 |
| Token 消耗异常大 | 清洗前有大量重复模板文本 | 统计文本长度和重复片段 | 增加去重逻辑;更 aggressive 的清洗 |
| LLM 回答与文档不符 | 查询时未正确引用召回内容 | 打印最终发送给 LLM 的 prompt | 检查 RetrievalQA 的 prompt 模板,确保包含召回文本 |
这些问题的共性规律是:绝大多数情况下,问题出在文档解析层,而不是 LLM 层。所以排查的时候,不要一上来就调 prompt,先检查解析链路。
9. 最佳实践与工程建议
9.1 文档加载层独立成服务
不要把文档解析逻辑写在业务代码里。建议把它独立成一个文件处理服务或函数,统一接收 PDF、DOCX、图片等文件,统一输出标准化的 Markdown 或 JSON 文本。
这样做的好处是:
- 业务侧不需要关心具体文件格式。
- 后续更换解析工具时,只需要改服务内部实现。
- 可以统一处理权限校验、格式限制、文件大小限制。
9.2 保留原始文件与解析结果的双存储
在实际知识库系统里,建议同时保存原始文件和解析后的文本。
- 原始文件用于用户预览、下载。
- 解析后文本用于向量化和检索。
这样当用户对答案存疑时,可以快速回到原文核对。很多 RAG 产品最终都会做“答案解析引用原文”功能,这就要求原始文件和解析文本的映射关系必须保留。
9.3 解析结果要可验证
给解析阶段增加一个质量校验环节。比如:
- 统计提取文本长度是否在合理范围。
- 随机抽几页对比原文档。
- 监控解析失败率和乱码率。
如果一个 PDF 解析后得到的文本只有几十个字,但原始 PDF 有几十页,那大概率解析有问题,不应该进入后续链路。
9.4 扫描件与文本型 PDF 分流处理
在数据导入阶段,先检测 PDF 是否有文本层。
- 有文本层:走文本解析链路。
- 无文本层:走 OCR 链路。
不要把两者混在一个流程里,否则要么 OCR 耗时严重,要么文本 PDF 被错误地转成图片。
9.5 切块策略跟文档结构走
不要只用固定字符长度切块。更好的方式是优先按标题层级切块,再按段落切块。这样每个 chunk 都有自己的“主题上下文”,检索时更容易命中。
如果文档有明确的章节结构,可以先解析成 Markdown,再按#、##层级切分。
9.6 注意安全与权限
PDF 解析阶段也是安全事件的常见入口。实践中的基本要求:
- 限制上传文件大小和类型。
- 对二进制文件做病毒扫描(按需)。
- 解析服务与业务服务做最小权限隔离。
- 涉及敏感合同、身份信息时,解析结果要按原文档的权限等级管理。
- 不要在日志里打印完整解析文本,防止敏感信息泄露。
这些不是可有可无的“合规要求”,而是在生产环境中必须有的底线。
9.7 不要一开始就追求“所有格式全都要支持”
很多团队一开始就要求支持 PDF、Word、PPT、图片、网页、邮件等全部格式,最后每个格式都做得不深,问题频发。
更务实的做法是:先支持最常见的 2 到 3 种格式(通常就是 PDF、Word、Markdown),跑通稳定流程,再逐步扩展。每个格式的接入,都要单独走一遍“解析 → 清洗 → 切块 → 验证”的流程。
10. 总结与后续学习方向
这篇内容的核心判断就一句话:把 PDF 解析交给专业数据加载工具,把语义理解交给 LLM,两者各司其职,RAG 系统才能真正稳定。
以前在项目里看到过太多次类似问题:团队花了很多精力调 prompt、调模型参数,结果问题根源只是 PDF 文本提取乱序,模型根本没有读到正确的内容。一旦把文档加载层前置,很多“模型效果差”的问题会自然消失。
如果你想继续深入,有这几个方向值得关注:
第一,解析层的深度优化。
不同行业的文档差异非常大。学术论文侧重双栏与公式还原,合同侧重表格与条款定位,扫描书籍侧重 OCR 与版式复原。选择哪种解析工具、如何调整参数,是真正的工程经验积累。
第二,文档结构还原的质量评估。
怎么自动判断解析质量?有没有可量化的指标?这其实是 RAG 系统中最容易被忽视的评估点。
第三,RAG 管道的全链路可观测性。
不仅是解析阶段,从原始文档到最终回答,每一步都应该有日志和中间产物。出问题时能快速定位到具体环节,对于生产系统至关重要。
第四,多模态文档处理。
当文档里包含大量图片、流程图中时,光提取文本是不够的。需要判断哪些版面需要 OCR、哪些图片需要用多模态模型描述,并设计合理的存储结构。
把基础打牢之后,你会发现自己对 LLM 应用的理解会完全不一样:模型的能力边界只是问题的一半,数据从文档到模型之间的那一段路,才是真正决定成败的部分。
建议你把文中的最小示例跑一遍,拿一个自己手边真实的 PDF 测试,看看解析效果,打印出每个 chunk 的内容,感受一下“解析质量对检索效果的影响”。只有亲手跑过一遍,才能真正理解为什么说“LLM Is Not a PDF Parser”。