游乐游手机版
首页/AI教程/文章详情

RAG实战第一步:多格式文档加载与文本预处理

时间:2026-06-09 16:01
文档预处理不当会导致RAG系统检索结果不稳定,如PDF表格解析出两个版本。预处理目标包括格式统一、内容干净、语义完整切分及元数据保留。处理流程分为加载、清洗、分割三步,需去除噪声字符、规范空白,并以段落或句子为边界切分,确保语义连贯。

先从一个真实案例说起。某技术团队在搭建RAG系统时,遇到一个颇为棘手的问题:代码完全一致,运行结果却忽好忽坏,有时能准确命中目标,有时关键数据就这样悄然流失。

RAG 第一步:多格式文档加载与文本预处理实战

经过层层排查,问题根源竟然出在文档本身:PDF里的一张表格,在解析过程中被同时生成了两个版本——一份是结构清晰的表格数据,另一份则是被打散成文本的乱码信息。进行向量检索时,两个版本都被召回,大模型根本无法判断该信任哪一个,结果自然时而正确、时而出错。

这就是文档预处理不到位所引发的典型后果。那么,文档预处理究竟要实现哪些目标?概括来说,就是以下四点:

目标说明错误示例
格式统一将多种格式的文档统一转换为纯文本PDF 中的表格被乱码化
内容干净去除噪声字符、规范空白符保留页眉页脚、乱码字符
语义完整切分时不破坏语义边界在句子中间截断
元数据保留记录来源、页码等信息回答时无法溯源

而根据AWS的实践指南,RAG文档处理所面临的挑战远比想象中复杂:缺少清晰的章节标题,导致内容上下文难以识别;术语不一致、缩写未定义,模型理解时容易产生偏差;重复内容和冗长描述不仅浪费token,还会干扰检索效果;PDF中的图片被截断,关键信息直接丢失;同一个术语在不同位置含义不同,回答的准确性自然难以保障。

RAG 文档处理的核心要求

整个处理流程可以归纳为三个步骤:加载→清洗→分割。看似简单,但每个环节都有其技术要点。

flowchart LRsubgraph 文档预处理流水线direction TBsubgraph Row1 [ ]direction LRA[1.文档加载
Loading] B[2.文本清洗
Cleaning]C[3.文本分割
Splitting]endsubgraph Row2 [ ]direction LRA1[统一文本格式
保留元数据]B1[去除噪声字符
规范空白换行]C1[语义边界切分
控制块大小]end A -.- A1B -.- B1C -.- C1end

具体而言,核心要求主要有三点:

1. 格式统一

PDF、Markdown、Word……不同格式的文档,必须首先转换成统一的纯文本格式,才能进入后续流程。转换后的结果大致如下:

// 统一后的文本格式示例 { "content": "这是文档的纯文本内容...", "metadata": { "source": "technical_guide.pdf", "page": 42, "title": "第三章:核心概念" } }

2. 内容干净

页眉页脚、特殊字符、多余空白、乱码字符……这些对检索没有帮助的噪声信息,必须彻底清理干净。

噪声类型示例处理方式
页眉页脚"机密文档·第3页"正则匹配删除
特殊字符\u0000\ufffd替换或删除
多余空白连续空格、空行过多规范化处理
乱码字符编码错误导致过滤或修复

3. 语义完整

在对文档进行切分时,务必确保每个片段在语义上相对完整。例如,“闭包是指函数能够记住并访问它的词法作用域”这句话,不能从“词法作”这个位置截断,否则上下文就断裂了。合理的切分应当以段落、句子为自然边界。

// ❌ 错误:在语义边界外截断 const badChunk = "闭包是指函数能够记住并访问它的词法作"; // 句子被截断 // ✅ 正确:保持语义完整 const goodChunk = "闭包是指函数能够记住并访问它的词法作用域,即使这个函数在它的词法作用域之外执行。";

常用文档加载器使用详解

在LangChain中,前端开发者最熟悉的几种文档格式,加载起来并不复杂。

格式加载器前端应用场景
TXTTextLoader日志文件、配置文件
MarkdownUnstructuredMarkdownLoader技术文档、README
CSVCSVLoader数据导出、报表
PDFPDFLoader用户手册、合同文档

实际开发时,可以编写一个函数,根据文件扩展名自动匹配合适的加载器,这样调用起来更加高效便捷。

