头图

如何在 JavaScript 中为 RAG 应用分块文本

原文链接:How to Chunk Text in JavaScript for Your RAG Application
作者:Phil Nash
译者:倔强青铜三

前言

大家好,我是倔强青铜三。是一名热情的软件工程师,我热衷于分享和传播IT技术,致力于通过我的知识和技能推动技术交流与创新,欢迎关注我,微信公众号:倔强青铜三。欢迎点赞、收藏、关注,一键三连!!!

检索增强生成(RAG)应用程序以数据为起点,因此在开始构建时,您很可能会面临的第一个挑战是将数据塑造成适合与向量数据库大型语言模型 (LLM)一起使用的形状。在本文中,我们将探讨在 JavaScript 中处理文本数据的不同方法,探索如何将其拆分为块并为其准备用于 RAG 应用程序。

为什么分块很重要

通常,您需要将大量非结构化文本内容分解成较小的数据块。这些文本块被转换为向量嵌入,并存储在像Astra DB这样的向量数据库中。

与整个文档相比,较小的文本块包含较少的主题或想法,这意味着它们的嵌入将包含更集中的含义。这使得每个块更容易与传入的用户查询匹配,并且能够更准确地检索。当您改进检索时,您可以向 LLM 提供更少但更相关的令牌,从而创建更准确和有用的 RAG 系统。

如果您想深入了解文本分块背后的理论,可以阅读这篇介绍分块本质的文章,或者阅读 Unstructured 关于分块最佳实践的文章。

JavaScript 中的分块

让我们超越理论,看看 JavaScript 中分块数据的选项。我们将要研究的库有:llm-chunkLangChainLlamaIndexsemantic-chunking。您可以使用我制作的这个应用程序来尝试这些分块库。

对于更复杂的用例,我们还将研究使用 Unstructured API。接下来,我们从这些模块中最简单的开始。

llm-chunk

llm-chunk的描述是“超级简单易用的文本分块器”,确实如此!您可以通过 npm 安装它

npm install llm-chunk

它主要是一个函数chunk,并只需要几个选项。您可以选择生成的块的最大和最小大小,选择块之间的重叠大小,并选择按句子或段落分割文本的策略。

默认情况下,它会按段落分割文本,最大长度为 1,000 个字符,没有最小长度或重叠。

import { chunk } from 'llm-chunk';
const text = loadText(); // 获取要分割的文本
const chunks = chunk(text);

您可以选择按句子分割文本,更改每块的最大和最小字符数,或更改每块应重叠多少个字符。

const chunks = chunk(text, { minLength: 128, maxLength: 1024, overlap: 128 });

对于更复杂的用例,您可以选择解析一组分隔符。这些会变成正则表达式,并用作分割文本的基础,而不是仅仅按段落或句子分割。llm-chunk简单、快速,是您刚开始 RAG 之旅时的绝佳选择。

LangChain

LangChain不仅仅是文本分块器。它是一个库,可帮助您加载数据、拆分数据、嵌入数据、将数据存储在和从向量数据库(包括Astra DB)中检索数据,向 LLM 提供提示,等等。关于LangChain有很多值得探索的地方,但我们将重点放在文本分块上。

您可以通过以下命令安装 LangChain 文本分块器:

npm install @langchain/textsplitters

如果您使用的是主langchain 模块,则@langchain/textsplitters作为依赖项包含在内。

LangChain 包含三个主要的分块器类,分别是CharacterTextSplitterRecursiveCharacterTextSplitterTokenTextSplitter。接下来,我们看看它们各自的工作原理。

CharacterTextSplitter

这是 LangChain 提供的最简单的分块器。它按字符分割文档,然后将段落合并回去,直到它们达到所需的块大小,并按所需的字符数重叠块。

分割文本的默认字符是“\n\n”。这意味着它旨在初始按段落分割文本,但您也可以提供要按字符分割的字符。这是使用 LangChain 分块器的示例:

import { CharacterTextSplitter } from "@langchain/textsplitters";
const text = loadText(); // 获取要分割的文本
const splitter = new CharacterTextSplitter({
  chunkSize: 1024,
  chunkOverlap: 128
});
const output = await splitter.splitText(text);

如果你正在使用 LangChain 的其余部分来创建数据管道,你也可以输出LangChain 文档:

const output = await splitter.createDocuments([text]);

RecursiveCharacterTextSplitter

