夜雨聆风学习资料网

ARTICLE · 1300598

19-RAG文档加载精讲-从文件解析到可追溯的Document

19-RAG文档加载精讲-从文件解析到可追溯的Document
做知识库问答,最容易被一笔带过的步骤是什么?通常不是模型,而是“先把文档加载进来”。一行 load() 没报错,就继续切分、向量化,直到用户问起某个操作步骤,系统才开始答非所问。

回头检查,PDF 读到的是页眉,网页抓到的是导航,操作手册中的表格丢了列关系。文件读取成功,不等于知识提取成功。 本篇用运维手册这个常见场景,把加载阶段应该交付什么讲清楚。

一、加载器交付的,不只是一个字符串

LangChain 用 Document 承载正文和元数据。加载器负责把外部格式转成这个接口,后续切分器和向量库就不必分别理解 PDF、Word 与网页。加载器接口

字段
应该放什么
后续用途
page_content
可用于检索的正文
切分、向量化、生成上下文
metadata.source
稳定的来源标识
定位原文、展示引用
metadata.page
加载器提供的页索引
定位 PDF 页面
自定义版本字段
文件版本或内容摘要
判断是否需要重建索引

这里有一个边界:Document 是统一容器,不是质量认证。乱码、空白和登录提示也可以装进去。正文检查必须由应用补上,不能指望后面的 Embedding 模型自动纠正。

还有一个容易忽略的问题:元数据会自动参与语义检索吗? 通常不会。常见向量库适配器嵌入的是 page_content;标题如果只放在元数据里,需要明确决定是否拼入待嵌入文本。

二、先把四种来源分开处理

来源
本文使用的入口
需要额外检查
TXT
TextLoader
编码、换行、异常替换字符
DOCX
Docx2txtLoader
表格列关系、页眉页脚混入
PDF
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 docs

main 是示意选择器,不是通用规则。需要先看页面结构,再选正文区域。仅靠浏览器执行 JavaScript 才出现的内容,不会因为更换 BeautifulSoup 选择器就自动产生。WebBaseLoader 官方实现

这里关闭重定向,是为了不在检查域名后又跳去其他地址。但域名白名单仍不等于完整 SSRF 防护,外部采集还需要网络出口限制、DNS/IP 校验及访问授权。加载器解决格式问题,不负责代替安全网关。

五、lazy_load() 能解决所有内存问题吗?

不能。它让应用逐个消费文档,但底层解析器仍可能先读取完整文件或构建解析对象。list(loader.lazy_load()) 又会把结果全部收集回来,并不会因为方法名有 lazy 就变成低内存流程。

大规模导入应分批处理、限制单文件大小,并把失败文件单独登记。不要在循环末尾一次性提交所有内容,也不要捕获所有异常后返回空列表,否则运维人员无法区分“没有知识”和“导入失败”。

加载过程还要区分三类状态:等待处理、处理成功、处理失败。失败记录至少保留来源、解析器版本、错误类别和重试次数。日志尽量不输出整篇正文,尤其是含内部账号、地址或业务数据的文件。

对于本地文件,也不能只按扩展名判断可信程度。示例的路径限制只是最小边界,接入用户上传时还应检查实际格式、解压大小及解析资源消耗。Word 文档属于压缩容器,几十 KB 的上传体积并不能证明解析后的资源开销也小。

六、入库之前,先做一次可追溯验收

验收项
要问的问题
不通过时怎么办
内容完整
关键章节和表格还在吗
更换解析策略或复核
来源定位
能回到哪个文件、哪一页
补齐来源,不急着嵌入
版本识别
更新后能找到旧记录吗
建立稳定文档标识
权限边界
谁能查询这份知识
在服务端记录访问范围

加载阶段不需要解决所有问题,但必须把问题暴露出来。对相同格式建立小型回归样本,包含中文、表格、空页和异常编码;升级解析器后先比较正文和元数据,再重跑整个知识库。

如何抽查一张参数表?不要只搜索列名是否出现。可以准备一个“参数默认值是多少”的问题,同时核对参数名、默认值和适用条件是否仍然绑定。三列文字都在,顺序错了,模型同样可能把别行的默认值拿过来回答。

遇到这类问题,优先把表格解析成带列名的行记录,或保留原始表格供专门的查询流程使用。没有可靠的表格结构时,应标记为待处理内容,而不是把扁平文本包装成已经验证的知识。

另外,来源标识、内容摘要和权限范围不能混用。文件移动不一定意味着知识变化,正文相同也不意味着两份文件有相同的访问权限。去重时只比较内容摘要,可能把一个部门的内部资料并到另一个部门的公开资料下。

比较稳妥的做法是先在同一授权范围内识别重复,再由稳定文档标识串起来源与版本;公开引用只展示读者有权看到的信息,不要直接泄露服务器上的绝对路径。这样后续更新、删除和引用回查才有一致的依据。

如果原文已经丢失,换一个更强的生成模型能补回来吗? 它只能猜,无法恢复从未提供的事实。先保证知识正确进入系统,再讨论如何切得更合适,才是可排查的 RAG 流程。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料