文档预处理不当会导致RAG系统检索结果不稳定,如PDF表格解析出两个版本。预处理目标包括格式统一、内容干净、语义完整切分及元数据保留。处理流程分为加载、清洗、分割三步,需去除噪声字符、规范空白,并以段落或句子为边界切分,确保语义连贯。
先说一个真实的案例。某个团队在搭建RAG系统时,撞上了一个挺邪门的问题:代码明明一模一样,运行结果却时好时坏,有时候能精准命中,有时候关键数据就这么溜走了。

长期稳定更新的攒劲资源: >>>点此立即查看<<<
层层排查下来,问题居然出在文档本身:PDF里的一张表格,在解析时被同时生成了两个版本——一份是结构清晰的表格,另一份则是被打散成文本的乱码。向量检索时,两个版本都被召回,大模型哪分得清该信哪个,结果就是时而正确,时而抽风。
这就是RAG文档预处理没做到位的典型后果。那么,RAG文档处理到底要达成什么目标?说白了,就是四件事:
| 目标 | 说明 | 错误示例 |
|---|---|---|
| 格式统一 | 将多格式文档转为统一文本 | 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
具体来说,RAG文档预处理的核心要求有三点:
PDF、Markdown、Word……不同格式的文档,必须先转换成统一的纯文本格式,这样才能往后走。转换后的结果,大概长这样:
// 统一后的文本格式示例
{
"content": "这是文档的纯文本内容...",
"metadata": {
"source": "technical_guide.pdf",
"page": 42,
"title": "第三章:核心概念"
}
}
页眉页脚、特殊字符、多余空白、乱码字符……这些对检索没有帮助的噪声,必须清理干净。RAG文档处理中的文本清洗尤其重要。
| 噪声类型 | 示例 | 处理方式 |
|---|---|---|
| 页眉页脚 | "机密文档·第3页" | 正则匹配删除 |
| 特殊字符 | \u0000、\ufffd | 替换或删除 |
| 多余空白 | 连续空格、空行过多 | 规范化 |
| 乱码字符 | 编码错误导致的 | 过滤或修复 |
切分文档时,务必保证每个片段在语义上是相对完整的。比如“闭包是指函数能够记住并访问它的词法作用域”这句话,就不能在“词法作”这个地方截断,不然上下文就断了。好的切分,应该以段落、句子为自然边界。这是RAG文档分割的关键。
// 错误:在语义边界外截断
const badChunk = "闭包是指函数能够记住并访问它的词法作"; // 句子被截断
// 正确:保持语义完整
const goodChunk = "闭包是指函数能够记住并访问它的词法作用域,即使这个函数在它的词法作用域之外执行。";
LangChain 对前端开发最友好的几种格式,加载起来并不复杂。选择合适的RAG文档加载器是第一步。
| 格式 | 加载器 | 前端应用场景 |
|---|---|---|
| TXT | TextLoader | 日志文件、配置文件 |
| Markdown | UnstructuredMarkdownLoader | 技术文档、README |
| CSV | CSVLoader | 数据导出、报表 |
PDFLoader | 用户手册、合同文档 |
实际使用时,可以写一个函数,根据文件扩展名自动选择合适的加载器,这样调用起来更省心。这也是RAG文档加载的常见实践。
// 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的标题等。这些元数据是后续溯源的关键,一定要保留好。在RAG文档加载环节,元数据保留直接影响检索质量。
// 加载后的 Document 结构示例
{
pageContent: "文档的实际文本内容...",
metadata: {
source: "technical_guide.pdf", // 来源文件
page: 42, // 页码(PDF)
line: 15, // 行号(TXT)
title: "核心概念" // 标题(Markdown)
}
}
清洗这块,每个项目遇到的“脏数据”都不太一样,但RAG文档预处理需要处理的主要类型就那几种:
| 清洗内容 | 优化前 | 优化后 |
|---|---|---|
| 特殊字符 | function test(){console.log("hello")} | function test(){console.log("hello")} |
| 多余空白 | "闭包是Ja vaScript的核心" | "闭包是 Ja vaScript 的核心" |
| 不换行空格 | hellou00A0world | hello world |
| 控制字符 | Hellou0000World | HelloWorld |
| 编码问题 | effected | effected |
下面是一个比较完备的文本清洗函数实现,可以应对绝大多数RAG文档处理的场景:
// 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组件,支持多文件上传、状态管理(待处理、处理中、已完成、失败)以及简单的模拟清洗。这也是前端RAG文档处理的典型实现。
// src/components/DocumentUpload.vue
已选文件 ({{ files.length }})
配合一个预览组件,可以让用户直观地看到RAG文档处理清洗前后的变化:
// src/components/DocumentPreview.vue
{{ fileName }}
{{ displayContent }}
文本切分是影响检索质量的关键环节。参数设置得当,检索效果能上一个台阶;设置不当,结果就只能随缘了。RAG文档分割策略直接影响检索效果。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| chunk_size | 500-1000 | 每块字符数,过小丢失上下文,过大噪声增多 |
| chunk_overlap | 50-200 | 相邻块重叠,保证语义连续性 |
| separators | ["\n\n", "\n", "。", ","] | 优先按段落、句子边界切分 |
实际实现时,可以用LangChain的RecursiveCharacterTextSplitter,并针对中文场景自定义分隔符的优先级:
// src/splitters/text-splitter.ts
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
// 中文分隔符优先级(从高到低)
const CHINESE_SEPARATORS = [
"\n\n", // 段落分隔
"\n", // 换行
"。", // 句号
"!", // 感叹号
"?", // 问号
";", // 分号
",", // 逗号
" ", // 空格
"", // 字符级(最后手段)
];
interface SplitterConfig {
chunkSize: number; // 每块最大字符数
chunkOverlap: number; // 块间重叠字符数
}
const defaultConfig: SplitterConfig = {
chunkSize: 800,
chunkOverlap: 100,
};
export function createTextSplitter(config: SplitterConfig = defaultConfig) {
return new RecursiveCharacterTextSplitter({
chunkSize: config.chunkSize,
chunkOverlap: config.chunkOverlap,
separators: CHINESE_SEPARATORS,
});
}
/**
* 分割文档并过滤空块
*/
export async function splitDocuments(docs: any[], config?: SplitterConfig) {
const splitter = createTextSplitter(config);
const chunks = await splitter.splitDocuments(docs);
// 过滤空白块,避免污染检索结果
const validChunks = chunks.filter(chunk =>
chunk.pageContent && chunk.pageContent.trim().length > 0
);
console.log(` 文档分割完成: ${docs.length} 个文档 → ${validChunks.length} 个块`);
return validChunks;
}
针对不同类型的内容,RAG文档分割策略也需要调整:
| 文档类型 | chunk_size | chunk_overlap | 说明 |
|---|---|---|---|
| 技术文档 | 800-1000 | 100-150 | 代码示例需要更多上下文 |
| 自然语言文章 | 500-800 | 50-100 | 段落相对短小 |
| 结构化文档 | 300-500 | 30-50 | 表格、列表为主 |
| 对话记录 | 1000-1200 | 150-200 | 需要保留对话连贯性 |
把上面几个步骤串起来,就是一个完整的RAG文档处理流水线:
// src/pipeline/document-pipeline.ts
import { loadDocument } from '../loaders';
import { cleanDocuments } from '../cleaners/text-cleaner';
import { splitDocuments } from '../splitters/text-splitter';
import path from 'path';
import { fileURLToPath } from 'url';
interface PipelineResult {
chunks: any[];
stats: {
originalSize: number;
cleanedSize: number;
chunkCount: number;
processingTime: number;
};
}
export async function processDocument(filePath: string): Promise
实际跑一遍,RAG文档处理的效果还是很可观的:
| 维度 | 处理前 | 处理后 | 改善 |
|---|---|---|---|
| 文本长度 | 125,000 字符 | 89,000 字符 | 减少 28.8% |
| 特殊字符 | 47 个 | 0 个 | 100% 移除 |
| 空行 | 156 行 | 42 行 | 73% 减少 |
| 块数量 | - | 142 块 | chunk_size=800 |
1. PDF表格数据乱码或重复。 这是RAG文档处理中最头疼的问题之一。一张表格在向量库里可能同时存在“结构化”和“乱码版”两个版本,大模型根本无法判断该信哪个。解决方案是写一个启发式规则,识别那些看起来像表格的文本块——比如包含“Table data:”前缀或日期模式的,直接剔除。
2. 特殊字符导致检索失败。 某些嵌入模型对特殊字符非常敏感,碰到无法识别的字符就会生成无效向量,导致召回率为0。解决方式很简单:在文本清洗阶段,用一个字符集过滤器,只保留中英文、数字和常用符号。
3. 元数据丢失导致无法溯源。 用户问“答案来自哪里”,结果回答不上来,这显然不行。关键就是在整个RAG文档处理流程中,确保每个切分后的chunk都继承了原始文档的元数据,比如文件名、页码。LangChain的切割器默认就会做这件事。
4. 文档过大导致内存溢出。 如果遇到50MB以上的大PDF,直接全部加载到内存里肯定扛不住。这时候需要采用分块读取或流式处理的方式,分批进行。
这篇教程系统地梳理了RAG文档处理中文档加载与预处理的技术要点,从格式统一、文本清洗,到语义切分和元数据保留,每一步都有实践价值。希望这些内容能为正在折腾RAG的你,提供一些实实在在的参考。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述