首页/新闻资讯/正文详情

RAG二 文档切分实战:从语义分层到长度优化

发布时间:2026/11/2 3:10:50 来源:云帆数科 栏目:资讯中心
RAG二 文档切分实战:从语义分层到长度优化
1. 背景与目标在构建知识库或 RAG 检索链路时Markdown 文档的切分质量直接决定了后续向量召回的效果。本文围绕一个基于 LangGraph 的导入流程节点讲解如何先按标题做语义切分再对超长块做递归二次切割最终产出结构清晰、长度可控的文本块。整体目标可以概括为三点语义完整优先以 Markdown 标题为边界保证每个块的主题相对独立。长度可控单块内容不超过模型上下文限制超长块需要二次切割。可追溯每个块保留所属标题与文件标题便于后续检索与引用。2. 文档切分节点概览文档切分节点node_document_split位于app/import_process/agent/nodes/node_document_split.py核心职责是把长篇、无结构或半结构化的 Markdown 文档转化为可直接向量化入库、可精准检索的微观语义单元Chunk。它是 RAG 知识库的核心基础切分质量直接决定后续检索召回率与答案准确率没有合理切分再好的向量模型也无法发挥作用。2.1 切割核心原则语义完整不切断句子、代码块、表格、条款保证单个 Chunk 是完整的知识点或逻辑单元。长度可控Chunk 大小适配大模型上下文窗口和 Embedding 模型输入限制不超上限、不碎片化。边界清晰相邻 Chunk 保留合理重叠避免关键信息落在两块交界处。可追溯每个 Chunk 携带完整元数据标题、父标题、文件名便于后续溯源和引用。3. RAG 文档切分核心思路文档切分的核心逻辑是「先保语义再控大小最后补细节」全程遵循三步核心流程所有操作都围绕「不破坏语义、适配检索」展开。3.1 第一步先做语义分层无论什么类型的文档先不考虑长度优先按文档本身的语义边界拆分这是 RAG 切分的核心原则——语义完整永远比「凑长度」更重要。核心逻辑找到文档的天然语义边界标题、章节、段落、条款、对话轮次按边界拆分确保每个拆分后的单元能独立表达一个完整的意思比如一个章节、一个步骤、一个条款、一轮对话。关键动作先识别文档结构是否有标题、条款、表格、代码块再按结构拆分不盲目按字符数切割。3.2 第二步再做长度优化语义拆分后会出现两种情况单个语义单元过长超过模型输入限制或者单个语义单元过短碎片化导致检索无意义。此时进行针对性优化超长处理对超过模型适配长度的语义单元进行递归切分按更小的语义边界段落 → 句子 → 标点切割不硬断句子、不破坏逻辑。过短处理对过短的语义单元如单个句子、零散短语在同语义层级内合并比如同一章节下的短段落合并避免碎片化。具体切割大小会根据不同文档类型有差异后文会单独说明。3.3 第三步最后补细节保障长度优化后补充两个关键细节确保切分结果可落地、可调试重叠设置相邻 Chunk 保留一定重叠5%~10%避免关键信息如步骤、参数、条款落在切口上导致检索漏招。元数据补齐给每个 Chunk 添加元数据标题、父标题、文件名、块序号便于后续检索溯源、问题定位。4. 不同类型文档的切分形式不同文档的结构、用途不同切分形式和侧重点也会不同。下面按类型介绍切分形式、核心要求与操作细节。4.1 技术文档 / 手册 / API 文档这类文档有清晰的标题层级#一级标题、##二级标题……包含步骤、代码块、参数说明核心是「步骤完整、代码可读、参数不遗漏」。语义拆分按标题层级切分一级标题拆分为大模块二级标题拆分为子模块以此类推确保每个模块对应一个完整的知识点。代码块保护识别代码块标记/~~~代码块整体保留不拆分、不切割哪怕代码块过长也单独作为一个 Chunk避免代码不可读。长度优化若单个标题下的内容过长如超过 2000 字符可自行设定按段落拆分优先在段落空行处切割不切断步骤、不切断代码若内容过短如小于 300 字符且与相邻子标题属于同一知识点合并为一个 Chunk。细节补充重叠率 5%~8%如 2000 字符的 Chunk 重叠 100~160 字符元数据保留父标题、当前标题、文件名便于追溯知识点所属模块。核心要求不切断步骤、不切断代码块、不切断参数说明确保每个 Chunk 能独立体现一个操作或知识点。4.2 论文 / 研究报告 / 长叙述文本这类文档有摘要、引言、章节、结论以文字叙述为主核心是「论点与论据连贯、逻辑完整」无代码块多为段落式结构。语义拆分按章节标题加段落切分先按章节标题拆分为大模块再按段落拆分为子模块确保每个子模块对应一个完整的论点或论据如「2.1 实验方法」下的某一段实验描述。长度优化若单个段落过长如超过 3000 字符按句子拆分优先在句号、感叹号处切割不硬断句子若单个段落过短如小于 400 字符且与相邻段落属于同一论点合并为一个 Chunk。细节补充重叠率 8%~12%如 2500 字符的 Chunk 重叠 200~300 字符避免论点边界丢失元数据保留章节标题、段落序号、文件名标注论点所属章节。核心要求论点与论据不分离不切断句子确保每个 Chunk 能完整表达一个观点或一段论述。4.3 对话记录 / 日志 / 工单这类文档无标题、无章节多为「角色 内容」的轮次结构如客服对话、系统日志核心是「上下文承接、轮次完整」。语义拆分按轮次或时间戳切分优先按完整轮次拆分如「用户提问 → 客服回复」为一个完整轮次日志按时间戳分段拆分。长度优化若轮次过长如多轮对话累计超过 1500 字符按单轮对话拆分确保每轮对话独立成块若轮次过短如单句对话小于 200 字符合并相邻轮次如连续 3 轮短对话合并为一个 Chunk避免碎片化。细节补充重叠率 15%~20%如 1000 字符的 Chunk 重叠 150~200 字符确保上下文承接不丢失元数据保留角色如用户、客服、时间戳、文件名标注对话或日志的时间顺序。核心要求轮次完整、上下文承接不切断单轮对话确保每个 Chunk 能体现一段完整的交互或日志片段。4.4 法律 / 合同 / 制度文档这类文档有清晰的条款编号如「第一条、第二条」语言严谨核心是「条款完整、不破坏引用关系」不可拆分单条条款。语义拆分按条款编号切分单条条款如「第一条 定义」作为一个基础语义单元若条款过长如包含多个子条款按子条款拆分如「第一条 1.1 定义」。长度优化若单条条款过长如超过 2000 字符按条款内的分句拆分优先在分号、句号处切割不破坏条款逻辑不合并任何条款哪怕条款过短避免条款边界模糊、引用错误。细节补充重叠率 5%~10%如 1500 字符的 Chunk 重叠 75~150 字符元数据保留条款编号、父条款若有、文件名标注条款所属章节。核心要求不拆分单条条款、不破坏引用关系确保每个 Chunk 是完整的一条或一段条款可直接用于法律检索、条款引用。5. 切分的核心原则补充语义优先原则无论长度如何先保证语义完整不硬断句子、代码、条款这是 RAG 切分的底线。模型适配原则Chunk 大小不超过 Embedding 模型输入限制总和不超过大模型上下文窗口的 70%。不碎片化原则单个 Chunk 不小于 300 字符纯中文条款除外避免零散碎片导致检索噪声。边界保护原则代码块、表格、条款整体保留不拆分、不切割。6. 代码实现步骤本节点负责将 Markdown 文本切分为适合向量检索的语义块Chunk采用「先按标题语义切分 → 再按长度精细化切割」的稳定策略。获取与清洗内容Step 1从 state 中提取 Markdown 内容与文件标题统一换行符格式\r\n/\r→\n保证跨平台兼容。按标题语义初切Step 2基于 Markdown 标题语法#~######进行语义级切分自动跳过代码块内的标题匹配避免误切注释保证每个块语义完整。无标题文档兜底处理Step 2 内置若文档无任何标题自动生成默认标题「无主题」确保内容不丢失、流程不中断。超长块递归精细化切割Step 3使用RecursiveCharacterTextSplitter对超过指定长度的语义块进行二次切割按「段落 → 换行 → 句子 → 空格」优先级切割不产生碎片、不硬断句子、无需手动合并。构建标准 Chunk 结构为每个切片补充完整元数据title、content、file_title、parent_title、part序号保证可检索、可溯源。本地备份与状态更新Step 4将切分结果备份到本地chunks.json文件同时将最终 chunks 存入 state供后续向量入库使用。7. 全局配置先定义切分相关的常量方便后续统一调整# 全局配置可根据模型调整 CHUNK_MAX_SIZE 500 # 500 字符触发二次切割 # 单个文本块最大长度控制不超过模型上下文 CHUNK_SIZE 200 # 小值方便测试切割 # 块之间重叠长度保证语义不丢失 CHUNK_OVERLAP 20其中CHUNK_MAX_SIZE是触发二次切割的阈值CHUNK_SIZE与CHUNK_OVERLAP则交给 LangChain 的递归切割器使用。8. 依赖导入import json import os import re from pathlib import Path from typing import Tuple, List, Dict, Any from langchain_text_splitters import RecursiveCharacterTextSplitter from app.core.logger import logger, node_log, step_log from app.import_process.agent.state import ImportGraphState from app.utils.task_utils import add_running_task, add_done_task9. 第一步数据校验与清洗该步骤负责从 state 中取出md_content、file_title和md_path完成非空校验、兜底读取以及换行符统一。step_log(step_1_validate_clean) def step_1_validate_clean(state) - Tuple[str, str]: 1. 获取数据 md_content, file_title, md_path 2. md_content 进行非空校验空 - 异常 3. md_content 不为空 - 数据清洗 - 字符串统一替换 4. file_title 进行非空判断 - 空 - 通过 md_path 获取 file_title 5. 返回数据即可 md_content state[md_content] file_title state[file_title] md_path state[md_path] # 进行必要非空校验 if not md_content: logger.warning(没有从 state 读取到 md_content 内容我们使用 md_path 尝试再次读取) if md_path: md_content Path(md_path).read_text(encodingutf-8) state[md_content] md_content if not md_content: logger.error(md_content 没数据并且尝试读取 md_path 依然没有数据终止执行) raise ValueError(md_content 没数据并且尝试读取 md_path 依然没有数据终止执行) if not file_title: logger.warning(没有在 state 读取到 file_title计划给与默认值) if md_path: file_title Path(md_path).stem if not file_title: file_title default state[file_title] file_title md_content 内容进行清洗\r\n、\r -gt; \n md_content md_content.replace(\r\n, \n).replace(\r, \n) return md_content, file_title/code/pre 这里有两个细节值得注意一是 md_content 缺失时会尝试从 md_path 兜底读取避免上游传参不完整导致流程中断二是统一换行符保证后续按 \n 切行时行为一致。 10. 第二步按标题做语义切分 核心思路是逐行扫描 Markdown识别 # 到 ###### 的标题行并以标题为边界聚合内容。同时需要跳过代码块内部的 #避免把注释误判为标题。 step_log(step_2_data_chunk_title) def step_2_data_chunk_title(md_content, file_title) - List[Dict[str, str]]: 1. 定义正则 2. 根据 \n 进行行的切割 3. 准备数据容器 4. 循环处理每行数据 5. 检查是否进入和退出代码块 6. 判断是不是标题且不在代码块 - 标题处理 7. 不是代码块也不是标题 - 普通行处理 8. 跳出循环后处理最后一个标题的内容 9. 检查没有标题的文档 - title default, md_content 10. 返回结果 # 1. 定义正则 reg re.compile(r^\s*#{1,6}\s.) # 2. 根据 \n 进行行的切割 lines md_content.split(\n) 3. 准备数据容器 chunks [] # 最终结果 current_title None # 记录当前标题 current_title_lines [] # 记录当前标题的行内容 is_code_block False # 是否进入代码块默认没有 chunk_size 0 # 切的块数 4. 循环处理每行数据 for line in lines: # 5. 检查是否进入和退出代码块 if line.startswith() or line.startswith(~~~): is_code_block not is_code_block current_title_lines.append(line) continue # 6. 判断是不是标题且不在代码块 -amp;gt; 标题处理 if reg.match(line) and not is_code_block: # 结算上一个标题的内容 if current_title: chunks.append({ content: \n.join(current_title_lines), title: current_title, file_title: file_title }) # 清空旧数据 current_title line current_title_lines [current_title] chunk_size 1 else: # 7. 不是代码块也不是标题 -amp;gt; 普通行处理 current_title_lines.append(line) 8. 跳出循环后处理最后一个标题的内容 if current_title: chunks.append({ content: \n.join(current_title_lines), title: current_title, file_title: file_title }) 9. 检查没有标题的文档 if chunk_size 0: chunks.append({ content: md_content, title: default, file_title: file_title }) chunk_size 1 10. 返回结果 logger.info(f完成语义切割切块数量{chunk_size}内容{chunks[:3]}) return chunks/code/pre 几个关键点 代码块状态机用 is_code_block 布尔值在 与 ~~~ 之间翻转保证代码里的 # 注释不会被当成标题。 标题即内容首行current_title_lines 初始化为 [current_title]让块内容自带标题便于后续展示与检索。 无标题兜底当 chunk_size 为 0 时整篇文档作为一个 default 块返回。 11. 第三步超长块二次切割 按标题切分后单个块仍可能超过 CHUNK_MAX_SIZE。此时使用 LangChain 的 RecursiveCharacterTextSplitter 做二次切割并为每个子块补充 parent_title 与 part 信息。 step_log(step_3_data_refine_chunk) def step_3_data_refine_chunk(chunks) - List[Dict[str, str]]: 作用将超过指定 size 的标题内容如 1000 字符进行二次切分 二次切分产生 parent_title 与 part。 入参chunks 出参chunks含 part、parent_title 步骤 1. 先定义 LangChain 提供的递归切割器块大小、重叠部分、切割符号 2. 循环原来的 chunks 3. 获取每块的 content 长度 4. 没有超过 - 不需要二次切割 - 补充 part、parent_title 5. 超过 - 递归切割器二次切割 6. 最终记录结果 final_chunks # 1. 定义递归切割器 splitter RecursiveCharacterTextSplitter( chunk_sizeCHUNK_SIZE, chunk_overlapCHUNK_OVERLAP, separators[\n\n, \n, 。, , , , , , ] ) final_chunks [] 2. 循环原来的 chunks for chunk in chunks: content chunk.get(content, ) title chunk.get(title, default) file_title chunk.get(file_title, default) # 3. 获取每块的 content 长度 if len(content) amp;lt; CHUNK_MAX_SIZE: # 4. 没有超过 -amp;gt; 不需要二次切割 final_chunks.append({ content: content, title: title, file_title: file_title, parent_title: title, part: 0 }) continue 5. 超过 -amp;gt; 递归切割器二次切割 sub_texts splitter.split_text(content) for idx, sub_text in enumerate(sub_texts): final_chunks.append({ content: sub_text, title: title, file_title: file_title, parent_title: title, part: idx }) 6. 最终记录结果 logger.info(f完成二次切割最终块数量{len(final_chunks)}) return final_chunks/code/pre 切割符号的顺序体现了「先粗后细」的策略优先按段落切其次按行、句号、感叹号、问号、分号、逗号最后退化到空格与空字符串尽可能保证语义边界完整。 12. 流程总览 flowchart TD A[读取 state: md_content / file_title / md_path] -- B{md_content 是否为空} B -- 是 -- C[尝试从 md_path 读取] C -- D{是否读取成功} D -- 否 -- E[抛出异常终止流程] D -- 是 -- F[统一换行符] B -- 否 -- F F -- G[按标题语义切分] G -- H{单块长度是否超过 CHUNK_MAX_SIZE} H -- 否 -- I[直接保留补充 parent_title 与 part] H -- 是 -- J[递归切割器二次切割] J -- K[补充 parent_title 与 part] I -- L[输出 final_chunks] K -- L 13. 小结 这套切分方案的核心是「两级切割」第一级用标题做语义边界保证主题聚合第二级用递归切割器控制长度保证不超上下文。配合 parent_title 与 part 字段后续无论是做向量检索还是原文回溯都能快速定位到内容所属章节。 实践中还可以继续优化例如把 CHUNK_SIZE 与 CHUNK_OVERLAP 做成配置项或针对代码块单独设置不切割策略避免代码被截断影响可读性。