CharacterTextSplitter的天真之处在于它对文本的结构考虑甚少。RecursiveCharacterTextSplitter通过使用分隔符列表来逐步分解文本,从而解决了这一问题,直到它创建出适合您所需大小的块。默认情况下,它首先按段落分割文本,然后按句子,最后按单词分割。

import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
const text = loadText(); // 获取要分割的文本
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 1024,
  chunkOverlap: 128
});
const output = splitter.splitText(text);

您可以为RecursiveCharacterTextSplitter提供不同的字符,以便它可以根据不同的文本类型分割文本。还有两个类——MarkdownTextSplitterLatexTextSplitter——可以轻松地分别分割 Markdown 或 Latex。

但这种分块器也可以用来分割代码。例如,如果您想分割一个 JavaScript 文件,您可以这样做:

const splitter = RecursiveCharacterTextSplitter.fromLanguage("js", {
  chunkSize: 300,
  chunkOverlap: 0,
});
const jsOutput = await splitter.splitText(jsCode);

RecursiveCharacterTextSplitter是一个通用的文本分块器,是您构建用于为 RAG 应用程序流入数据的管道时很好的起点。

TokenTextSplitter

TokenTextSplitter采用不同的方法,首先使用js-tiktoken将文本转换为标记,然后将这些标记分成块,最后将这些标记转换回文本。

对于 LLM 来说,令牌是消耗内容的方式,而一个令牌可以是整个单词或单词的一部分。OpenAI 很好地展示了他们的模型如何将文本分解为标记。您可以这样使用TokenTextSplitter

import { TokenTextSplitter } from "@langchain/textsplitters";
const text = loadText(); // 获取要分割的文本
const splitter = new TokenTextSplitter({
  chunkSize: 1024,
  chunkOverlap: 128
});
const output = splitter.splitText(text);

LlamaIndex

LlamaIndex也负责比单纯的文本分块更多的工作。不过,我们将专注于其分块功能,这些功能也可在 LlamaIndex 之外使用。

LlamaIndex 将文档块视为节点,其余的库也围绕这些节点工作。有三种可用的处理器:SentenceSplitterMarkdownNodeParserSentenceWindowNodeParser

您可以使用以下命令安装整个 LlamaIndex 套件:

npm install llamaindex

如果您只需要文本解析器,可以只安装 LlamaIndex 核心:

npm install @llamaindex/core

SentenceSplitter

SentenceSplitter是 LlamaIndex 分块器中最简单的。它将文本分成句子,然后将它们合并成一个比给定的chunkSize小的字符串。

LlamaIndex 的这种方法与前面的分块器不同,因为它按令牌测量块的大小,而不是按字符测量。每个令牌大约有四个字符,因此如果您想使用DataStax 建议的默认值 1024 个字符,重叠 128 个字符,您应该将块大小设置为 256 个令牌,将重叠大小设置为 32 个令牌。

SentenceSplitter返回一个块数组,如下所示

import { SentenceSplitter } from "@llamaindex/core/node-parser";
const text = loadText(); // 获取要分割的文本
const splitter = new SentenceSplitter({
  chunkSize: 256,
  chunkOverlap: 32,
});
const output = splitter.splitText(text);

MarkdownNodeParser

如果您有 Markdown 文本要拆分,MarkdownNodeParser可以基于文档中的标题将其拆分为逻辑部分。

这个分块器不允许您设置块大小或重叠,因此您会失去此级别的控制。它还可以对 LlamaIndex 文档或节点进行操作。在本例中,我们首先将文本转换为文档,然后从中获取节点。

import { MarkdownNodeParser } from "@llamaindex/core/node-parser";
import { Document } from "@llamaindex/core/schema";
const text = loadText(); // 获取要分割的文本
const splitter = new MarkdownNodeParser();
const nodes = splitter.getNodesFromDocuments([new Document({ text })]);
const output = nodes.map(node => node.text);

SentenceWindowNodeParser

最后的 LlamaIndex 解析器将文本文本分解成句子,然后为每个句子产生一个节点,该节点在句子的两侧都有一个窗口。您可以选择窗口的大小。选择窗口大小为 1 将产生包含三个句子的节点:当前句子,一个在前,一个在后。窗口大小为 2 将产生包含五个句子的节点:当前句子,以及两侧各两个句子。

此解析器也适用于文档;你可以这样使用它:

import { SentenceWindowNodeParser } from "@llamaindex/core/node-parser";
import { Document } from "@llamaindex/core/schema";
const text = loadText(); // 获取要分割的文本
const splitter = new SentenceWindowNodeParser({ windowSize: 3 });
const nodes = splitter.getNodesFromDocuments([new Document({ text })]);
const output = nodes.map(node => node.text);

语义分块

Semantic-chunking在 JavaScript 世界中并不是一个流行的文本分块器,但我希望将它包括在内,因为它提供了不同的方法。它仍然可以控制块的最大大小,但它的分块方式更有趣。

它会先将文本分解成句子,然后使用本地下载的模型(默认情况下,它使用Xenova/all-MiniLM-L6-v2,但如果您愿意,可以选择其他模型)为每个句子生成嵌入式向量。然后根据余弦相似度,将句子分组到块中。这样做的目的是将相关的句子分组在一起,然后在话题发生变化时开始一个新的块。

这是一种比仅按块大小分块更智能的分块方式,甚至比将节标题考虑在内的 Markdown 解析器更智能。权衡是它可能更慢,因为它需要进行更多的计算。

使用它仍然很简单;通过以下命令安装它:

npm install semantic-chunking

您可以向chunkit函数传递一个similarityThreshold,即两个句子被包含在同一个块中的最小余弦相似度。较高的阀值提供了一个较高的标准,可能会导致较小的块。较低的阀值将允许更少但更大的块。始终值得尝试这个设置,以找到适合您数据的设置。

import { chunkit } from "semantic-chunking";
const text = loadText(); // 获取要分割的文本
const chunks = chunkit(text, {
  maxTokenSize: 256,
  similarityThreshold: 0.5
});

围绕进一步合并块有很多其他选项,可以查阅文档以获取更多详细信息

非结构化

Unstructured是一个用于从文件中提取数据并以智能方式对其进行分块的平台。它有一个开源工具包、一个无代码平台和一个API。这里我们将研究 API。

API 支持从各种不同类型的文件中提取数据,包括包含图像或数据表的 PDF 等文档。以下是调用 Unstructured API 的简单示例;您可以阅读Unstructured API 文档,了解关于从 PDF 和图像中提取数据等行为的更多功能。

首先,您应该安装 API 客户端:

npm install unstructured-client

您需要一个 API 密钥。您可以获得免费 API 密钥,或者付费服务有两周的免费试用期。一旦您有了 API 密钥,就可以使用客户端调用 API。

在此示例中,我正在对 Markdown 文件中的文本进行分块,但可以查看API 参数,了解对其他类型的文件的行为。

import { UnstructuredClient } from "unstructured-client";
import { ChunkingStrategy } from "unstructured-client/sdk/models/shared/index.js";
import { readFileSync } from "node:fs";

const client = new UnstructuredClient({
  serverURL: "https://api.unstructuredapp.io",
  security: {
    apiKeyAuth: "YOUR_API_KEY_HERE",
  },
});

const data = readFileSync("./post.md");

const res = await client.general.partition({
  partitionParameters: {
    files: {
      content: data,
      fileName: "post.md",
    },
    chunkingStrategy: ChunkingStrategy.BySimilarity,
  },
});
if (res.statusCode == 200) {
  console.log(res.elements);
}

您会注意到我选择了相似度分块策略;您可以在文档中阅读其他可用的分块策略,以及用于像图像和 PDF 这样的文档的分区策略

你的分块策略是什么?

事实证明,JavaScript 生态系统中有许多选项可用于将文本拆分为块。从像llm-chunk这样的快速简便方法、提供完整的 GenAI 管道的完整库(如LangChainLlamaIndex),到更复杂的方法(如semantic-chunking和具有加载和拆分文档的所有选项的Unstructured API)。

为您的 RAG 应用程序选择正确的分块策略对于获得良好的结果至关重要,因此请确保阅读有关最佳实践的文章,并尝试可用的选项以查看它们的结果。如果您想尝试使用llm-chunkLangChainLlamaIndexsemantic-chunking的库,请查看示例应用程序Chunkers

一旦您对分块进行了排序,下一步就是将这些块转换为向量嵌入,因此请查看如何使用Astra DB Vectorize轻松完成此操作。

最后感谢阅读!欢迎关注我,微信公众号倔强青铜三。欢迎点赞收藏关注,一键三连!!!

倔强青铜三
41 声望0 粉丝