上一篇(demo-20)搭建了完整的 RAG 流程,其中第一步是「加载文档」。真实项目里的资料不会只有 Markdown,还会有纯文本、CSV、JSON、PDF 等格式。
这一篇专门讲 Loader:它们如何把不同格式统一成 Document,以及 TextLoader、CSVLoader、JSONLoader、PDFLoader 和 Markdown 文档的常见用法。
代码基于 JavaScript 版
langchainv1。TextLoader / JSONLoader 来自@langchain/classic,CSVLoader / PDFLoader 来自@langchain/community。
一、Loader 在 RAG 中的位置
RAG 的完整链路是:
Loader -> Document[] -> TextSplitter -> Embedding -> VectorStore -> Retriever -> LLM
Loader 只负责第一步:把文件读成统一的 Document。
{
pageContent: "文档正文……",
metadata: {
source: "data/notes.txt",
line: 1,
loc: { pageNumber: 1 },
...
}
}
无论原始格式是什么,Loader 的输出都是 Document[]。后续的切分、向量化和检索不需要关心文件原本是 CSV 还是 PDF。
所有 Loader 的调用方式也一致:
const documents = await loader.load();
常用 Loader 一览
| 格式 | Loader | 包 | 默认粒度 | 主要 metadata |
|---|---|---|---|---|
| 纯文本 | TextLoader | @langchain/classic | 一个文件一个 Document | source |
| CSV | CSVLoader | @langchain/community | 一行一个 Document | source、line |
| JSON | JSONLoader | @langchain/classic | 一个字符串一个 Document | source、line |
PDFLoader | @langchain/community | 一页一个 Document | source、pdf、loc.pageNumber | |
| Markdown | 无独立 Loader | — | 建议 TextLoader + MarkdownTextSplitter | source |
Markdown 的说明:JavaScript 版 LangChain 没有内置的
MarkdownLoader。Markdown 本质是文本,日常用TextLoader载入,再用MarkdownTextSplitter按标题和段落切分即可。如果需要解析复杂文档结构,可以使用依赖 Unstructured 服务的UnstructuredLoader。
二、环境准备
安装 Loader 和相关依赖:
npm install @langchain/classic @langchain/community \
@langchain/textsplitters pdf-parse d3-dsv
各依赖的作用:
| 依赖 | 作用 |
|---|---|
@langchain/classic | TextLoader、JSONLoader |
@langchain/community | CSVLoader、PDFLoader |
@langchain/textsplitters | MarkdownTextSplitter |
pdf-parse | PDFLoader 解析 PDF 的底层依赖 |
d3-dsv | CSVLoader 解析 CSV 的底层依赖 |
本 demo 使用的示例文件:
| 文件 | 内容 |
|---|---|
data/notes.txt | LangChain 学习笔记 |
data/users.csv | 用户表格 |
data/faq.json | 产品 FAQ |
data/manual.pdf | 两页的产品手册 |
data/handbook.md | Markdown 使用手册 |
运行:
node demo-21/index.js
这个 demo 不需要调用大模型,也不消耗 API Token,只演示 Loader 的输出。
三、TextLoader:纯文本
TextLoader 是最简单的 Loader:把整个文件读成一个 Document。
import { TextLoader } from "@langchain/classic/document_loaders/fs/text";
const loader = new TextLoader("data/notes.txt");
const docs = await loader.load();
console.log(docs.length); // 1
console.log(docs[0].metadata);
// { source: "data/notes.txt" }
console.log(docs[0].pageContent);
本 demo 的实际输出:
Document 数量:1
[1] source=notes.txt
LangChain 学习笔记 LangChain 是一个用于构建大模型应用的开源框架。
它把模型调用、提示词、工具、检索和记忆抽象成可组合的组件。
RAG 分为离线索引和在线生成两个阶段……
特点:
- 一个文件 = 一个 Document;
- 不保留段落、标题等结构;
metadata只有source;- 适合
.txt、.log,也可以直接读.md原始文本; - 大文件还需要交给 TextSplitter 切分。
TextLoader 也支持传入 Blob,浏览器或上传场景可以直接用:
const loader = new TextLoader(new Blob(["你好,LangChain"]));
四、CSVLoader:表格数据
CSVLoader 把表格的每一行转换成一个 Document。
import { CSVLoader } from "@langchain/community/document_loaders/fs/csv";
const loader = new CSVLoader("data/users.csv");
const docs = await loader.load();
示例 CSV:
id,name,city,interest
1,小明,杭州,RAG
2,小红,上海,Agent
3,小刚,深圳,长期记忆
默认输出:
Document 数量:4
[1] source=users.csv,line=1
id: 1
name: 小明
city: 杭州
interest: RAG
[2] source=users.csv,line=2
id: 2
name: 小红
...
每一列会写成 列名: 值 的形式,metadata.line 记录行号,方便定位。
只取某一列
如果只关心某一列,可以把这一列作为 pageContent:
const loader = new CSVLoader("data/users.csv", { column: "name" });
const docs = await loader.load();
console.log(docs.map((doc) => doc.pageContent));
// ["小明", "小红", "小刚", "小美"]
其它选项
new CSVLoader("data/users.csv", {
column: "interest",
separator: ",",
});
column:指定内容列;separator:自定义分隔符,默认逗号。
注意:
- CSVLoader 假设第一行是表头;
- 字段里本身包含逗号时,需要标准 CSV 引号转义;
- 大文件会一次性读入内存,超大 CSV 建议流式处理或先入库;
- 中文 CSV 注意文件编码,建议统一使用 UTF-8。
五、JSONLoader:结构化 JSON
JSONLoader 默认会递归提取 JSON 中所有字符串,每个字符串变成一个 Document。
import { JSONLoader } from "@langchain/classic/document_loaders/fs/json";
const loader = new JSONLoader("data/faq.json");
const docs = await loader.load();
示例 JSON:
{
"product": "星尘笔记",
"faqs": [
{
"question": "如何导出笔记?",
"answer": "在设置页面选择「导出」,支持 Markdown 和 PDF 两种格式。"
},
{
"question": "是否支持多人协作?",
"answer": "专业版支持最多 20 人同时编辑,并且保留历史版本。"
}
]
}
不传 pointer 时,输出 8 个 Document,包括产品名、问题、答案和 meta 里的字符串:
[1] 星尘笔记
[2] 如何导出笔记?
[3] 在设置页面选择「导出」,支持 Markdown 和 PDF 两种格式。
...
用 JSON Pointer 选择子树
更常见的做法是只取需要的部分:
const loader = new JSONLoader("data/faq.json", "/faqs");
const docs = await loader.load();
输出 6 个 Document,只包含两组的 question 和 answer,不再包含 product 和 meta。
精确到字段
可以传数组,指定多个 JSON Pointer:
const loader = new JSONLoader("data/faq.json", [
"/faqs/0/question",
"/faqs/1/question",
]);
JSON Pointer 的规则:
- 用
/分隔层级; - 数组用下标,例如
/faqs/0/answer; - 不支持通配符
*,需要逐个字段指定; - 如果 JSON 是数组,根路径可以用
/0表示第一个元素。
JSON Lines
如果文件是 JSON Lines(每行一个 JSON 对象),使用 JSONLinesLoader:
import { JSONLinesLoader } from "@langchain/classic/document_loaders/fs/json";
const loader = new JSONLinesLoader("data/events.jsonl", "/text");
注意:
- 无 pointer 时会提取所有字符串,文档数量可能很多;
- 结构化字段之间会失去关联,FAQ 的 question 和 answer 会变成两条独立 Document;
- 如果业务需要保留对象级关系,建议在加载后自己组装,或改用自定义 Loader。
六、PDFLoader:PDF 文档
PDFLoader 使用 pdf-parse 提取文本,默认一页一个 Document。
import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf";
const loader = new PDFLoader("data/manual.pdf");
const docs = await loader.load();
本 demo 的两页 PDF 输出:
Document 数量:2
[1] source=manual.pdf,pdf={...},loc={"pageNumber":1}
星尘笔记 PDF 手册 第一章 产品简介 星尘笔记是一款面向知识工作者的笔记工具……
[2] source=manual.pdf,pdf={...},loc={"pageNumber":2}
如果发现内容丢失,可以从历史版本中恢复。第八章 联系与支持……
metadata 里包含:
source:文件路径;pdf:PDF 版本、页数等元信息;loc.pageNumber:当前页码,做引用溯源时很有用。
合并为一页
如果不想按页拆分:
const loader = new PDFLoader("data/manual.pdf", {
splitPages: false,
});
const docs = await loader.load();
// Document 数量:1
常用选项:
new PDFLoader("data/manual.pdf", {
splitPages: true,
parsedItemSeparator: "\n",
});
splitPages:是否按页拆分,默认true;parsedItemSeparator:同一页内文本片段的连接符。
PDF 的常见坑
- 扫描件不能直接提取文字:需要 OCR(如 Tesseract、云 OCR);
- 复杂排版顺序可能错乱:双栏、表格、页眉页脚会干扰阅读顺序;
- 字体编码问题:某些 PDF 提取后可能出现兼容字符或乱码;
- 表格结构丢失:PDF 里的表格通常变成普通文本,需要额外解析;
- 页数很多时:按页拆分会产生大量小 Document,建议再做合并或切分。
生产环境如果 PDF 质量参差不齐,可以考虑 Unstructured、LlamaParse、云文档解析服务等更专业的方案。
七、Markdown 文档:TextLoader + MarkdownTextSplitter
JavaScript 版 LangChain 没有独立的 MarkdownLoader。推荐做法:
import { TextLoader } from "@langchain/classic/document_loaders/fs/text";
import { MarkdownTextSplitter } from "@langchain/textsplitters";
const docs = await new TextLoader("data/handbook.md").load();
const splitter = new MarkdownTextSplitter({
chunkSize: 120,
chunkOverlap: 20,
});
const chunks = await splitter.splitDocuments(docs);
载入结果仍然是一个 Document:
Document 数量:1
[1] source=handbook.md
# 星尘笔记使用手册 星尘笔记是一款面向知识工作者的笔记工具……
按 Markdown 结构切分后:
Document 数量:4
[1] # 星尘笔记使用手册
星尘笔记是一款面向知识工作者的笔记工具……
[2] ## 快速开始
1. 注册账号并创建工作区。
2. 点击「新建笔记」,输入标题和正文。
...
[3] ## 数据同步
云端同步默认开启……
[4] ## 常见问题
如果同步失败,先检查网络和登录状态……
MarkdownTextSplitter 会优先在标题、列表、段落等边界切分,尽量保持语义完整。metadata.source 会从原 Document 继承。
如果需要更复杂的结构解析
JavaScript 版没有专门的 Markdown Loader,但可以使用通用的 UnstructuredLoader:
import { UnstructuredLoader } from "@langchain/community/document_loaders/fs/unstructured";
const loader = new UnstructuredLoader("data/handbook.md", {
apiUrl: "http://localhost:8000",
});
const docs = await loader.load();
它依赖 Unstructured API 或本地 Unstructured 服务,适合需要解析复杂文档结构的场景。普通 Markdown 项目用 TextLoader + MarkdownTextSplitter 就足够了。
关于 frontmatter
如果 Markdown 顶部有 YAML frontmatter,TextLoader 会把它当作普通文本读入。需要把 frontmatter 变成 metadata 时,可以在加载后自己解析:
const [frontmatter, body] = rawText.split(/^---$/m);
或者使用专门的 frontmatter 解析库。Loader 本身不负责这件事。
八、把 Loader 输出统一交给后续流程
不同 Loader 的输出结构完全一致,可以直接汇总:
const allDocs = [
...textDocs,
...csvDocs,
...faqDocs,
...pdfDocs,
...markdownChunks,
];
然后进入同一条流水线:
Loader -> Document[] -> TextSplitter -> Embedding -> VectorStore
本 demo 的汇总结果:
总 Document 数:17
- notes.txt: 1
- users.csv: 4
- faq.json: 6
- manual.pdf: 2
- handbook.md: 4
不同格式的切分建议
| 格式 | 建议 |
|---|---|
| 纯文本 | 按段落或固定长度切分,保留重叠 |
| CSV | 一行一个 Document,通常不需要再切 |
| JSON | 用 pointer 选择业务对象,再按字段组装 |
| 按页加载,再按段落切分;注意页眉页脚 | |
| Markdown | MarkdownTextSplitter 按标题和段落切分 |
工程实践
- 保留 metadata:
source、页码、行号、标题路径,方便引用溯源; - 加载后先检查:空文档、乱码、重复内容、页眉页脚;
- 不要过早切分:先拿到完整 Document,再按格式选择切分策略;
- 批量加载:大量文件可以用
DirectoryLoader,或自己遍历目录; - 增量更新:记录文件哈希或更新时间,只重新索引变化的文件;
- 权限过滤:把租户 ID、可见范围写进 metadata,检索时过滤;
- 删除同步:源文件删除后,向量库里的对应文档也要删除。
九、Python 对应写法
Python 侧的 Loader 主要集中在 langchain_community.document_loaders:
from langchain_community.document_loaders import (
TextLoader,
CSVLoader,
JSONLoader,
PyPDFLoader,
)
# 纯文本
text_docs = TextLoader("data/notes.txt", encoding="utf-8").load()
# CSV:每行一个 Document
csv_docs = CSVLoader(
"data/users.csv",
csv_args={"delimiter": ","},
encoding="utf-8",
).load()
# JSON:用 jq 语法选择子树
json_docs = JSONLoader(
"data/faq.json",
jq_schema=".faqs[]",
text_content=False,
).load()
# PDF:通常一页一个 Document
pdf_docs = PyPDFLoader("data/manual.pdf").load()
Markdown 同样推荐 TextLoader + MarkdownTextSplitter:
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import MarkdownTextSplitter
docs = TextLoader("data/handbook.md", encoding="utf-8").load()
splitter = MarkdownTextSplitter(chunk_size=120, chunk_overlap=20)
chunks = splitter.split_documents(docs)
如果需要结构化的 Markdown 解析,可以用 UnstructuredMarkdownLoader:
from langchain_community.document_loaders import UnstructuredMarkdownLoader
docs = UnstructuredMarkdownLoader("data/handbook.md").load()
注意 Python 的 JSONLoader 使用 jq 语法(.faqs[]),JavaScript 版使用 JSON Pointer(/faqs),两者不要混用。
十、小结
- Loader 的作用:把不同格式统一成
Document { pageContent, metadata }。 - TextLoader:纯文本,一文件一 Document。
- CSVLoader:表格,一行一 Document,可用
column只取某列。 - JSONLoader:结构化 JSON,用 JSON Pointer 选择子树或字段。
- PDFLoader:按页提取文本,扫描件需要 OCR,复杂排版要额外处理。
- Markdown:没有独立 MarkdownLoader,推荐
TextLoader + MarkdownTextSplitter。 - 统一出口:所有 Loader 都返回
Document[],后续接切分、Embedding、向量库和 RAG。
一句话:Loader 不负责理解文档,它只负责把不同格式变成统一的 Document。