相关推荐

企业班车供应商怎么选:招标评审真正在看的五件事
企业班车供应商怎么选:招标评审真正在看的五件事

企业班车供应商怎么选:招标评审真正在看的五件事 一份班车招标文件发出去,收回七八份报价,价格差能拉到 30%。评审会上最先被翻开的,往往不是那张报价表。 这不是采购不专业,而是行业里的一个共识:班车是&q… · 2026/11/2 3:10:24

超级具身智能“感知-决策中枢”研究(34):基于FRA架构的Query与安全防御新范式
超级具身智能“感知-决策中枢”研究(34):基于FRA架构的Query与安全防御新范式

前沿技术探索:FRA核心定义与设计理念 FRA(Factorized Robotic Agent, FRA,亦称:因式分解机器人智能体)是一套因式化数实融合一体化新型智能系统,核心设计理念是全链路系统解耦,将机器人作业的感知、推理、动作执行三大核心能力拆解为独立平行子模块,整合为统一、闭环、… · 2026/11/2 3:09:58

论文解读与源码复现|R2C:把房间映射成棋盘,让 LLM 成为低层动作规划器(CVPR 2025)
论文解读与源码复现|R2C:把房间映射成棋盘,让 LLM 成为低层动作规划器(CVPR 2025)

论文解读与源码复现|R2C:把房间映射成棋盘,让 LLM 成为低层动作规划器(CVPR 2025)论文首页题目与摘要截图 一、前言 LLM 做机器人"大脑"做高层规划没问题,但让它直接控制"身体"&#x… · 2026/11/2 5:02:31

