前言
书接上文 , 学习输入 ——prompt 、输出—— OutputParser
而现在 , 借着期末考后余波(后天还有场英语 , 不管了 🤡,一个礼拜没码文 、 撸码 , 已经十分饥渴 ) , 学学如何使用 langchain 框架加载文档Ducument Loader (加载器), 在此之前 , 我觉得首先需要认识一下 Ducument Loader 在哪里充当什么角色 ?
请看下面这张图 , 这是我认真绘制好的一张图 , 图片展现了文本到向量数据的过程 , 我们只关注红色部分“加载器” , 后面部分将在后期文章持续跟新 , 构建一个 RAG 的整体认识
从图中可以知道 ,加载器(Ducument Loader)是知识库的第一步 , 作用是将文档加载 , 之后抽象为统一的文档对象 Document , 为了直观的理解 , 我举一个我使用 ai 工具阅读文档的常用方法 , 安装完电脑版的豆包后 , 拖动文档到右下角 , 便可以进行问答
效果如下图 :
那么 ,我们就开始思考了 , 如何使用代码实现文档的加载 ?
发散思考🧐
首先文档格式种类繁多 , 诸如
- MarkDown
- CSV
- TXT
- ...
甚至数据来源不只是文档 , 其他数据源的数据也需要加载
- 数据库
- 网络爬虫
- 网页
- ...
上面分析了两个问题 , 数据格式多样性 , 数据来源多样性 , 对于这些多样性,采取分而治之 , 因地制宜 , 每种数据使用不同的 api 。
那么加载之后呢 ?
统一抽象👀
加载之后 , 如果数据还是如此多样性 , 便不好使用代码来统一所有的情况 , 于是 ,对所有的数据进行抽象,
抽象为 Document 对象
interface Document {
pageContent: string;
metadata: Record<string, any>;
}
//Ducument的TypeScript对象
pageContent:类型为字符串,表示文档的正文内容。metadata:类型为Record<string, any>,表示文档的元数据,键为字符串,值可以是任意类型。
补充:
Record 是 TypeScript 中的一个内置工具类型,用于构造对象类型。它接受两个类型参数:
- 第一个参数是键的类型(通常是字符串字面量类型或联合类型)。
- 第二个参数是值的类型。
打印看看
import { Document } from "langchain/document";
const test = new Document({ pageContent: "hello langchain", metadata: { source: "lange " } });
console.log(test)
分而治之😋
在官网如是说到 , 分为两大类 , 一起来玩玩
File loaders
我觉得有必要从全体看看 File loader , 以便后续查阅 ,我将挑选常用文档类型进行 demo
All document loaders 所有文档加载器
| Name 名称 | Description 描述 |
|---|---|
| Multiple individual files多个单独的文件 | This example goes over how to load data from multiple file paths. The... 这个示例介绍了如何从多个文件路径加载数据。 |
| ChatGPT filesChatGPT 文件 | This example goes over how to load conversations.json from your ChatG... 这个示例介绍了如何从您的 ChatG...加载 conversations.json 文件。 |
| CSV | This notebook provides a quick overview for getting started with 本笔记本提供了快速入门的概述 |
| DirectoryLoader目录加载器 | This notebook provides a quick overview for getting started with 本笔记本提供了快速入门的概述 |
| Docx filesDocx 文件 | The DocxLoader allows you to extract text data from Microsoft Word do... DocxLoader 允许您从 Microsoft Word 中提取文本数据... |
| EPUB filesEPUB 文件 | This example goes over how to load data from EPUB files. By default, ... 此示例介绍如何从 EPUB 文件中加载数据。默认情况下,... |
| JSON filesJSON 文件 | The JSON loader use JSON pointer to target keys in your JSON files yo... JSON 加载器使用 JSON 指针来定位您的 JSON 文件中的键... |
| JSONLines filesJSONLines 文件 | This example goes over how to load data from JSONLines or JSONL files... 此示例介绍了如何从 JSONLines 或 JSONL 文件中加载数据... |
| Notion markdown exportNotion Markdown 导出 | This example goes over how to load data from your Notion pages export... 这个示例介绍了如何从您的 Notion 页面导出中加载数据... |
| Open AI Whisper AudioOpen AI Whisper 音频 | Only available on Node.js. 仅适用于 Node.js。 |
| PDFLoaderPDF 加载器 | This notebook provides a quick overview for getting started with 本笔记本提供了快速入门的概述 |
| PPTX filesPPTX 文件 | This example goes over how to load data from PPTX files. By default, ... 此示例介绍如何从 PPTX 文件中加载数据。默认情况下,... |
| Subtitles字幕 | This example goes over how to load data from subtitle files. One docu... 此示例介绍如何从字幕文件中加载数据。一个文档... |
| TextLoader | This notebook provides a quick overview for getting started with 本笔记本提供了快速入门的概述 |
| Unstructured无结构 | This notebook provides a quick overview for getting started with 本笔记本提供了快速入门的概述 |
展示 pdf 、txt 的加载 , 就地取材🤡
TextLoader
在当前代码文件中 , 加载 data/poem.txt
import { TextLoader } from "langchain/document_loaders/fs/text";
const loader = new TextLoader("data/poem.txt");
const docs = await loader.load();
console.log(docs);
console.log(docs[0].pageContent);
//loder 加载后返回的是一个Document对象数组,可以通过下标访问其中的Document对象
console.log(docs.metadata);
[
Document {
pageContent: "辛苦遭逢起一经,干戈寥落四周星。山河破碎风飘絮,身世浮沉雨打萍。\r\n惶恐滩头说惶恐,零丁洋里叹零丁。人生自古谁无死?留取丹心照汗青。",
metadata: { source: "data/poem.txt" }
}
]
辛苦遭逢起一经,干戈寥落四周星。山河破碎风飘絮,身世浮沉雨打萍。
惶恐滩头说惶恐,零丁洋里叹零丁。人生自古谁无死?留取丹心照汗青。
undefined
上面的输出 ,说明了一下几点 :
- TextLoader 等加载器 , 输出的格式是
[
Document {
pageContent: "辛苦遭逢起一经,干戈寥落四周星。山河破碎风飘絮,身世浮沉雨打萍。\r\n惶恐滩头说惶恐,零丁洋里叹零丁。人生自古谁无死?留取丹心照汗青。",
metadata: { source: "data/poem.txt" }
}
]
- loder 加载后返回的是一个Document对象数组,可以通过下标访问其中的Document对象
PDFLoader
pdf 是最常用的了 , 很多的 ai 知识库都支持 , 如下 : 我使用 pdf 保存王国维的《人间词话》中我最喜欢的一段话 , 之后加载出来
import { PDFLoader } from "langchain/document_loaders/fs/pdf";
const loader = new PDFLoader("data/人间词话.pdf", { splitPages: false });
const pdf = await loader.load()
console.log(pdf);
输出 :
[
Document {
pageContent: "1\n" +
"人间词话\n" +
"王国维在《人间词话》中第二十六条的一段论述,让我们奉为经典。原文是:“古今之成大事业、\n" +
"大学问者,必经过三种之境界:‘昨夜⻄⻛凋碧树,独上高楼,望尽天涯路。’此第一境也。‘衣带\n" +
"渐宽终不悔,为伊消得人憔悴。’此第二境也。‘众里寻他千百度,回头蓦⻅,那人正在灯火阑珊\n" +
"处。’此第三境也。” ",
metadata: {
source: "data/人间词话.pdf",
pdf: {
version: "1.10.100",
info: {
PDFFormatVersion: "1.4",
IsAcroFormPresent: false,
IsXFAPresent: false,
Creator: "Chromium",
Producer: "Skia/PDF m93",
CreationDate: "D:20250109181030+00'00'",
ModDate: "D:20250109181030+00'00'"
},
metadata: null,
totalPages: 1
}
}
}
]
需要注意的是 : pdf 加载器中 , 打印出来 pdfs是一个 Document 数组,其中每一个 Document 对象对应了 pdf 中的一页,这是 PDFLoader 的默认行为。
我们可以使用配置splitPages: false关闭这个特性 , 特性如下 :
directoryLoader
这个是加载一个目录下的文档 ,
比如以下结构的 , 包含多种格式的文档
src/document_loaders/example_data/example/
├── example.json
├── example.jsonl
├── example.txt
└── example.csv
参考 : js.langchain.com/docs/integr…
ok , 来一次 demo 实战
我的目录下 : 有如下格式
我加载./data 目录下的所有文件 , 由于输出有限 , 我只保留了“.txt”和“人间词话“这个 pdf
import { DirectoryLoader } from "langchain/document_loaders/fs/directory";
import { TextLoader } from "langchain/document_loaders/fs/text";
import { PDFLoader } from "langchain/document_loaders/fs/pdf";
const loader = new DirectoryLoader(
"./data",
{
".pdf": (path) => new PDFLoader(path, { splitPages: false }),
".txt": (path) => new TextLoader(path),
}
);
const docs = await loader.load();
//console.log(docs);
const filteredDocs = docs.filter((doc) => doc.metadata.source.endsWith(".txt")||doc.metadata.source.includes("人间词话"));
console.log(filteredDocs);
这段代码里 , 也有很多思想 ,值得借鉴学习 :
策略模式
代码使用了策略模式(Strategy Pattern)。策略模式是一种行为设计模式,它允许在运行时选择算法或策略,并将这些算法封装在独立的类中,使得它们可以互换。
在这个例子中,DirectoryLoader 类根据文件扩展名选择不同的加载策略(.pdf 使用 PDFLoader,.txt 使用 TextLoader)。这些加载策略被封装在独立的函数中,并通过配置对象传递给 DirectoryLoader。
这种设计模式的优点是可以轻松地添加新的文件类型加载器,而无需修改现有的代码,从而提高了代码的扩展性和维护性。
关注点分离
它还实现了Separation of Concerns(Separation of Concerns),即 concerns(关注点)分离,将不同的关注点(如文件加载和数据处理)分离到不同的类中,使得代码更易于理解、维护和扩展。
输出如下 :
[
Document {
pageContent: "辛苦遭逢起一经,干戈寥落四周星。山河破碎风飘絮,身世浮沉雨打萍。\r\n惶恐滩头说惶恐,零丁洋里叹零丁。人生自古谁无死?留取丹心照汗青。",
metadata: {
source: "d:\lesson_hm\LangChainJs\05_Embedding\data\poem.txt"
}
},
Document {
pageContent: "1\n" +
"人间词话\n" +
"王国维在《人间词话》中第二十六条的一段论述,让我们奉为经典。原文是:“古今之成大事业、\n" +
"大学问者,必经过三种之境界:‘昨夜⻄⻛凋碧树,独上高楼,望尽天涯路。’此第一境也。‘衣带\n" +
"渐宽终不悔,为伊消得人憔悴。’此第二境也。‘众里寻他千百度,回头蓦⻅,那人正在灯火阑珊\n" +
"处。’此第三境也。” ",
metadata: {
source: "d:\lesson_hm\LangChainJs\05_Embedding\data\人间词话.pdf",
pdf: {
version: "1.10.100",
info: {
PDFFormatVersion: "1.4",
IsAcroFormPresent: false,
IsXFAPresent: false,
Creator: "Chromium",
Producer: "Skia/PDF m93",
CreationDate: "D:20250109181030+00'00'",
ModDate: "D:20250109181030+00'00'"
},
metadata: null,
totalPages: 1
}
}
},
Document {
pageContent: "\r\n" +
"日本人之称我中国也,一则曰老大帝国,再则曰老大帝国。是语也,盖袭译欧西人之言也。呜呼!我中国其果老大矣乎?梁启超曰:恶!是何言!是何言!吾心目中有一少年中国在。\r\n" +
"\r\n" +
"\r\n" +
"\r\n" +
"欲言国之老少,请先言人之老少。老年人常思既往,少年人常思将来。惟思既往也,故生留恋心;惟思将来也,故生希望心。惟留恋也,故保守;惟希望也,故进取。惟保守也,故永旧;惟进取也,故日新。惟思既往也,事事皆其所已经者,故惟知照例;惟思将来也,事事皆其所未经者,故常敢破格。老年人常多忧虑,少年人常好行乐。惟多忧也,故灰心;惟行乐也,故盛气。惟灰心也,故怯懦;惟盛气也,故豪壮。惟怯懦也,故苟且;惟豪壮也,故冒险。惟苟且也,故能灭世界;惟冒险也,故能造世界。老年人常厌事,少年人常喜事。惟厌事也,故常觉一切事无可为者;惟好事也,故常觉一切事无不可为者。老年人如夕照,少年人如朝阳;老年人如瘠牛,少年人如乳虎;老年人如僧,少年人如侠;老年人如字典,少年人如戏文;老年人如鸦片烟,少年人如泼兰地酒;老年人如别行星之陨石,少年人如大洋海之珊瑚岛;老年人如埃及沙漠之金字塔,少年人如西伯利亚之铁路;老年人如秋后之柳,少年人如春前之草;老年人如死海之潴为泽,少年人如长江之初发源。此老年人与少年人性格不同之大略也。任公曰:人固有之,国亦宜然。\r\n" +
"\r\n" +
"\r\n" +
"\r\n" +
"梁启超曰:伤哉,老大也!浔阳江头琵琶妇,当明月绕船,枫叶瑟瑟,衾寒于铁,似梦非梦之时,追想洛阳尘中春花秋月之佳趣。西宫南内,白发宫娥,一灯如穗,三五对坐,谈开元、天宝间遗事,谱《霓裳羽衣曲》。青门种瓜人,左对孺人,顾弄孺子,忆侯门似海珠履杂遝之盛事。拿破仑之流于厄蔑,阿剌飞之幽于锡兰,与三两监守吏,或过访之好事者,道当年短刀匹马驰骋中原,席卷欧洲,血战海楼,一声叱咤,万国震恐之丰功伟烈,初而拍案,继而抚髀,终而揽镜。呜呼,面皴齿尽,白发盈把,颓然老矣!若是者,舍幽郁之外无心事,舍悲惨之处无天地;舍颓唐之外无日月,舍叹息之外无音声;舍待死之外无事业。美人豪杰且然,而况寻常碌碌者耶?生平亲友,皆在墟墓;起居饮食,待命于人。今日且过,遑知他日?今年且过,遑恤明年?普天下灰心短气之事,未有甚于老大者。于此人也,而欲望以拏云之手段,回天之事功,挟山超海之意气,能乎不能?呜呼!我中国其果老大矣乎?立乎今日以指畴昔,唐虞三代,若何之郅治;秦皇汉武,若何之雄杰;汉唐来之文学,若何之隆盛;康乾间之武功,若何之烜赫。历史家所铺叙,词章家所讴歌,何一非我国民少年时代良辰美景、赏心乐事之陈迹哉!而今颓然老矣!昨日割五城,明日割十城,处处雀鼠尽,夜夜鸡犬惊。十八省之土地财产,已为人怀中之肉;四百兆之父兄子弟,已为人注籍之奴,岂所谓 “老大嫁作商人妇” 者耶?呜呼!凭君莫话当年事,憔悴韶光不忍看!楚囚相对,岌岌顾影,人命危浅,朝不虑夕。国为待死之国,一国之民为待死之民。万事付之奈何,一切凭人作弄,亦何足怪!\r\n" +
"\r\n" +
"\r\n" +
"\r\n" +
"任公曰:我中国其果老大矣乎?是今日全地球之一大问题也。如其老大也,则是中国为过去之国,即地球上昔本有此国,而今渐澌灭,他日之命运殆将尽也。如其非老大也,则是中国为未来之国,即地球上昔未现此国,而今渐发达,他日之前程且方长也。欲断今日之中国为老大耶?为少年耶?则不可不先明 “国” 字之意义。夫国也者,何物也?有土地,有人民,以居于其土地之人民,而治其所居之土地之事,自制法律而自守之;有主权,有服从,人人皆主权者,人人皆服从者。夫如是,斯谓之完全成立之国,地球上之有完全成立之国也,自百年以来也。完全成立者,壮年之事也。未能完全成立而渐进于完全成立者,少年之事也。故吾得一言以断之曰:欧洲列邦在今日为壮年国,而我中国在今日为少年国。夫古昔之中国者,虽有国之名,而未成国之形也。或为家族之国,或为酋长之国,或为诸侯封建之国,或为一王专制之国。虽种类不一,要之,其于国家之体质也,有其一部而缺其一部。正如婴儿自胚胎以迄成童,其身体之一二官支,先行长成,此外则全体虽粗具,然未能得其用也。故唐虞以前为胚胎时代,殷周之际为乳哺时代,由孔子而来至于今为童子时代。逐渐发达,而今乃始将入成童以上少年之界焉。其长成所以若是之迟者,则历代之民贼有窒其生机者也。譬犹童年多病,转类老态,或且疑其死期之将至焉,而不知皆由未完成未成立也。非过去之谓,而未来之谓也。且我中国畴昔,岂尝有国家哉?不过有朝廷耳!我黄帝子孙,聚族而居,立于此地球之上者既数千年,而问其国之为何名,则无有也。夫所谓唐、虞、夏、商、周、秦、汉、魏、晋、宋、齐、梁、陈、隋、唐、宋、元、明、清者,则皆朝名耳。朝也者,一家之私产也。国也者,人民之公产也。朝有朝之老少,国有国之老少。朝与国既异物,则不能以朝之老少而指为国之老少明矣。文、武、成、康,周朝之少年时代也。幽、厉、桓、赧,则其老年时代也。高、文、景、武,汉朝之少年时代也。元、平、桓、灵,则其老年时代也。自余历朝,莫不有之。凡此者谓为一朝廷之老也则可,谓为一国之老也则不可。一朝廷之老且死,犹一人之老且死也,于吾所谓中国者何与焉。然则,吾中国者,前此尚未出现于世界,而今乃始萌芽云尔。天地大矣,前途辽矣。美哉我少年中国乎!玛志尼者,意大利三杰之魁也。以国事被罪,逃窜异邦。乃创立一会,名曰 “少年意大利”。举国志士,云涌雾集以应之。卒乃光复旧物,使意大利为欧洲之一雄邦。夫意大利者,欧洲之第一老大国也。自罗马亡后,土地隶于教皇,政权归于奥国,殆所谓老而濒于死者矣。而得一玛志尼,且能举全国而少年之,况我中国之实为少年时代者耶!堂堂四百余州之国土,凛凛四百余兆之国民,岂遂无一玛志尼其人者!龚自珍氏之集有诗一章,题曰《能令公少年行》。吾尝爱读之,而有味乎其用意之所存。我国民而自谓其国之老大也,斯果老大矣;我国民而自知其国之少年也,斯乃少年矣。西谚有之曰:“有三岁之翁,有百岁之童。” 然则,国之老少,又无定形,而实随国民之心力以为消长者也。吾见乎玛志尼之能令国少年也,吾又见乎我国之官吏士民能令国老大也。吾为此惧!夫以如此壮丽浓郁翩翩绝世之少年中国,而使欧西日本人谓我为老大者,何也?则以握国权者皆老朽之人也。非哦几十年八股,非写几十年白折,非当几十年差,非捱几十年俸,非递几十年手本,非唱几十年喏,非磕几十年头,非请几十年安,则必不能得一官、进一职。其内任卿贰以上,外任监司以上者,百人之中,其五官不备者,殆九十六七人也。非眼盲则耳聋,非手颤则足跛,否则半身不遂也。彼其一身饮食步履视听言语,尚且不能自了,须三四人左右扶之捉之,乃能度日,于此而乃欲责之以国事,是何异立无数木偶而使治天下也!且彼辈者,自其少壮之时既已不知亚细亚、欧罗巴为何处地方,汉祖唐宗是那朝皇帝,犹嫌其顽钝腐败之未臻其极,又必搓磨之,陶冶之,待其脑髓已涸,血管已塞,气息奄奄,与鬼为邻之时,然后将我二万里山河,四万万人命,一举而界于其手。呜呼!老大帝国,诚哉其老大也!而彼辈者,积其数十年之八股、白折、当差、捱俸、手本、唱喏、磕头、请安,千辛万苦,千苦万辛,乃始得此红顶花翎之服色,中堂大人之名号,乃出其全副精神,竭其毕生力量,以保持之。如彼乞儿拾金一锭,虽轰雷盘旋其顶上,而两手犹紧抱其荷包,他事非所顾也,非所知也,非所闻也。于此而告之以亡国也,瓜分也,彼乌从而听之,乌从而信之!即使果亡矣,果分矣,而吾今年七十矣,八十矣,但求其一两年内,洋人不来,强盗不起,我已快活过了一世矣!若不得已,则割三头两省之土地奉申贺敬,以换我几个衙门;卖三几百万之人民作仆为奴,以赎我一条老命,有何不可?有何难办?呜呼!今之所谓老后、老臣、老将、老吏者,其修身齐家治国平天下之手段,皆具于是矣。西风一夜催人老,凋尽朱颜白尽头。使走无常当医生,携催命符以祝寿,嗟乎痛哉!以此为国,是安得不老且死,且吾恐其未及岁而殇也。\r\n" +
"\r\n" +
"\r\n" +
"\r\n" +
"任公曰:造成今日之老大中国者,则中国老朽之冤业也。制出将来之少年中国者,则中国少年之责任也。彼老朽者何足道,彼与此世界作别之日不远矣,而我少年乃新来而与世界为缘。如僦屋者然,彼明日将迁居他方,而我今日始入此室处。将迁居者,不爱护其窗栊,不洁治其庭庑,俗人恒情,亦何足怪!若我少年者,前程浩浩,后顾茫茫。中国而为牛为马为奴为隶,则烹脔鞭棰之惨酷,惟我少年当之。中国如称霸宇内,主盟地球,则指挥顾盼之尊荣,惟我少年享之。于彼气息奄奄与鬼为邻者何与焉?彼而漠然置之,犹可言也。我而漠然置之,不可言也。使举国之少年而果为少年也,则吾中国为未来之国,其进步未可量也。使举国之少年而亦为老大也,则吾中国为过去之国,其澌亡可翘足而待也。故今日之责任,不在他人,而全在我少年。少年智则国智,少年富则国富,少年强则国强,少年独立则国独立,少年自由则国自由,少年进步则国进步,少年胜于欧洲则国胜于欧洲,少年雄于地球则国雄于地球。红日初升,其道大光。河出伏流,一泻汪洋。潜龙腾渊,鳞爪飞扬。乳虎啸谷,百兽震惶。鹰隼试翼,风尘吸张。奇花初胎,矞矞皇皇。干将发硎,有作其芒。天戴其苍,地履其黄。纵有千古,横有八荒。前途似海,来日方长。美哉我少年中国,与天不老!壮哉我中国少年,与国无疆!",
metadata: {
source: "d:\lesson_hm\LangChainJs\05_Embedding\data\少年中国说.txt"
}
}
]
web loaders
如果说 ,之前的文本都是本地的 , 那么来一次网络数据源 玩玩呢👀
静态 html
如下图 , 我爬取 ,我掘金专栏的信息 , 指定爬取 main 标签中的信息
import "cheerio";
import { CheerioWebBaseLoader } from "langchain/document_loaders/web/cheerio";
const loader = new CheerioWebBaseLoader(
"https://juejin.cn/user/3806962499980916/columns",
{
selector: "main",
}
);
const docs = await loader.load();
console.log(docs);
console.log(docs[0].pageContent)
部分输出如下 :
接入搜索引擎
到 SerpApi 中获取 key
import { SerpAPILoader } from "langchain/document_loaders/web/serpapi";
import {load} from "dotenv";
const env = await load();
const apiKey = env["SERP_KEY"]
const question = "稀土掘金是什么 ?"
const loader = new SerpAPILoader({ q: question, apiKey });
const docs = await loader.load();
console.log({ docs });
搜索结果部分展示
总结
以上介绍些 langchain 加载数据的一些原理 , 以及一些常用方法的尝鲜 , 具体还需结合业务开发 ,
就码文到这里了 , 感觉输出有点过度 🤡 , 几天高强度的抢救 , 有些累了 , 不过值得我狂笑的是 : 之前为了研究些东西旷了水课"地球科学概论", 那位老师说要给我 平时分 0 分 ,今天看了下成绩 , 良好, 那一天我旷课射出的子弹 , 没有正中眉心 , 也没有落下 ,而是还在飞 , but who care ?! 我还会继续 , 继续花精力在喜欢的东西上🤡 , 想吃瓜看这篇 : 我知微风意 🤡| Vue.js手搓天气组件