// src/loaders/index.ts import { TextLoader } from "@langchain/classic/document_loaders/fs/text"; // txt import { CSVLoader } from "@langchain/community/document_loaders/fs/csv"; // csv import { UnstructuredLoader } from "@langchain/community/document_loaders/fs/unstructured"; // md / 通用 // 1. 加载 TXT 文件 async function loadTxtFile(filePath: string) { const loader = new TextLoader(filePath); const docs = await loader.load(); return docs; } // 2. 加载 Markdown 文件(保留标题结构) async function loadMarkdownFile(filePath: string) { const loader = new UnstructuredLoader(filePath); const docs = await loader.load(); // Markdown 的标题层级会被保留在 metadata 中 return docs; } // 3. 加载 CSV 文件 async function loadCsvFile(filePath: string) { const loader = new CSVLoader(filePath); const docs = await loader.load(); // 每行 CSV 变成一个 Document,列名存入 metadata return docs; } // 4. 根据文件扩展名自动选择加载器 export async function loadDocument(filePath: string) { const ext = filePath.split('.').pop()?.toLowerCase(); switch (ext) { case 'txt': return loadTxtFile(filePath); case 'md': return loadMarkdownFile(filePath); case 'csv': return loadCsvFile(filePath); default: throw new Error(`不支持的文件格式: ${ext}`); } }

这里有一个容易被忽略的关键点:加载器会自动提取文档的元数据。例如来源文件名、PDF的页码、TXT的行号、Markdown的标题等。这些元数据是后续溯源的重要依据,务必妥善保留。

// 加载后的 Document 结构示例 { pageContent: "文档的实际文本内容...", metadata: { source: "technical_guide.pdf", // 来源文件 page: 42, // 页码(PDF) line: 15, // 行号(TXT) title: "核心概念" // 标题(Markdown) } }

文本清洗与预处理方案

在文本清洗环节,每个项目遇到的“脏数据”类型可能各不相同,但需要处理的主要类别大致有以下几种:

清洗内容优化前优化后
特殊字符function test(){console.log("hello")}function test(){console.log("hello")}
多余空白"闭包是Ja vaScript的核心""闭包是 Ja vaScript 的核心"
不换行空格hellou00A0worldhello world
控制字符Hellou0000WorldHelloWorld
编码问题effectedeffected

下面是一个功能较为完备的清洗函数实现,可以应对绝大多数常见场景:

// src/cleaners/text-cleaner.ts interface CleanOptions { removeSpecialChars?: boolean; // 移除特殊控制字符 normalizeWhitespace?: boolean; // 规范化空白字符 removeEmptyLines?: boolean; // 移除空行 trimLines?: boolean; // 每行首尾去空格 maxLineLength?: number; // 单行最大长度 } const defaultOptions: CleanOptions = { removeSpecialChars: true, normalizeWhitespace: true, removeEmptyLines: true, trimLines: true, maxLineLength: 1000, }; /** * 清洗文本内容 * @param text 原始文本 * @param options 清洗选项 * @returns 清洗后的文本 */ export function cleanText(text: string, options: CleanOptions = defaultOptions): string { let cleaned = text; // 1. 移除特殊控制字符(保留换行和制表符) if (options.removeSpecialChars) { cleaned = cleaned.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); // 替换不换行空格为普通空格 cleaned = cleaned.replace(/\u00A0/g, ' '); // 替换零宽字符 cleaned = cleaned.replace(/[\u200B-\u200D\uFEFF]/g, ''); } // 2. 规范化空白字符 if (options.normalizeWhitespace) { // 连续空格 → 单个空格 cleaned = cleaned.replace(/[ \t]+/g, ' '); // 连续换行 → 最多两个 cleaned = cleaned.replace(/\n{3,}/g, '\n\n'); } // 3. 每行首尾去空格 if (options.trimLines) { cleaned = cleaned.split('\n').map(line => line.trim()).join('\n'); } // 4. 移除空行 if (options.removeEmptyLines) { cleaned = cleaned.split('\n').filter(line => line.length > 0).join('\n'); } // 5. 截断过长的行 if (options.maxLineLength) { cleaned = cleaned.split('\n').map(line => line.length > (options.maxLineLength as number) ? line.slice(0, options.maxLineLength) + '...' : line ).join('\n'); } return cleaned; } /** * 批量清洗文档数组 */ export function cleanDocuments(docs: any[], options?: CleanOptions): any[] { return docs.map(doc => ({ ...doc, pageContent: cleanText(doc.pageContent, options), })); }

前端实现文档批量上传与读取

对于前端应用来说,RAG流程的起点就是用户上传文档。这里通过一个配合File API即可实现。下面是一个功能较为完整的Vue组件,支持多文件上传、状态管理(待处理、处理中、已完成、失败)以及简单的模拟清洗操作。

// src/components/DocumentUpload.vue