Python API自动化测试实战:数据结构与函数设计
Python API自动化测试实战:数据结构与函数设计

1. 项目概述:Python在API自动化测试中的核心价值在接口自动化测试领域,Python凭借其简洁语法和丰富生态已成为事实上的标准工具之一。本系列教程的第三部分将深入讲解Python基础知识中与接口测试强相关的核心概念,这些内容不是孤立的语法学习… · 2026/11/2 21:36:22

NumPy手写前馈神经网络:MNIST训练推理完整闭环
NumPy手写前馈神经网络:MNIST训练推理完整闭环

简介:面向深度学习入门者与课程实验需求,这份资料围绕MNIST手写数字识别任务,提供可直接运行的前馈神经网络训练与测试代码,以及已经训练好的模型权重和完整模型文件,省去从环境搭建到反复调参的等待,适合课… · 2026/11/2 21:36:22

LogViewer:内存映射与行偏移索引实现GB级日志秒开
LogViewer:内存映射与行偏移索引实现GB级日志秒开

简介:LogViewer 是一款专注超大文本快速打开的轻量级阅读工具,面向需要查阅 GB 级日志文件、堆栈记录或数据导出的开发、运维及测试人员,解决常见编辑器打开大文件时卡顿、假死甚至崩溃的问题。压缩包仅 552KB,内含 5 个文件&… · 2026/11/2 21:36:22

