新闻详情

新闻详情

首页 / 资讯中心 / 详情

Langchain4j文档处理实战:从加载到分割的完整指南

发布时间:2026/10/2 13:31:13来源:尧图网络
Langchain4j文档处理实战:从加载到分割的完整指南
做 RAG 项目的这段时间我踩过最深的坑不是向量库选型不是 Embedding 模型调参而是最基础的文档处理环节。文本加载不干净、分割策略不对后面所有环节的效果都会跟着崩。Langchain4j 在 Java 生态里把文档加载Loading和文本分割Splitting封装成了完整链路但我发现很多人在用的时候只知其然照着 Demo 抄一换真实场景就出问题。这篇文章我会把 Langchain4j 文档处理从加载到分割的完整流程拆开讲透包括 Document 对象怎么设计、 Loader 怎么选、几种分割策略到底有什么区别、真实项目中参数应该怎么定以及我在实测中踩过的各种坑。内容围绕 Java Langchain4j但背后的处理思路在 Python 生态里也一样适用。适合正在做知识库问答、私有文档 QA、或者刚开始接触 Langchain4j 的开发者参考。1. 为什么说文档处理是整个 RAG 链路的胜负手1.1 一个容易被忽视的事实模型再好也救不了脏数据RAG 的完整链路是文档加载 → 文本分割 → 向量化 → 存储 → 检索 → 生成。大多数人把精力放在后面几步觉得 Embedding 模型选得好、向量库性能高就行。但实际情况是如果你的原始文档在加载和分割阶段就已经引入了大量噪音后面的检索准确率一定会被拖下水。举一个实测例子。我之前处理一批 PDF 格式的招标文件直接用了默认配置做加载和分割结果问答系统回答“投标截止时间是几点”这类问题时经常答非所问。后来排查才发现PDF 里表格被解析成了混乱文本长段落被生硬截断语义完整的内容被劈成了两半向量化之后根本检索不到有效片段。这就是文档处理环节没做好的典型代价。从原理上讲RAG 的效果上限由检索质量决定而检索质量又由文本块的质量决定。模型只是把检索到的内容做归纳和生成如果检索出来的文本块本身就是残缺的、语义断裂的、夹杂大量页眉页脚噪音的那生成结果自然不可靠。这个因果关系很多人要踩过坑之后才真正理解。1.2 Langchain4j 在这条链路上做了什么Langchain4j 的设计思路是把文档处理抽象成几个可以独立替换的环节Document统一的数据结构包含文本内容和元数据。DocumentParser负责把不同格式的文件解析为纯文本。DocumentLoader负责从不同来源读取文档配合 Parser 使用。DocumentSplitter负责把长文档切分成适合向量化和检索的文本块。这四个概念对应了文档处理的完整流程而且每一步都可以通过自定义实现来扩展。理解这套抽象之后你会发现不管换了什么格式、什么来源核心的处理逻辑都是通用的。这样的设计对 Java 开发者特别友好。Python 生态里 LangChain 虽然功能更全但文档处理部分依赖大量第三方库版本冲突时有发生。Langchain4j 相对轻量同时保持了足够的扩展空间。它没有把文档处理做成一个黑盒而是拆成了几个你可以完全控制的环节这在实际排障时能省很多时间。2. 环境准备依赖引入和版本选择2.1 Maven 依赖怎么加我先说明一点Langchain4j 的模块拆分在不同版本里变化比较大。我下面用 0.35.0 版本为例这是我在生产环境里验证过的组合API 稳定文档也比较齐全。Maven 配置dependency groupIddev.langchain4j/groupId artifactIdlangchain4j/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-document-parser/artifactId version0.35.0/version /dependency dependency groupIddev.langchain4j/groupId artifactIdlangchain4j-document-transformer/artifactId version0.35.0/version /dependency如果你用 Gradle对应写法implementation dev.langchain4j:langchain4j:0.35.0 implementation dev.langchain4j:langchain4j-document-parser:0.35.0 implementation dev.langchain4j:langchain4j-document-transformer:0.35.0这里有个容易混淆的地方核心包 langchain4j 里其实已经包含了 Document、TextSegment 这些基础类但 PDF、DOCX 这些格式的解析器是放在独立模块里的。如果你只引入了核心包调用 ApachePdfBoxDocumentParser 时就会报 ClassNotFoundException。2.2 版本选择别盲目追新也不建议停在太老的版本Langchain4j 迭代速度很快。早期版本里很多 API 是实验性的可能升一个小版本就 deprecated 了。我在项目里吃过这个亏用 0.31.0 写好的代码升到 0.35.0 之后DocumentSplitters 类的方法签名发生了变化导致编译报错。我的建议是新项目直接选当前稳定的最新版本但锁死在 pom 里不要频繁升级。如果你是在维护老项目升级前先看 Release Notes 里的 breaking changes重点检查这四类 API 是否变动模块升级重点检查项langchain4j-coreDocument、TextSegment、Metadata 的构造方式document-parser各类 Parser 的类名、包路径document-transformer分割器的工厂方法和参数顺序各集成模块向量库、嵌入模型的客户端初始化方式2.3 JDK 版本要求Langchain4j 要求 JDK 8 以上但如果你用的是 JDK 8很多高级特性用不了。我在本地开发用的是 JDK 17生产环境也是 17实测没遇到兼容性问题。如果你的项目还在 JDK 8 上建议至少确认一下依赖树里有没有引入需要 JDK 11 以上才能运行的传递依赖。提示文档处理本身不依赖 AI 服务所以这一阶段可以完全离线运行。调试时不需要配 API Key你可以在本地写单元测试直接验证加载和分割的结果。3. 文档加载从文件、URL 到各类格式的接入姿势3.1 先理解 Document 数据结构在 Langchain4j 里一个文档被抽象为 Document 对象核心就两个部分text()文档全文文本。metadata()文档的元数据比如文件名、来源 URL、页码、标题等。元数据这一点容易被忽视但它非常关键。分割后的每个文本块都会继承所属文档的元数据后续做检索时你可以根据元数据过滤来源、定位页码甚至做权限控制。我见过不少人在加载阶段完全不管 Metadata等到要按来源过滤时才发现无法实现只能回头改代码。创建 Document 最简单的方式Document doc Document.from(项目背景...全文内容..., Metadata.from(Map.of( source, 招商公告.txt, category, 公告 )));3.2 从文件系统加载FileSystemDocumentLoader最常用的入口是 FileSystemDocumentLoader。它接受路径和 Parser返回 Document 列表。Path path Paths.get(/data/docs/); ListDocument documents FileSystemDocumentLoader.loadDocumentsRecursively(path);这个方法会自动递归扫描目录并按照文件扩展名匹配对应的 Parser。默认情况下它支持 txt、html、pdf、docx 等常见格式。如果文件格式不常见或者你想强制指定解析器可以这样写ListDocument documents FileSystemDocumentLoader.loadDocumentsRecursively( path, new ApachePdfBoxDocumentParser() );这里有个细节loadDocumentsRecursively 加载的是目录下所有可解析文件而 loadDocument 是加载单个文件。如果你只想处理特定前缀或后缀的文件建议先自己过滤文件列表再逐个加载。3.3 其他来源URL、类路径除了文件系统Langchain4j 还提供了 UrlDocumentLoader可以从 HTTP 地址加载文档。这个在做实时文档同步时很好用Document doc UrlDocumentLoader.load( URI.create(https://example.com/document.html).toURL(), new HtmlTextDocumentParser() );我个人很少直接用 UrlDocumentLoader因为真实业务中网页抓取往往需要处理登录、超时、重定向等问题这些它都管不了。更好的做法是自己写一个简单的抓取逻辑拿到 HTML 字符串后再交给 Parser 处理。比如把 HttpClient 请求到的响应体字符串直接构造成 Document再走后续的清洗和分割流程。还有一种常见需求是从 classpath 或项目资源目录加载文件。这其实不用专门的方法直接用 FileSystemDocumentLoader 指到 resources 目录即可。或者你用 Java 自身的方式读取资源流再转成字符串后构造 Document。3.4 格式解析PDF、DOCX、CSV 怎么选 Parser不同格式需要不同的 DocumentParser。Langchain4j 提供了一系列开箱即用的 Parser格式Parser 类备注TXTTextDocumentParser直接读文本HTMLHtmlTextDocumentParser去除标签提取正文PDFApachePdfBoxDocumentParser基于 PDFBoxDOCXApachePoiDocumentParser基于 POICSVCsvDocumentParser逐行拆分XLSXApachePoiDocumentParser表格形式我实测下来PDF 是最容易出问题的格式。ApachePdfBoxDocumentParser 对扫描版 PDF纯图片无能为力解析出来是空文本对复杂排版多栏、表格、页眉页脚的解析结果也不理想。如果你遇到的 PDF 是扫描版必须先加 OCR 环节或者换用其他文档预处理方案。DOCX 相对好一些Apache POI 对 Word 文档的段落结构解析得比较完整。但如果文档里有复杂的嵌套表格也会出现内容顺序错乱的问题。遇到这类文档我一般先手动打开看看结构再决定是不是要用自定义 Parser 做后处理。3.5 自定义 DocumentParser 的场景Langchain4j 的 Parser 接口很简单核心就是文档输入流转 Documentpublic interface DocumentParser { Document parse(InputStream inputStream); }自定义 Parser 主要出现在两种场景一是专有格式。比如你有一套 .tmpl 模板文件内容是自定义语法需要定制解析规则。二是需要做预处理。比如解析 PDF 时把页眉页脚去掉、把图表说明和正文分离这些逻辑放在 Parser 里最合适。public class CustomPdfParser implements DocumentParser { private final ApachePdfBoxDocumentParser delegate new ApachePdfBoxDocumentParser(); Override public Document parse(InputStream inputStream) { Document document delegate.parse(inputStream); String cleanedText cleanText(document.text()); return Document.from(cleanedText, document.metadata()); } private String cleanText(String text) { // 去掉页眉页脚、合并多余空行、修正全角字符 return text; } }这里的原则是Parser 层做的是“从原始格式到干净文本”的转换不要在这里做业务相关的筛选逻辑。业务筛选放到后面加载完成之后统一处理才能保持职责清晰。4. 文档分割决定问答效果好坏的核心环节4.1 为什么要分割以及分割的评判标准一个大模型有上下文窗口限制不可能把整本手册一次塞进去。即使某些模型的上下文窗口大到可以装下整本书检索时也会因为文本粒度过粗而命中大量无关内容。分割质量直接影响两个指标召回率重要信息有没有被切出来并被正确向量化。精准率检索到的文本块是不是真的包含答案而不是夹杂大量无关信息。一个好的分割应该尽量保持语义完整性。也就是说一个文本块尽量是一个完整的话题、一个完整的段落、最好是一个完整的意思单元。从工程角度讲还要考虑块的大小是否适合向量化——太大检索效果差太小又丢失上下文。我见过一个典型反例有人把一份操作手册按 100 字切块结果“打开配置文件”和“修改参数项”这种前后依赖的动作被切到了不同块里问答系统回答“怎么修改参数”时只能检索到后半部分完全不知道要先打开配置文件。这就是分割粒度过细导致的语义断裂。4.2 Langchain4j 里的分割器体系Langchain4j 里DocumentSplitter 和 TextSegment 是分割环节的核心。DocumentSplitter 负责把 Document 切成多个 TextSegment每个 TextSegment 包含一段文本和对应的元数据。内置的分割器都通过 DocumentSplitters 这个工厂类创建。最常用的是 recursiveDocumentSplitter splitter DocumentSplitters.recursive( 300, // 每个块的字符数 50 // 相邻块之间的重叠字符数 );这个接口签名很好理解——目标块大小和重叠大小。recursive 的意思是它会尝试按段落、句子、词的顺序逐级寻找切割点优先在语义边界比较自然的地方切而不是在硬编码的固定位置暴力截断。4.3 不同分割策略的适用场景对比策略特点适用场景recursive兼顾语义和长度按段落/句子/词逐级切泛用性最强默认推荐按固定字符数实现简单块大小非常均匀对块大小有严格限制的场景按段落保持段落完整语义边界清晰段落较短的说明文档、条款按句子保留句子完整性适合短文本拼接FAQ、原句回答类任务以我自己的项目经验recursive 在大多数场景下是最好用的。它不是那种“数够 300 字就一刀切”的粗暴逻辑而是会去文档里找段落换行、句号等自然边界尽量让每个文本块读起来像一段完整的话。当然recursive 也不是银弹。如果你的文档本身段落很短比如条款类文档每条就一两句话按段落切可能比 recursive 更合适。反之如果你的文档是连续的大段文字recursive 会在句子边界处切分效果也不错。4.4 参数到底怎么定这是被问得最多的问题块大小和重叠到底应该设多少没有标准答案但我可以给出一个参考思路。先把选型维度拆清楚Embedding 模型的影响不同的 Embedding 模型对输入长度有限制通常以 token 数计算。比如某些模型最大输入是 512 token那就意味着你的文本块不能超过这个上限。中文字符和 token 的换算比例大约是 1 个汉字 ≈ 1~2 个 token具体要看模型。检索目标的影响如果你的系统是让用户做文档问答块可以稍大一些比如 500~800 字符这样上下文信息足。如果做的是片段匹配、短句检索块宜小比如 100~200 字符。重叠量的影响重叠是为了避免信息恰好落在切分边界上导致丢失。一般取块大小的 10%~20% 即可。比如块大小 500 字符重叠 50~100 字符。我之前做过一次对照实验。同一批 PDF 文档一组用 300/50 切分一组用 1000/200 切分问了 30 个业务问题结果前者准确率明显更高。原因是这批文档本身就是问答段落结构小一点的分块恰好能保留完整的问答对。换了另一批长文档300/50 的反而不如 800/100。所以我的结论是参数没有银弹但按照“先按语义边界切再按长度约束合并”的思路来调效率会高很多。4.5 从 Document 到 TextSegment 之后会发生什么分割完成之后得到的是 List 。每个 TextSegment 拥有 text() 和 metadata()metadata 来自原始 Document同时还会自动打上索引信息。实际项目中接下来一般就是把 TextSegment 送入 Embedding 模型生成向量然后存入向量库。这里有一个容易被忽略的点TextSegment 的文本长度应该和 Embedding 模型的输入限制匹配否则超长截断会破坏语义。ListTextSegment segments splitter.split(document); for (TextSegment segment : segments) { ListFloat vector embeddingModel.embed(segment.text()).content(); // 存入向量库 }我自己是在这个阶段统一做文本清洗的比如去掉纯空白段落、合并过短的分块、过滤掉长度小于 20 字符的无效片段。因为分割器产生的粒度不一定完美后处理能校正一部分问题。5. 完整链路PDF 导入到分块入库的实战走通5.1 业务场景和预期效果用一个我实际做过的场景一个政策文件库输入一批 PDF 政策文件最终实现的效果是用户可以提问“某政策对小微企业补贴额度是多少”系统自动从文档中检索并生成回答。我这里不接入真实的大模型和向量库而是把链路走到“分割完成并打印分块信息”这一步方便你在本地直接复现。实际项目里把最后一段替换成 Embedding 向量库即可。5.2 完整代码import dev.langchain4j.data.document.Document; import dev.langchain4j.data.document.DocumentSplitter; import dev.langchain4j.data.document.Metadata; import dev.langchain4j.data.document.loader.FileSystemDocumentLoader; import dev.langchain4j.data.document.parser.apache.pdfbox.ApachePdfBoxDocumentParser; import dev.langchain4j.data.document.splitter.DocumentSplitters; import dev.langchain4j.data.segment.TextSegment; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; public class DocumentProcessDemo { public static void main(String[] args) { // 1. 加载目录下的所有 PDF 文件 Path pdfDir Paths.get(/data/policy_docs); ApachePdfBoxDocumentParser parser new ApachePdfBoxDocumentParser(); ListDocument documents FileSystemDocumentLoader.loadDocumentsRecursively(pdfDir, parser); System.out.println(加载文档数量 documents.size()); // 2. 遍历每个文档做基础清洗后分割 DocumentSplitter splitter DocumentSplitters.recursive(500, 80); for (Document doc : documents) { // 打印文档基本信息 System.out.println(文档来源 doc.metadata().getString(file_name)); System.out.println(文档总字数 doc.text().length()); // 分割 ListTextSegment segments splitter.split(doc); System.out.println(切分块数 segments.size()); // 3. 打印每个分块的前 100 个字符方便直观检查切分质量 for (int i 0; i segments.size(); i) { TextSegment segment segments.get(i); String preview segment.text().length() 100 ? segment.text().substring(0, 100) ... : segment.text(); System.out.printf([%d] 长度%d, 预览%s%n, i, segment.text().length(), preview.replace(\n, )); } } } }5.3 运行结果和效果分析我在本地用 3 份 PDF 政策文件跑过这个流程。加载环节没有问题3 份 PDF 都被正确识别。分割环节用了 500/80 的参数其中一份文件全文 3800 多字被切成 9 块。每一块基本对应文档中的一个完整章节内容。有些段落比较长被跨块切开了但重叠的 80 个字符保证了下文起始信息和上文有衔接。直观效果可以看输出里的长度和预览重点是检查有没有出现“一块里面前半部分是上一章、后半部分是下一章”的情况。如果出现说明段落边界没有正确识别这时候我会先去检查 PDF 解析出的原始文本里换行符是否完整——很多 PDF 导出的文本换行符位置和视觉排版不一致会导致分割器找不到正确的段落断点。5.4 接入向量库的路子上面的例子到分割就结束了。实际项目里接下来的代码一般是EmbeddingModel embeddingModel OpenAiEmbeddingModel.builder() .apiKey(System.getenv(OPENAI_API_KEY)) .modelName(text-embedding-3-small) .build(); EmbeddingStoreTextSegment store new InMemoryEmbeddingStore(); for (TextSegment segment : segments) { Embedding embedding embeddingModel.embed(segment.text()).content(); store.add(embedding, segment); }这里只是示意真正的生产环境要考虑 EmbeddingStore 的持久化、批量处理的限流、失败重试等。但核心逻辑就是这样分割之后直接走向量化没有多余步骤。6. 实测下来最值得注意的几个坑6.1 中文编码问题这是最基础的坑但出现频率极高。加载文本文件时如果文件是 GBK 编码而默认的 TextDocumentParser 按 UTF-8 读取就会出现乱码。解决办法有两个一是把源文件统一转成 UTF-8二是自定义 Parser在读取输入流时指定字符集DocumentParser parser inputStream - { String text new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); return Document.from(text); };注意上面这个实现忽略了 Metadata。实际用的时候建议加上来源信息。另外一个技巧是写完自定义 Parser 之后先用一段包含生僻字、全角标点的样本文本做单元测试防止“看起来能跑但实际有乱码”的问题。6.2 PDF 解析出来的文本可能“不是你以为的样子”PDFBox 解析的结果是多个文本片段按照内部结构拼接的而不是按人的阅读顺序重排。常见问题包括表格内容被横向拼接到正文里读起来像乱码。页眉页脚重复出现在每个文本块里污染检索结果。多栏排版的文档左右栏内容交错在一起。扫描版 PDF 根本没有文本层解析结果为空字符串。应对方式对于页眉页脚可以在加载后用字符串匹配的方式批量去掉对于扫描版只能走 OCR对于多栏、复杂表格最省事的办法是优先让业务方提供 Word 版本或带书签的数字化 PDF虽然这个建议听起来很“需求侧”但在实际落地中确实是最有效的。6.3 分割之后信息丢了一半很多人会遇到一个问题分割前 Document 里有 Metadata分割后 TextSegment 里的 Metadata 也还在但自己在查询阶段想按来源过滤时发现 filter 条件不生效。排查步骤是先打印 TextSegment 的 metadata 内容。Langchain4j 在分割时会把继承自 Document 的 Metadata 复制到每个 Segment但如果你在构造 Document 时就没有设置 metadata这里自然为空。另外有些集成模块在往向量库写入时需要手动指定哪些 metadata 要存入索引否则查询时拿不到。我的建议是加载文档后先统一补充元数据比如文档名、类别、上传时间再走分割。这个习惯能省掉后面很多麻烦。6.4 空白文本和超短分块数据处理中最隐形的杀手是空白文本。PDF 解析失败返回空文本时后面的分割、向量化不会报错但会生成一个零向量或者噪音向量拉低整体检索准确率。我的做法是在分割后的循环里加过滤ListTextSegment validSegments segments.stream() .filter(seg - seg.text().trim().length() 20) .toList();这个 20 字符是经验值。太短的文本块基本没有语义向量化之后和其他文本的相似度也不稳定留着只会添乱。6.5 调试技巧本地先跑通再接外部服务文档处理阶段不应该依赖任何外部 AI 服务。我在开发时经常做的一件事是把加载和分割写成一个简单的测试用例用本地文件作为输入直接打印分块结果肉眼检查切分质量。这样迭代速度很快而且不消耗任何 API 调用。等到切分质量稳定了再接入 Embedding 和向量库。这个顺序符合“先保证输入质量再做下游优化”的原则也能避免把问题定位到错误层次。就我自己的使用体验来说Langchain4j 的文档处理链路设计得足够清晰但越清晰的框架越考验使用者的数据处理意识。加载和分割看似只是管线前两步实际上决定了整个 RAG 应用能在多大程度上发挥价值。同一个模型、同一个向量库在文档处理上花一小时和花一秒最终效果可能天差地别。最后分享一个我在项目中养成的习惯每次拿到一批新文档我不会直接开写代码而是先手动抽样看两三份搞清楚格式、排版、噪声源在哪里再决定用哪个 Parser、怎么清洗、分割参数取多少。这一步看起来费时间实际上是排掉最多坑的投入。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

AI Engineering from Scratch:产线级AI系统手工锻造指南 2026/10/2 14:12:24

AI Engineering from Scratch:产线级AI系统手工锻造指南

1. 这不是“搭积木”,而是亲手锻造AI系统的底层逻辑“AI Engineering from Scratch”——看到这个标题,很多人第一反应是:又要从零写Transformer?又要手推反向传播?其实完全不是。我带过六支AI工程团队,从金…

阅读更多 →
国产MCU实战对比:STM32、GD32与CH32V103在真实项目中的表现与选型 2026/10/2 14:12:24

国产MCU实战对比:STM32、GD32与CH32V103在真实项目中的表现与选型

1. 从一块“不听话”的板子说起去年这个时候,我手里同时开着三块开发板:一块STM32F103C8T6的最小系统板,一块GD32F103C8T6的替代板,还有一块CH32V103C8T6的RISC-V评估板。三块板子引脚基本兼容,价格却差了一大截。当时…

阅读更多 →
【保姆级教程】用 WeChat 3.9 + Memotrace + Claude Code 复现前任 Skills:把 settings 改到 TaoToken 2026/10/2 14:12:24

【保姆级教程】用 WeChat 3.9 + Memotrace + Claude Code 复现前任 Skills:把 settings 改到 TaoToken

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

阅读更多 →
燃油效率与CO2排放数据集:从回归预测到特征工程的完整实战解析 2026/10/2 14:12:24

燃油效率与CO2排放数据集:从回归预测到特征工程的完整实战解析

在做车辆能耗分析或者机器学习回归练手项目的时候,很多朋友都会遇到一个尴尬:想找个真实、干净、又足够经典的数据集,翻来覆去不是收费就是结构混乱。今天聊的这个数据集,是我个人特别推崇的一个入门到进阶都适用的真实项目——汽…

阅读更多 →
反无人机自动防御系统实战:立体防护与智能反制技术详解 2026/10/2 14:12:24

反无人机自动防御系统实战:立体防护与智能反制技术详解

近两年低空安防的需求变化特别快,从大型活动保障到关键基础设施防护,“低慢小”目标的威胁已经不再是纸上谈兵的概念,而是每一天都在发生的现实挑战。我参与过几套反无人机自动防御系统的部署项目,从方案设计、设备选型到现场调试…

阅读更多 →
记忆系统与 Agent 定制完全指南(七):Agent 与工具的深度集成——把 MCP 配置改到 TaoToken 2026/10/2 14:12:17

记忆系统与 Agent 定制完全指南(七):Agent 与工具的深度集成——把 MCP 配置改到 TaoToken

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

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