ARTICLE · 1300598
19-RAG文档加载精讲-从文件解析到可追溯的Document
load() 没报错,就继续切分、向量化,直到用户问起某个操作步骤,系统才开始答非所问。回头检查,PDF 读到的是页眉,网页抓到的是导航,操作手册中的表格丢了列关系。文件读取成功,不等于知识提取成功。 本篇用运维手册这个常见场景,把加载阶段应该交付什么讲清楚。
一、加载器交付的,不只是一个字符串
LangChain 用 Document 承载正文和元数据。加载器负责把外部格式转成这个接口,后续切分器和向量库就不必分别理解 PDF、Word 与网页。加载器接口
page_content | ||
metadata.source | ||
metadata.page | ||
这里有一个边界:Document 是统一容器,不是质量认证。乱码、空白和登录提示也可以装进去。正文检查必须由应用补上,不能指望后面的 Embedding 模型自动纠正。
还有一个容易忽略的问题:元数据会自动参与语义检索吗? 通常不会。常见向量库适配器嵌入的是 page_content;标题如果只放在元数据里,需要明确决定是否拼入待嵌入文本。
二、先把四种来源分开处理
TextLoader | ||
Docx2txtLoader | ||
PyPDFLoader | ||
WebBaseLoader |
本文使用 Python 3.12,兼容示例验证版本为 langchain-community==0.4.2、langchain-core==1.7.0。该社区包已停止维护,这些类适合理解现有项目;新项目应评估受维护的独立集成,或直接使用解析库再构造 Document。锁定版本只能帮助复现,不能代替安全维护。官方维护公告
python -m pip install "langchain-community==0.4.2""langchain-core==1.7.0" \"pypdf==6.21.4""docx2txt==0.9""beautifulsoup4==4.15.0"以下 Python 代码顺序执行。示例只读取可信目录 knowledge/ 中已有的文件,不开放任意用户路径上传。
import hashlibfrom pathlib import Pathfrom langchain_core.documents import Documentfrom langchain_community.document_loaders import ( TextLoader, Docx2txtLoader, PyPDFLoader,)ROOT = Path("knowledge").resolve()defload_file(relative_path: str) -> list[Document]: path = (ROOT / relative_path).resolve()ifnot path.is_relative_to(ROOT) ornot path.is_file():raise ValueError("文件不存在,或超出知识目录") loaders = {".txt": lambda: TextLoader(str(path), encoding="utf-8"),".docx": lambda: Docx2txtLoader(str(path)),".pdf": lambda: PyPDFLoader(str(path), mode="page"), }if path.suffix.lower() notin loaders:raise ValueError("不支持的文件格式")with path.open("rb") as stream: version = hashlib.file_digest(stream, "sha256").hexdigest() docs = loaders[path.suffix.lower()]().load()ifnot docs:raise ValueError("未提取到任何文档")for doc in docs: text = doc.page_content.strip()ifnot text or"\ufffd"in text:raise ValueError("发现空白或疑似解码异常,转人工检查") doc.page_content = text doc.metadata.update({"source": path.relative_to(ROOT).as_posix(),"file_version": version, })return docsdocuments = load_file("runbook.txt")print(len(documents), documents[0].metadata)这段代码保留加载器提供的页信息,同时统一来源和版本。文件摘要用于识别字节变化,不代表业务版本号;即使只修改了文件内部的作者信息或排版,也可能触发重新处理。生产系统可以同时记录业务版本与抽取文本摘要。
空白页为什么直接报错?这里采取严格验收策略,避免静默漏页。真实手册可能有封面背页,可以明确列出允许忽略的页,并留下记录,而不是把所有空结果一律过滤后宣称解析成功。
三、PDF 能打开,为什么提取不到正文?
PDF 更接近页面展示格式,不等同于结构化文本文件。扫描件可能只有图片;双栏布局、合并单元格和绘图文字,也可能无法按预期还原。pypdf 本身不是 OCR 工具。文本提取边界
flowchart LR A[原始文件] --> B[格式解析] B --> C{正文是否可用} C -->|是| D[补充来源与版本] C -->|否| E[OCR或人工复核] D --> F[切分与索引]不要把“有几个字符”当作可用标准。扫描手册可能只提取出页码,看起来不为空,操作步骤却完全缺失。应抽查首页、中间页、表格页,再核对关键标题和命令是否存在。
页码也有两套语义:加载器的 page 常是从零开始的物理页索引,手册印刷页码可能从正文才开始。展示引用时可以给索引加一,但不要把它冒充文档印刷页码;有 page_label 时应结合实际结果确认。
四、网页返回 200,不代表抓到了知识
网页先要拿到正确 HTML,再从中提取正文。下面只演示单个已授权、服务端返回正文的页面;请把域名、路径和正文标签换成自己的真实配置,不要批量抓取未知站点。
import bs4from urllib.parse import urlsplitfrom langchain_community.document_loaders import WebBaseLoaderdefload_page(url: str, allowed_host: str) -> list[Document]: parts = urlsplit(url)if parts.scheme != "https"or parts.hostname != allowed_host:raise ValueError("网页不在允许范围") loader = WebBaseLoader( web_paths=(url,), bs_kwargs={"parse_only": bs4.SoupStrainer("main")}, bs_get_text_kwargs={"separator": "\n", "strip": True}, requests_kwargs={"timeout": 15, "allow_redirects": False}, raise_for_status=True, header_template={"User-Agent": "KnowledgeIngest/1.0"}, ) docs = loader.load()ifnot docs orany(not d.page_content.strip() for d in docs):raise ValueError("未找到正文,检查选择器或页面渲染方式")return docsmain 是示意选择器,不是通用规则。需要先看页面结构,再选正文区域。仅靠浏览器执行 JavaScript 才出现的内容,不会因为更换 BeautifulSoup 选择器就自动产生。WebBaseLoader 官方实现
这里关闭重定向,是为了不在检查域名后又跳去其他地址。但域名白名单仍不等于完整 SSRF 防护,外部采集还需要网络出口限制、DNS/IP 校验及访问授权。加载器解决格式问题,不负责代替安全网关。
五、lazy_load() 能解决所有内存问题吗?
不能。它让应用逐个消费文档,但底层解析器仍可能先读取完整文件或构建解析对象。list(loader.lazy_load()) 又会把结果全部收集回来,并不会因为方法名有 lazy 就变成低内存流程。
大规模导入应分批处理、限制单文件大小,并把失败文件单独登记。不要在循环末尾一次性提交所有内容,也不要捕获所有异常后返回空列表,否则运维人员无法区分“没有知识”和“导入失败”。
加载过程还要区分三类状态:等待处理、处理成功、处理失败。失败记录至少保留来源、解析器版本、错误类别和重试次数。日志尽量不输出整篇正文,尤其是含内部账号、地址或业务数据的文件。
对于本地文件,也不能只按扩展名判断可信程度。示例的路径限制只是最小边界,接入用户上传时还应检查实际格式、解压大小及解析资源消耗。Word 文档属于压缩容器,几十 KB 的上传体积并不能证明解析后的资源开销也小。
六、入库之前,先做一次可追溯验收
加载阶段不需要解决所有问题,但必须把问题暴露出来。对相同格式建立小型回归样本,包含中文、表格、空页和异常编码;升级解析器后先比较正文和元数据,再重跑整个知识库。
如何抽查一张参数表?不要只搜索列名是否出现。可以准备一个“参数默认值是多少”的问题,同时核对参数名、默认值和适用条件是否仍然绑定。三列文字都在,顺序错了,模型同样可能把别行的默认值拿过来回答。
遇到这类问题,优先把表格解析成带列名的行记录,或保留原始表格供专门的查询流程使用。没有可靠的表格结构时,应标记为待处理内容,而不是把扁平文本包装成已经验证的知识。
另外,来源标识、内容摘要和权限范围不能混用。文件移动不一定意味着知识变化,正文相同也不意味着两份文件有相同的访问权限。去重时只比较内容摘要,可能把一个部门的内部资料并到另一个部门的公开资料下。
比较稳妥的做法是先在同一授权范围内识别重复,再由稳定文档标识串起来源与版本;公开引用只展示读者有权看到的信息,不要直接泄露服务器上的绝对路径。这样后续更新、删除和引用回查才有一致的依据。
如果原文已经丢失,换一个更强的生成模型能补回来吗? 它只能猜,无法恢复从未提供的事实。先保证知识正确进入系统,再讨论如何切得更合适,才是可排查的 RAG 流程。















































