LRU与LFU缓存淘汰算法详解与实现
LRU与LFU缓存淘汰算法详解与实现

1. 缓存淘汰算法基础概念在计算机系统中,缓存是一种用空间换时间的设计策略。当缓存空间被占满时,我们需要决定哪些数据应该被保留,哪些数据可以被淘汰——这就是缓存淘汰算法要解决的问题。LRU(Least Recently Used)和… · 2026/11/2 21:34:21

从TCP粘包到半包缓存:C++长度字段编解码与边界验证
从TCP粘包到半包缓存:C++长度字段编解码与边界验证

简介:这份代码包面向具备 C 基础、正在学习网络编程或需要解决 TCP 粘包问题的开发者,提供一套可编译运行的服务器与客户端完整示例。内容围绕收发函数封装、协议头与消息结构体定义、消息回调注册展开,把粘包与丢包处理沉到通信层&#xff0… · 2026/11/2 21:34:21

零售巨头智能采购系统架构与优化实践
零售巨头智能采购系统架构与优化实践

1. 塔吉特采购技术现状与挑战解析 作为全球零售业巨头,塔吉特(Target)每年处理超过500亿美元的采购订单。其采购系统需要对接2,000多家核心供应商和15,000SKU的商品数据。在实际运营中,采购订单成功率受多重因素影响: … · 2026/11/2 21:34:21

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18

雷达信号调制技术解析:从LFM到伪线性调频原理与应用
雷达信号调制技术解析:从LFM到伪线性调频原理与应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 1:01:15

基于MAX14431与TM4C123的工业SPI隔离电路设计全攻略
基于MAX14431与TM4C123的工业SPI隔离电路设计全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 1:01:15

PLFM_RADAR:相位线性调频雷达信号处理框架
PLFM_RADAR:相位线性调频雷达信号处理框架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 1:01:15

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28

STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码