RAG时,检索效果的优劣,和文本的分块的情况有很大关系。

SpringAI中通过TokenTextSplitter对文本分块。本文对SpringAI提供的TokenTextSplitter源码进行了分析,并给出一些自己的想法,欢迎大家互相探讨。

查看了TokenTextSplitter的源码,其进行文本分块的核心代码如下:

protected List<String> doSplit(String text, int chunkSize) {if (text != null && !text.trim().isEmpty()) {// 将分割的内容转为对应token的列表List<Integer> tokens = this.getEncodedTokens(text);List<String> chunks = new ArrayList();int num_chunks = 0;while(!tokens.isEmpty() && num_chunks < this.maxNumChunks) {// 根据token列表,按照chunkSize或者token列表长度的最小值进行截取List<Integer> chunk = tokens.subList(0, Math.min(chunkSize, tokens.size()));// 将token转为字符串String chunkText = this.decodeTokens(chunk);if (chunkText.trim().isEmpty()) {tokens = tokens.subList(chunk.size(), tokens.size());} else {// 从文本最后开始,获取英文的.!?和换行符的索引int lastPunctuation = Math.max(chunkText.lastIndexOf(46), Math.max(chunkText.lastIndexOf(63), Math.max(chunkText.lastIndexOf(33), chunkText.lastIndexOf(10))));// 如果索引值不是-1,并且索引大于分块的最小的字符数,对分块内容进行截取if (lastPunctuation != -1 && lastPunctuation > this.minChunkSizeChars) {chunkText = chunkText.substring(0, lastPunctuation + 1);}// 如果keepSeparator是false,将本文中的换行符替换为空格String chunkTextToAppend = this.keepSeparator ? chunkText.trim() : chunkText.replace(System.lineSeparator(), " ").trim();if (chunkTextToAppend.length() > this.minChunkLengthToEmbed) {// 将分块内容添加到分块列表中chunks.add(chunkTextToAppend);}// 对原来的token列表进行截取,用于排除已经分块的内容tokens = tokens.subList(this.getEncodedTokens(chunkText).size(), tokens.size());++num_chunks;}}if (!tokens.isEmpty()) {String remaining_text = this.decodeTokens(tokens).replace(System.lineSeparator(), " ").trim();if (remaining_text.length() > this.minChunkLengthToEmbed) {chunks.add(remaining_text);}}return chunks;} else {return new ArrayList();}}

参数说明: 

chunkSize: 每个文本块以 token 为单位的目标大小(默认值:800)。
minChunkSizeChars: 每个文本块以字符为单位的最小大小(默认值:350)。
minChunkLengthToEmbed: 文本块去除空白字符或者处理分隔符后,用于嵌入处理的文本的最小长度(默认值:5)。
maxNumChunks: 从文本生成的最大块数(默认值:10000)。
keepSeparator: 是否在块中保留分隔符(例如换行符)(默认值:true)。


TokenTextSplitter拆分文档的逻辑

1.使用 CL100K_BASE 编码将输入文本编码为 token列表

2.根据 chunkSize 对编码后的token列表进行截取分块

3.对于分块:

        (1)将token分块再解码为文本字符串

        (2)尝试从后向前找到一个合适的截断点(默认是英文的句号、问号、感叹号或换行符)。

        (3)如果找到合适的截断点,并且截断点所在的index大于minChunkSizeChars,则将在该点截断该块

        (4)对分块去除两边的空白字符,并根据 keepSeparator 设置,如果为false,则移除换行符

        (5)如果处理后的分块长度大于 minChunkLengthToEmbed,则将其添加到分块列表中

4.持续执行第2步和第3步,直到所有 token 都被处理完或达到 maxNumChunks

5.如果还有剩余的token没有处理,并且剩余的token进行编码和转换处理后,长度大于 minChunkLengthToEmbed,则将其作为最终块添加

源码中,是根据英文的逗号,叹号,问号和换行符进行文本的截取。这显然不太符合中文文档的语法习惯。为此,我们对源码进行修改,增加分割符的列表,用户可以根据文档的中英文情况,自行设置分割符。自定义的分割类代码如下:

package com.renr.springainew.controller;import com.knuddels.jtokkit.Encodings;
import com.knuddels.jtokkit.api.Encoding;
import com.knuddels.jtokkit.api.EncodingRegistry;
import com.knuddels.jtokkit.api.EncodingType;
import com.knuddels.jtokkit.api.IntArrayList;
import org.springframework.ai.transformer.splitter.TextSplitter;
import org.springframework.util.Assert;import java.util.*;/*** @Classname MyTextSplit* @Description TODO* @Date 2025-07-26 9:46* @Created by 老任与码*/
public class MyTextSplit extends TextSplitter {private static final int DEFAULT_CHUNK_SIZE = 800;private static final int MIN_CHUNK_SIZE_CHARS = 350;private static final int MIN_CHUNK_LENGTH_TO_EMBED = 5;private static final int MAX_NUM_CHUNKS = 10000;private static final boolean KEEP_SEPARATOR = true;private final EncodingRegistry registry;private final Encoding encoding;private final int chunkSize;private final int minChunkSizeChars;private final int minChunkLengthToEmbed;private final int maxNumChunks;private final boolean keepSeparator;private final List<String> splitList;public MyTextSplit() {this(800, 350, 5, 10000, true, Arrays.asList(".", "!", "?", "\n"));}public MyTextSplit(boolean keepSeparator) {this(800, 350, 5, 10000, keepSeparator, Arrays.asList(".", "!", "?", "\n"));}public MyTextSplit(int chunkSize, int minChunkSizeChars, int minChunkLengthToEmbed, int maxNumChunks, boolean keepSeparator, List<String> splitList) {this.registry = Encodings.newLazyEncodingRegistry();this.encoding = this.registry.getEncoding(EncodingType.CL100K_BASE);this.chunkSize = chunkSize;this.minChunkSizeChars = minChunkSizeChars;this.minChunkLengthToEmbed = minChunkLengthToEmbed;this.maxNumChunks = maxNumChunks;this.keepSeparator = keepSeparator;if (splitList == null || splitList.isEmpty()) {this.splitList = Arrays.asList(".", "!", "?", "\n");} else {this.splitList = splitList;}}protected List<String> splitText(String text) {return this.doSplit(text, this.chunkSize);}protected List<String> doSplit(String text, int chunkSize) {if (text != null && !text.trim().isEmpty()) {List<Integer> tokens = this.getEncodedTokens(text);List<String> chunks = new ArrayList();int num_chunks = 0;while (!tokens.isEmpty() && num_chunks < this.maxNumChunks) {List<Integer> chunk = tokens.subList(0, Math.min(chunkSize, tokens.size()));String chunkText = this.decodeTokens(chunk);if (chunkText.trim().isEmpty()) {tokens = tokens.subList(chunk.size(), tokens.size());} else {int lastPunctuation = splitList.stream().mapToInt(chunkText::lastIndexOf).max().orElse(-1);// 46 .  63 ?  33 !   10换行// int lastPunctuation = Math.max(chunkText.lastIndexOf(46), Math.max(chunkText.lastIndexOf(63), Math.max(chunkText.lastIndexOf(33), chunkText.lastIndexOf(10))));if (lastPunctuation != -1 && lastPunctuation > this.minChunkSizeChars) {chunkText = chunkText.substring(0, lastPunctuation + 1);}String chunkTextToAppend = this.keepSeparator ? chunkText.trim() : chunkText.replace(System.lineSeparator(), " ").trim();if (chunkTextToAppend.length() > this.minChunkLengthToEmbed) {chunks.add(chunkTextToAppend);}tokens = tokens.subList(this.getEncodedTokens(chunkText).size(), tokens.size());++num_chunks;}}if (!tokens.isEmpty()) {String remaining_text = this.decodeTokens(tokens).replace(System.lineSeparator(), " ").trim();if (remaining_text.length() > this.minChunkLengthToEmbed) {chunks.add(remaining_text);}}return chunks;} else {return new ArrayList();}}private List<Integer> getEncodedTokens(String text) {Assert.notNull(text, "Text must not be null");return this.encoding.encode(text).boxed();}private String decodeTokens(List<Integer> tokens) {Assert.notNull(tokens, "Tokens must not be null");IntArrayList tokensIntArray = new IntArrayList(tokens.size());Objects.requireNonNull(tokensIntArray);tokens.forEach(tokensIntArray::add);return this.encoding.decode(tokensIntArray);}}

测试代码:

    public void init2() {// 读取文本文件TextReader textReader = new TextReader(this.resource);// 元数据中增加文件名textReader.getCustomMetadata().put("filename", "医院.txt");// 获取Document对象,只有一个记录List<Document> docList = textReader.read();// 指定分割符List<String> splitList = Arrays.asList("。", "!", "?", System.lineSeparator());MyTextSplit splitter = new MyTextSplit(300, 100, 5, 10000, true, splitList);List<Document> splitDocuments = splitter.apply(docList);System.out.println(splitDocuments);}

另外,根据源码,minChunkSizeChars的值要小于chunkSize的值才有意义。

根据CL100K_BASE编码,300长度的token转为本文内容后,文本内容的长度在220-250之间(根据本例的中文文档测试,实际存在误差),转换比例在70%到80%多,为了根据特定的字符进行分割,所以minChunkSize的值最好小于210。

根据源码的逻辑,分割文本时,可能出现如果分隔符的索引小于minChunkSizeChars,就不会对文本进行分割,于是,就会出现句子被断开的情况。

针对该现象,可以增加分割的字符种类;或者干脆将minChunkSizeChars设置为0(解决方案有点简单粗暴哈O(∩_∩)O哈哈~);还可以根据分割后的内容,进行手动修改,然后再进行向量化处理。

该代码存在的问题:

使用由于是先转为token列表;再转为字符串后,根据分割符进行截取;截取后转为token,再根据token长度截取token列表,索引多次转换后,使用CL100K_BASE编码会存在一些中文数据的丢失或者乱码情况。

经过测试,可以将编码方式修改为O200K_BASE编码。使用该编码后,中文转换的token列表长度小于文本本身的长度,所以分块时,需要重置chunkSize和minChunkSizeChars的值。

this.encoding = this.registry.getEncoding(EncodingType.O200K_BASE);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/90644.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/90644.shtml
英文地址,请注明出处:http://en.pswp.cn/web/90644.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python----大模型(RAG 的智能评估-LangSmith)

一、LangSmith LangSmith是LangChain的一个子产品&#xff0c;是一个大模型应用开发平台。它提供了从原 型到生产的全流程工具和服务&#xff0c;帮助开发者构建、测试、评估和监控基于LangChain 或其他 LLM 框架的应用程序。 安装 LangSmith pip install langsmith0.1.137 官网…

磁悬浮轴承转子不平衡质量控制策略设计:原理、分析与智能实现

磁悬浮轴承(Active Magnetic Bearing, AMB)以其无接触、无摩擦、高转速、无需润滑等革命性优势,在高端旋转机械领域(如高速电机、离心压缩机、飞轮储能、航空航天动力系统)展现出巨大潜力。然而,转子固有的质量不平衡是AMB系统面临的核心挑战之一,它诱发强同步振动,威胁…

C++查询mysql数据

文章目录 文章目录 1.前言 2. 代码 &#xff08;1&#xff09;执行查询SQL &#xff08;2&#xff09;获取结果集 &#xff08;3&#xff09;遍历结果集&#xff08;获取字段数、行数&#xff09; &#xff08;4&#xff09;释放资源 3.完整代码 1.前言 我们成功连接数…

【论文阅读】-《GenAttack: Practical Black-box Attacks with Gradient-Free Optimization》

GenAttack&#xff1a;利用无梯度优化的实用黑盒攻击 Moustafa Alzantot UCLA Los Angeles, U.S.A malzantotucla.edu Yash Sharma Cooper Union New York, U.S.A sharma2cooper.edu Supriyo Chakraborty IBM Research New York, U.S.A supriyous.ibm.com Huan Zhang UCLA Los…

CT、IT、ICT 和 DICT区别

这四个术语&#xff1a;CT、IT、ICT 和 DICT&#xff0c;是信息通信行业中常见的核心概念&#xff0c;它们既有演进关系&#xff0c;又有各自的技术重点。&#x1f539; 一、CT&#xff08;Communication Technology&#xff09;通信技术**定义&#xff1a;**以语音通信为核心的…

Effective C++ 条款4:确定对象被使用前已先被初始化

Effective C 条款4&#xff1a;确定对象被使用前已先被初始化核心思想&#xff1a;永远在使用对象前将其初始化。未初始化对象是未定义行为的常见来源&#xff0c;尤其对于内置类型。 1. 内置类型手动初始化 int x 0; // 手动初始化 const char* text &quo…

LangSmith的配置介绍

文章目录注册及登录生成API KeyLangSmith的配置方式一&#xff1a;放运行环境里方式二&#xff1a;写代码里执行代码查看LangSmith上是否看到本次运行的项目记录LangSmith的其他注意注册及登录 首先使用邮箱注册一个账号及设置密码&#xff0c;等收到收到邮件后&#xff0c;进…

Linux的生态与软件安装

坚持用 清晰易懂的图解 代码语言&#xff0c;让每个知识点变得简单&#xff01; &#x1f680;呆头个人主页详情 &#x1f331; 呆头个人Gitee代码仓库 &#x1f4cc; 呆头详细专栏系列 座右铭&#xff1a; “不患无位&#xff0c;患所以立。” Linux的生态与软件安装前言目录…

3.4 安全-分布式-数据库-挖掘

一、数据库的安全数据库里面的安全措施&#xff1a;用户标识和鉴定&#xff1a;用户的账户口令等存取控制&#xff1a;对用户操作进行控权&#xff0c;有对应权限码才能操作。密码存储和传输&#xff1a;加密存储。视图的保护&#xff1a;视图需要授权审计&#xff1a;专门的文…

多线程 Reactor 模式

目录 多线程 Reactor 模式的核心动机 多线程演进方向 多线程 Reactor 模型结构 多线程 EchoServer 实现核心部分 Handler 的多线程化 多线程 Reactor 的三个核心点 本篇文章内容的前置知识为 单线程 Reactor 模式&#xff0c;如果不了解&#xff0c;可点击链接学习 单线程…

[NLP]多电源域设计的仿真验证方法

多电源域设计的仿真验证方法 1. 更复杂的 Testbench 例子(多电源域、复杂低功耗场景) 假设有两个电源域 PD1 和 PD2,分别对应控制信号 pwr_sw_ctrl1、iso_ctrl1、ret_ctrl1 和 pwr_sw_ctrl2、iso_ctrl2、ret_ctrl2,且两域之间有通信。 RTL 端口声明(简化版) module top…

Apache Ignite 中 WHERE 子句中的子查询(Subqueries in WHERE Clause)的执行方式

这段内容是关于 Apache Ignite 中 WHERE 子句中的子查询&#xff08;Subqueries in WHERE Clause&#xff09;的执行方式 的说明。理解这段内容对于编写高效的 SQL 查询、避免性能瓶颈非常重要。下面我将为你 逐句解释并深入理解这段内容。&#x1f9fe; 原文翻译 解释 原文&a…

MySQL(153)如何使用全文索引?

MySQL的全文索引&#xff08;Full-Text Index&#xff09;是一种特殊的索引类型&#xff0c;专门用于加速文本数据的搜索。与普通的B树索引不同&#xff0c;全文索引适用于大文本字段&#xff08;如TEXT、VARCHAR等&#xff09;的全文搜索。它通过构建一个倒排索引&#xff0c;…

微分方程入门之入门之入门,纯笔记

当描述 相对变化量 比 绝对量 更容易时&#xff0c;微分方程就经常用到了。 比如&#xff0c;描述为什么种群数量增加or减少【相对】&#xff0c;比描述为什么它在某个时间点是某个特定值【绝对】更容易。 物理学中&#xff0c;运动经常用力来描述&#xff0c;力–>代表变化…

【C++】简单学——vector类(模拟实现)

模拟实现的准备工作 看源码&#xff0c;了解这个类的大概组成 1.先看成员变量 成员变量的组成是三个迭代器 问&#xff1a;这个iterator内嵌类型究竟是什么&#xff1f;即这个迭代器是什么 迭代器实际就是T* 问&#xff1a;这三个迭代器代表什么意思&#xff1f; 连蒙带猜…

【WRF】根据自动安装脚本安装 WRF / WRF-CHEM等

目录 GitHub 上 WRF 自动安装脚本 ⚙️ 脚本的作用 🖥️ 支持的系统 📦 可安装的 WRF 版本及其选项 ✅ 如何使用(以 WRF 4.6.1 为例) ✅ 依赖库的安装位置 完整安装脚本分析 参考 GitHub 上 WRF 自动安装脚本 GitHub 上的 WRF-Install-Script 项目的 Releases(发布版本…

M²IV:面向大型视觉-语言模型中高效且细粒度的多模态上下文学习

MIV&#xff1a; Towards Efficient and Fine-grained Multimodal In Context Learning in Large Vision-Language Models COLM 2025 why 新兴的研究方向&#xff1a;上下文学习&#xff08;ICL&#xff09;的效果“向量化”&#xff0c;其核心思想是用transformer内部的向量来…

龙迅#LT8711UXD适用于Type-C/DP1.4 /EDP转 HDMI2.0 功能,分辨率高达4K60HZ,可支持HDCP!

1. 描述LT8711UXD 是一款高性能双通道 Type-C/DP1.4 转 HDMI2.0 转换器&#xff0c;旨在将 USB Type-C 源或 DP1.4 源连接到 HDMI2.0 接收器。该LT8711UXD集成了一个符合 DP1.4 标准的接收器和一个符合 HDMI2.0 标准的发射器。此外&#xff0c;还嵌入了两个用于CC通信的CC控制器…

《计算机组成原理与汇编语言程序设计》实验报告一 基本数字逻辑及汉字显示

目 录 一、实验学时 二、实验目的 三、实验要求 四、实验内容 五、实验步骤 1、打开Logisim软件&#xff0c;列出异或逻辑电路真值表&#xff0c;并使用与、或、非基本原件实现异或逻辑电路。 2、打开Logisim软件&#xff0c;列出同或逻辑电路真值表&#xff0c;并使用…

聚焦牛牛道:绿色积分模式如何实现快速发展?

​绿色消费积分政策再次进入大众视野&#xff0c;这种能为企业减轻库存负担、让咨金周转更灵活的促销方式&#xff0c;很快就成了焦点。牛牛道作为积极践行这一政策的平台&#xff0c;凭借其独树一帜的商业模式和运营思路&#xff0c;在短时间内就取得了显著发展。一、牛牛道平…