Langchain.js | Ducument Loader👈| 文本这样加载😱

855 阅读20分钟

前言

书接上文 , 学习输入 ——prompt 、输出—— OutputParser

而现在 , 借着期末考后余波(后天还有场英语 , 不管了 🤡,一个礼拜没码文 、 撸码 , 已经十分饥渴 ) , 学学如何使用 langchain 框架加载文档Ducument Loader (加载器), 在此之前 , 我觉得首先需要认识一下 Ducument Loader 在哪里充当什么角色 ?

请看下面这张图 , 这是我认真绘制好的一张图 , 图片展现了文本到向量数据的过程 , 我们只关注红色部分“加载器” , 后面部分将在后期文章持续跟新 , 构建一个 RAG 的整体认识

从图中可以知道 ,加载器(Ducument Loader)是知识库的第一步 , 作用是将文档加载 , 之后抽象为统一的文档对象 Document , 为了直观的理解 , 我举一个我使用 ai 工具阅读文档的常用方法 , 安装完电脑版的豆包后 , 拖动文档到右下角 , 便可以进行问答

效果如下图 :

那么 ,我们就开始思考了 , 如何使用代码实现文档的加载 ?

发散思考🧐

首先文档格式种类繁多 , 诸如

  • PDF
  • 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 文件。
CSVThis 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... 此示例介绍如何从字幕文件中加载数据。一个文档...
TextLoaderThis 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手搓天气组件