尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

azure-search-openai-demo 数据切分算法深度解析:SentenceTextSplitter 的语义化 Chunk 实现原理

azure-search-openai-demo 数据切分算法深度解析:SentenceTextSplitter 的语义化 Chunk 实现原理 azure-search-openai-demo 数据切分算法深度解析SentenceTextSplitter 的语义化 Chunk 实现原理【免费下载链接】azure-search-openai-demoA sample app for the Retrieval-Augmented Generation pattern running in Azure, using Azure AI Search for retrieval and Azure OpenAI large language models to power ChatGPT-style and QA experiences.项目地址: https://gitcode.com/GitHub_Trending/az/azure-search-openai-demo本篇技术指南围绕 azure-search-openai-demo基于 Azure AI Search 与 Azure OpenAI 的 RAG 检索增强生成示例项目数据摄取管线中的核心切分模块展开系统讲解 docs/textsplitter.md 所描述的文本分块算法。读完本文你将掌握SentenceTextSplitter从按句积累到递归拆分超长跨度、再到跨页缝合与语义重叠的完整工作流并能直接对照 textsplitter.py 源码与测试用例理解每一处边界决策。背景为什么 RAG 需要句子级切分在 RAGRetrieval-Augmented Generation模式中文档需要先被切分成较小的块chunk再逐一生成向量嵌入并写入 Azure AI Search 索引。切分的原因很直接OpenAI 的上下文窗口有 token 上限如果整份文档作为一个检索单元命中的相关内容量可能超出模型承载能力也浪费成本。将内容切成聚焦的小块后系统只把最相关的片段注入 LLM 提示词既提升回答质量又降低开销详见 docs/data_ingestion.md 的 Chunking 小节。但切块本身是一个精度问题切得太随意会切断句子、割裂语义导致检索召回质量下降。azure-search-openai-demo 的SentenceTextSplitter正是为此设计它尽量对齐句子边界、尊重 token 上限、保持图片占位符的原子性并尝试修复 PDF 等格式造成的跨页断句。切分器的整体结构与职责划分textsplitter.py 定义了抽象基类TextSplitter第 15-24 行与两个具体实现实现类适用范围切分策略SimpleTextSplitter仅用于 JSON 文件按固定最大对象长度硬切不理解内容第 586-608 行SentenceTextSplitter其余所有格式按句子边界 token 上限 跨页修复 语义重叠的复杂策略两者的装配位置在 servicesetup.py 的build_file_processors第 246-309 行.json扩展名绑定SimpleTextSplitter第 290 行而.md、.txt、.csv、.pdf、.html以及 Document Intelligence 处理的.docx、.pptx、.xlsx等全部复用同一个SentenceTextSplitter实例第 256 行、291-309 行。从调用链上看切分器处于摄取流程的文本处理阶段filestrategy.py 的parse_file先调用对应解析器产出Page列表再交给 textprocessor.py 的process_text第 27-51 行调用splitter.split_pages(pages)把每个Page变成若干Chunk。Chunk与Page的数据结构定义在 page.pyPage第 91-106 行、Chunk第 109-120 行每个Chunk最终会作为独立文档写入 Azure AI Search 索引。高层设计目标与核心组件根据原文档SentenceTextSplitter需要同时满足六个设计目标产出与句子边界对齐的语义连贯块尊重每个块的最大 token 数硬上限 500同时遵循软性字符长度指导默认 1000 字符合并/归一化允许 20% 溢出容差包含figure的块不受大小限制图片块永不拆分保持结构化的图片占位符figure.../figure原子性内部绝不拆分并总是附加到已有的累积文本之后在可能时修复句中跨页断行同时遵守 token 与软字符预算避免空输出或未闭合的 figure 标签执行轻度归一化仅裁剪会导致轻微溢出的首尾空白不修改图片块。这些目标在源码中有明确的常量与配置支撑textsplitter.py 第 27-85 行ENCODING_MODEL text-embedding-ada-002 STANDARD_WORD_BREAKS [,, ;, :, , (, ), [, ], {, }, \t, \n] STANDARD_SENTENCE_ENDINGS [., !, ?] CJK_SENTENCE_ENDINGS [。, , , ‼, ⁇, ⁈, ⁉] DEFAULT_OVERLAP_PERCENT 10 DEFAULT_SECTION_LENGTH 1000 # Roughly 400-500 tokens for English bpe tiktoken.encoding_for_model(ENCODING_MODEL)值得注意的细节token 计数使用 OpenAI 的 tiktoken 库编码模型固定为text-embedding-ada-002的 BPE代码注释指出 text-embedding-3-XX 系列与它是同一套 BPE因此计数结果可复用。句末标点同时覆盖英文. ! ?与 CJK日文/中文语境下的。‼⁇⁈⁉断词字符则参考 W3C JIS X 4051 / jlreq 规范包含 40 余个 CJK 标点第 32-73 行确保中日文文本不会被在单词中间切断。SentenceTextSplitter.__init__第 194-204 行会把这些常量组织成运行参数sentence_endings、word_breaks、max_section_length1000、max_tokens_per_section500可通过构造参数覆盖、section_overlap1001000 × 10%以及semantic_overlap_percent10。从实现角度看切分流程由四个组件协作完成图片预处理用正则figure.*?/figure把每个页面文本先抽取为figure 块再进行任何跨度切分与递归累积器_ChunkBuilder第 132-185 行按句追加跨度直到下一次追加会突破字符或 token 限制时冲刷flush出一个块。can_fit第 153-159 行同时检查字符数之和与 token 数之和首个跨度只要自身不超限即可放入force_append第 168-169 行允许图片块无视 token 上限强制附加从而让标题 紧随其后的图片留在同一块超长跨度的递归细分先找句子边界再找断词位置最后退化为带重叠的中点切分跨页合并与语义重叠在块级别修复页面边界并追加前视重叠内容。逐页切分主流程split_pages第 380-583 行是核心入口。对每一页它按以下顺序处理原文档给出的 mermaid 流程图语义与源码一一对应图中三个术语对应代码中的不同粒度单元Block块一个完整的figure.../figure元素figure-only 块或一段不含 figure 的连续文本可能含多个句子。块从不跨越页面边界——因为blocks列表是按单页page.text构造的第 403-411 行Span跨度从文本块中切出的类句子片段Chunk块/输出单元最终输出将在 Azure AI Search 中单独索引。一个 chunk 可能由一个或多个 span、一个 figure、或文本 附加 figure组成。文本块切分为 span 的逻辑很朴素逐字符扫描遇到句末标点就把累积字符收成一个 span第 432-440 行。随后对每个 span 计算 token 数第 443 行分三种情况处理span 自身超过 token 上限罕见的长句先冲刷当前累积器再对 span 单独调用split_page_by_max_tokens递归切分第 445-448 行累积器放得下直接add追加第 450 行累积器放不下但 span 单独放得下先冲刷累积器再重试追加理论上必然成功因为此时 span 自身 token 数 ≤ 上限第 452-454 行源码注释称之为 guaranteed to fit。遇到 figure 块时第 421-429 行若累积器已有文本则用append_figure_and_flush把 figure 强制附加后冲刷让标题/前文与图片同块否则将 figure 单独作为一个 chunk 发出。这正是原文档所述figure 永不拆分、总是附加到已有累积文本的实现依据。超长跨度的递归处理当单个 span 超过 token 上限时递归切分开始工作。源码入口是split_page_by_max_tokens第 247-271 行其流程为用 tiktoken 对 span 编码并统计 token 数若在 token 上限内直接产出该 span否则调用_find_split_pos第 206-245 行寻找切分点以文本中点为中心、在中央三分之一窗口内向外扩展扫描优先找句末标点其次找断词字符找到边界则在边界字符之后切分该标点保留在第一半对两半递归窗口内找不到任何可接受边界时退化为中点切分并带对称 10% 重叠first_half text[:middle overlap]、second_half text[middle - overlap:]第 265-268 行重叠区域在两段中各出现一次递归直到所有碎片都在 token 上限内。对应流程图来自原文档两个容易混淆的关键澄清原文档特别标注源码亦可印证10% 重叠按原始字符长度计算int(len(text) * 0.10)不是按 token 计算因此重复区域长度是2 × floor(0.10 × 字符数)个字符两半的 token 数可能不同递归只在 span 本身超过 token 上限时触发。如果只是把 span 追加进累积器会溢出、但 span 单独能放下那么累积器被冲刷而不是走递归——这在第 450-454 行的add失败分支中实现。关于扫描窗口源码中_find_split_pos使用window_limit length // 3第 220 行来界定中央区域__init__中还有一个self.sentence_search_limit 100字段第 198 行从当前实现看实际扫描边界由三分之一窗口决定该字段可视为历史遗留的配置痕迹。测试对递归路径的覆盖相当全面test_prepdocslib_textsplitter.pytest_oversize_single_sentence_recursion第 248-257 行验证无标点长句的递归切分且每块 token 不超限test_recursive_split_prefers_word_break_over_overlap第 450-468 行验证有空格时优先断词而非中点重叠test_recursive_split_overlap_fallback_when_no_word_breaks第 471-490 行验证既无句末标点也无断词时确实产生重叠重复区域test_sentence_boundary_left_side/test_sentence_boundary_right_side第 425-435 / 411-422 行分别覆盖中点左右两侧发现边界的分支。跨页边界修复PDF 等格式的排版常把一句话从页尾切断到下一页页首。split_pages维护一个previous_chunk变量第 395 行每页处理完都会尝试与上一页的最后一个 chunk 做跨页缝合第 459-536 行。原文档描述了按序尝试的两种策略。策略一完整合并直接尝试把第 N 页的最后一个 chunk 与第 N1 页的第一个 chunk 拼接必须同时满足源码第 460-472 行的条件判断前一 chunk 末尾不是句末标点说明句子没结束新页首个 chunk 以小写字母开头连续性启发式且不以#开头、不是标题/列表样式、不以figure开头拼接后文本同时满足 token 上限500与软字符预算归一化后 ≤ 1.2 × 1000 字符。合并时调用_safe_concat第 88-108 行做智能拼接如果两侧已有空白边界则直接拼接如果左侧以 HTML 闭合标签结尾则不插入空格如果两侧都是字母数字字符则插入一个空格否则直接连接。test_safe_concat_html_tag_boundarytest_prepdocslib_textsplitter.py 第 533-543 行专门验证了/b后不插入空格的场景。策略二尾部残句前移当完整合并会违反大小限制时执行更精细的修复把前一 chunk 末尾未完成的残句片段抽出来前移到下一页开头与后续内容重聚。源码实现第 481-536 行在上一 chunk 文本中找最后一个句末标点prev_text.rfind确定保留部分与残句起点用fits()检查残句 新页首个 chunk是否满足字符与 token 预算若放不下走硬裁剪路径先按剩余字符预算截断再迭代收缩直到满足 token 约束第 504-516 行步长为 50 字符短残句步长为 1把能放下的片段用_safe_concat前拼到新页首个 chunk残句剩余部分leftover_fragment作为独立 chunk 插入到该 chunk 之前必要时递归切分。这与语义重叠有本质区别原文档明确强调前移是移动文本除了之后可能发生的递归切分重叠外不产生重复语义重叠是复制下一 chunk 的一小段前视预览前移只在跨页且完整合并过大时激活语义重叠是常规操作且有大小上限。测试覆盖了前移的多个分支test_cross_page_merge_fragment_shift_no_sentence_end第 321-335 行、test_cross_page_merge_fragment_shift_with_sentence_end_and_shortening第 338-359 行、test_cross_page_merge_fragment_shift_hard_trim第 362-379 行以及test_fragment_shift_token_limit_fits_false第 493-510 行——后者通过把max_section_length调到 5000 隔离字符约束专门触发仅因 token 超限导致 fits() 失败的裁剪循环。Chunk 归一化在完成块组装与跨页合并之后归一化步骤对所有非 figure 块执行轻度修正_normalize_chunk第 111-129 行调用点在 539-548 行包含figure的块完全不动视为原子单元绝不裁剪仅当前导空格单独导致块超过软字符预算时才裁剪前导空格while trimmed.startswith( ) and len(trimmed) max_chars第 124 行若块仅因尾部空白超出软上限 3 个字符以内len(trimmed) max_chars 3则rstrip()去除尾部空白/换行第 126-128 行不做激进的重排或内部空白折叠——保留原始格式只消除边界调整造成的琐碎溢出。测试方面test_normalization_trims_leading_space_overflowtest_prepdocslib_textsplitter.py 第 280-292 行把max_section_length收紧到 50 验证前导空格裁剪test_normalization_trims_trailing_space_overflow第 546-564 行构造恰好超限 3 字符并以空白结尾的块验证rstrip()生效且长度回落。语义重叠提升召回的前视复制为了提升检索召回率除最后一个 chunk 外每个 chunk 都会从下一 chunk 的开头借一小段前视内容追加到自己末尾下一 chunk 本身保持原样从而保证每个 chunk 的开头仍是干净的句子边界便于高亮与定位。源码实现集中在_append_overlap第 311-378 行其行为与文档描述逐条对应大小目标约为max_section_length × 10% 100 字符第 328 行来源总是取下一 chunk 的开头next_chunk.text[:target]绝不取上一 chunk 的尾部边界寻找前缀可向前扩展到 2 倍目标长度优先停在句末标点其次停在断词字符需已推进 20 字符以上两者都找不到则回退裁剪掉尾部的半个词第 336-352 行去重若前一 chunk 末尾已以该前缀结尾则不再追加第 355-356 行安全上限若拼接后超过字符软上限1200或 token 上限则从前缀起始处按断词/句末位置逐段收缩直到放得下或放弃第 358-377 行Figure 排除任一 chunk 含figure时既不给予也不接收重叠第 325-326 行。重叠的应用时机由split_pages末尾的两段逻辑控制第 550-564 行同页相邻 chunk 之间无条件尝试只要不含 figure跨页边界仅当_should_cross_page_overlap第 292-309 行通过启发式检查才应用——前一 chunk 以句中标点结束未完整收尾、下一 chunk 以小写字母开头、且下一 chunk 首行不像标题/列表/ figure。_is_heading_like第 273-290 行负责标题识别以#开头、全大写或短 Title Case 行≤ 80 字符且 ≤ 12 词、编号/罗马数字小节如1.、II)、以及-、*、•列表样式都被判定为标题。递归重叠与语义重叠的区别原文档的明确辨析递归重叠是切分单个超长 span 且找不到安全边界时的兜底手段会把中点区域复制进两段结果语义重叠则是在所有 chunk 已定稿后追加的单向前视复制。测试test_intra_page_semantic_overlap_applied第 597-620 行验证同页相邻块之间确实出现约 10% 的尾部重复test_append_overlap_preserves_next_chunk_start第 644-707 行验证重叠只追加到前一块末尾、后一块开头不被污染。实例对照从输入到输出原文档提供了五个外加 3b演示示例使用比真实 500 token 更小的上限以便展示。以下保留原始输入与输出并结合源码解释每个现象示例 1简单页面 → 单块输出Sentence one. Sentence two is slightly longer. Final short one.输出 1 个 chunk所有句子都满足限制累积器一次性冲刷。示例 2中间的原子图片块 → 两块输出Heading line Intro before the figure. figureimg srcx.png altX/figure Text that follows the figure. Another sentence.Chunk 0: Heading line Intro before the figure. figureimg srcx.png altX/figure Chunk 1: Text that follows the figure. Another sentence.figure 保持原子性并附加到前文对应第 421-424 行的append_figure_and_flush随后的文本进入下一块。示例 3超长单跨度 → 带 24 字符重叠的两块ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789Chunk 0: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKL Chunk 1: yz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789原长度 124递归兜底重叠 int(124 × 0.10) 12重复区域 2 × 12 24 字符yz0123456789ABCDEFGHIJKL出现在 Chunk 0 末尾与 Chunk 1 开头——因为中点附近没有任何句子边界。示例 3b有断词但无句号的超长跨度 → 在空格处切分alpha beta gamma delta epsilon zeta eta theta iota kappa lambda ... (continues)Chunk 0: alpha beta gamma delta epsilon zeta eta Chunk 1: theta iota kappa lambda ...中点附近没有句末标点但有空格断词字符切分器选择该边界而非生成 10% 重复重叠对应_find_split_pos第二优先级与test_recursive_split_prefers_word_break_over_overlap。示例 4跨页合并Page A: The procedure continues to operate Page B: under heavy load and completes successfully. Follow-up sentence.Chunk 0: The procedure continues to operate under heavy load and completes successfully. Chunk 1: Follow-up sentence.Page A 以句中结束、Page B 以小写开头且合并后不超限满足完整合并条件剩余部分形成第二块test_cross_page_merge_mid_sentence第 269-277 行验证了类似场景。示例 5合并过大时尾部残句前移Page A: Intro sentence finishes here. This clause is long but near the limit and the following portion would push it over Page B: so the trailing fragment carry-forward moves this trailing portion forward. Remaining context continues here.Chunk 0: Intro sentence finishes here. Chunk 1: This clause is long but near the limit and the following portion would push it over so the trailing fragment carry-forward moves this trailing portion forward. Remaining context continues here.完整合并会超限因此 Page A 末尾未完成的从句被前移到下一块开头对应第 481-536 行的前移逻辑与 Page B 内容重聚。工程质量测试与快照保障切分器是摄取管线中语义正确性要求最高的模块之一项目为其配备了成体系的测试test_prepdocslib_textsplitter.py多语言 PDF 实测test_sentencetextsplitter_multilang第 107-139 行对tests/test-data/下所有 PDF含日文、韩文、中文、阿拉伯文样本运行切分断言每个 chunk 字符数 ≤max_section_length × 1.2且 token 数 ≤ 500快照测试test_sentencetextsplitter_list_parse_and_split第 43-66 行与test_pages_with_figures第 179-197 行把切分结果序列化后与tests/snapshots/test_prepdocslib_textsplitter/下的快照比对防止算法行为回归figure 原子性test_large_figure_not_split第 200-218 行用 200 行表格行构造超大 figure在低 token 阈值下验证 figure 整块保留且标签配对test_figure_at_start_emitted第 221-231 行防止页首 figure 被漏发的回归test_unbalanced_figure_treated_as_text第 234-245 行验证残缺 figure 标记被当作普通文本安全切分边界条件test_sentencetextsplitter_split_empty_pages第 28-31 行验证空输入返回空列表。这些测试同时印证了原文档提到的两条隐性保证绝不输出空 chunkflush_into中有chunk.strip()判断第 174 行与绝不产生未闭合 figure 标签。扩展与调参指引切分器目前没有把max_tokens_per_section暴露为 CLI 参数——servicesetup.py中直接以默认 500 实例化第 256 行。如需调整切分行为原文档与数据摄取文档的建议一致直接修改 textsplitter.py 中的SentenceTextSplitter核心可调项包括DEFAULT_SECTION_LENGTH默认 1000软字符预算直接决定section_overlap与语义重叠目标大小DEFAULT_OVERLAP_PERCENT默认 10递归兜底重叠比例max_tokens_per_section构造参数默认 500每块硬 token 上限semantic_overlap_percent默认 10语义重叠的目标前缀比例。对 JSON 文件若需要调整SimpleTextSplitter的最大对象长度可在build_file_processors中为.json处理器传入SimpleTextSplitter(max_object_length...)当前默认 1000见第 592 行。小结SentenceTextSplitter用一套先按句累积、超限递归细分、跨页缝合修复、末尾语义重叠的分层策略把任意格式文档的页面流变成高质量、可索引、语义完整的 chunk 序列。理解它的四个层次——span 累积_ChunkBuilder、超长递归split_page_by_max_tokens_find_split_pos、跨页修复完整合并 残句前移、语义重叠_append_overlap——有助于你在遇到检索质量问题时快速定位是切分粒度、跨页断句还是重叠策略造成的偏差也能为基于该仓库二次开发自定义切分器提供可对照的基准实现。进一步阅读数据摄取管线总览含本地/云端摄取全流程、textsplitter.py 完整源码、切分器测试套件。【免费下载链接】azure-search-openai-demoA sample app for the Retrieval-Augmented Generation pattern running in Azure, using Azure AI Search for retrieval and Azure OpenAI large language models to power ChatGPT-style and QA experiences.项目地址: https://gitcode.com/GitHub_Trending/az/azure-search-openai-demo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表