Spring AI(14)——文本分塊優化

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(發布版本…

M2IV:面向大型視覺-語言模型中高效且細粒度的多模態上下文學習

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;在短時間內就取得了顯著發展。一、牛牛道平…