Spring AI与Ollama集成实战:构建智能文本处理微服务
在实际工程和开发场景中“Making”这个词往往被用来指代从零开始构建、组装或实现某个功能、系统或产品的全过程。它不仅仅是写代码还包括需求理解、技术选型、环境搭建、模块设计、编码实现、测试验证、部署上线以及后续的维护优化。对于开发者而言能否高效、可靠地完成一个“Making”过程直接决定了项目的质量和交付效率。当前人工智能AI技术正深度融入软件开发的各个环节从代码生成、测试到系统设计AI 辅助工具和框架如 Spring AI、各类 AI Agent、AI 编程插件正在改变传统的“Making”模式。然而工具本身并不能替代对底层原理、工程规范和问题排查能力的掌握。本文将围绕一个典型的“Making”流程展示如何在一个具体的技术场景例如构建一个具备 AI 能力的微服务端点中系统性地应用工程实践并重点阐述在集成 AI 组件时需要注意的关键细节、常见陷阱及排查方法。目标是让读者不仅能按步骤完成一个可运行的原型更能理解每一步背后的设计考量具备独立解决实际问题的能力。1. 明确目标与技术选型我们要“Make”什么在开始任何动手操作之前必须清晰地定义项目目标。模糊的需求是项目失败的主要根源之一。1.1 定义项目范围假设我们要构建一个简单的文本处理微服务它提供一个 REST API 接口。用户向该接口发送一段文本服务端调用一个 AI 模型例如用于文本摘要或情感分析进行处理并将结果返回给用户。这个目标看似简单但已经包含了 Web 服务搭建、AI 模型集成、API 设计等多个核心环节。1.2 技术栈选型及理由技术选型需要权衡学习成本、社区生态、性能要求和团队技术储备。Web 框架Spring Boot理由Java 生态中事实上的标准提供快速启动、自动配置和丰富的 Starter 依赖能极大简化 Web 服务的开发。与 Spring AI 等项目无缝集成。AI 模型集成Spring AI理由Spring 官方项目旨在简化 AI 功能在 Spring 应用中的集成。它提供了对多种 AI 模型提供商如 OpenAI, Azure OpenAI, Ollama 本地模型的抽象允许通过更换配置而非代码来切换模型符合 Spring 的编程习惯。项目管理与构建Maven理由在 Java 生态中广泛使用依赖管理清晰。本地 AI 模型可选Ollama理由对于不想依赖外部 API 或需要离线运行的场景Ollama 可以方便地在本地部署和运行大型语言模型LLMSpring AI 也提供了对它的支持。选型对比表组件选项一选项二本次选择理由Web 框架Spring BootQuarkus / Micronaut生态更成熟与 Spring AI 集成度最高AI 集成Spring AI直接调用 OpenAI SDK抽象程度高便于切换模型符合 Spring 风格构建工具MavenGradle受众更广配置文件结构简单明了本地模型Ollama直接使用 Hugging Face Transformers部署和使用更简单适合快速原型验证基于以上选型我们的项目雏形是一个基于 Spring Boot Spring AI 的 RESTful 服务可能使用 Ollama 提供的本地模型或云上的 AI 服务。2. 环境准备与项目初始化“工欲善其事必先利其器”。一个稳定、一致的开发环境是后续所有工作的基础。2.1 基础环境配置Java Development Kit (JDK)推荐使用 JDK 17 或 21LTS 版本。检查安装java -versionMaven确保 Maven 已安装并可用的。mvn -versionIDE集成开发环境IntelliJ IDEA推荐或 Eclipse。确保已安装并配置好 Maven 和 JDK。Ollama可选用于本地模型访问 Ollama 官网下载并安装。安装完成后拉取一个轻量级模型例如llama3.1:8bollama pull llama3.1:8b启动模型服务ollama run llama3.1:8b服务默认运行在http://localhost:11434。保持此终端运行。2.2 创建 Spring Boot 项目使用 Spring Initializr 快速生成项目骨架。访问 start.spring.io 。选择Project:MavenLanguage:JavaSpring Boot:选择最新的稳定版如 3.3.0Project Metadata:Group:com.exampleArtifact:ai-text-servicePackaging:JarJava:17 或 21Dependencies:添加Spring Web和Spring AI。注意在 Initializr 上可能无法直接找到Spring AI因为它相对较新。如果找不到可以手动在生成的pom.xml中添加依赖。点击 “Generate” 下载项目压缩包并解压到工作目录。2.3 检查与调整 Maven 依赖解压后用 IDE 打开项目。检查pom.xml文件。由于 Spring AI 可能不在 Initializr 默认列表中我们需要手动添加。完整的pom.xml关键部分示例?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.0/version relativePath/ /parent groupIdcom.example/groupId artifactIdai-text-service/artifactId version0.0.1-SNAPSHOT/version nameai-text-service/name descriptionDemo project for Spring AI integration/description properties java.version17/java.version spring-ai.version1.0.0-M3/spring-ai.version !-- 使用当时最新的稳定版本 -- /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Spring AI Starter -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency !-- 使用 Ollama 提供商 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-ollama-spring-boot-starter/artifactId /dependency !-- 测试依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project关键解释spring-ai.version必须明确指定且需要与你的 Spring Boot 版本兼容。请查阅 Spring AI 官方文档获取版本映射关系。spring-ai-bom引入 BOMBill of Materials来统一管理所有 Spring AI 相关依赖的版本避免冲突。spring-ai-ollama-spring-boot-starter这是为 Ollama 定制的 Starter它会自动配置与本地 Ollama 服务通信所需的客户端。3. 核心代码实现构建 AI 文本处理服务项目骨架搭建好后开始编写业务逻辑。我们将遵循 Controller-Service 的经典分层模式。3.1 配置 AI 模型连接首先需要在src/main/resources/application.properties中配置 Spring AI 如何连接到我们的模型以 Ollama 为例。# 应用服务器端口 server.port8080 # Spring AI Ollama 配置 spring.ai.ollama.base-urlhttp://localhost:11434 # Ollama 服务地址 spring.ai.ollama.chat.modelllama3.1:8b # 指定要使用的模型名称必须与 Ollama 拉取的模型一致重要如果使用 OpenAI 等云服务则需要配置 API Key 和 Base URL格式类似spring.ai.openai.api-keyyour-key。Spring AI 的配置属性因提供商而异。3.2 定义 API 请求和响应模型创建简单的 Java 类来表示 API 的输入和输出。这有助于保持代码的清晰度和类型安全。src/main/java/com/example/aitextservice/model/AiRequest.javapackage com.example.aitextservice.model; public class AiRequest { private String message; // 默认构造函数为 JSON 反序列化所必需 public AiRequest() { } public AiRequest(String message) { this.message message; } // Getter 和 Setter public String getMessage() { return message; } public void setMessage(String message) { this.message message; } }src/main/java/com/example/aitextservice/model/AiResponse.javapackage com.example.aitextservice.model; public class AiResponse { private String result; public AiResponse() { } public AiResponse(String result) { this.result result; } public String getResult() { return result; } public void setResult(String result) { this.result result; } }3.3 创建 AI 服务层服务层负责封装与 AI 模型交互的复杂逻辑。这里我们将注入 Spring AI 提供的ChatClient。src/main/java/com/example/aitextservice/service/AiTextService.javapackage com.example.aitextservice.service; import org.springframework.ai.chat.client.ChatClient; import org.springframework.stereotype.Service; Service public class AiTextService { private final ChatClient chatClient; // 通过构造函数注入 ChatClient public AiTextService(ChatClient chatClient) { this.chatClient chatClient; } public String processText(String userMessage) { // 使用 ChatClient 发起对话请求 String aiResponse chatClient.prompt() .user(userMessage) .call() .content(); return aiResponse; } }代码解释Service标记这是一个 Spring 管理的服务类 Bean。ChatClient这是 Spring AI 的核心接口提供了流式和非流式两种方式与 AI 模型交互。这里使用简单的非流式调用。chatClient.prompt().user(...).call().content()这是一个流畅的 API 调用链。prompt()开始构建提示。user(userMessage)设置用户输入的消息。call()执行调用返回一个Response对象。content()从响应中提取 AI 生成的文本内容。3.4 创建 REST 控制器控制器层接收 HTTP 请求调用服务层并返回 HTTP 响应。src/main/java/com/example/aitextservice/controller/TextController.javapackage com.example.aitextservice.controller; import com.example.aitextservice.model.AiRequest; import com.example.aitextservice.model.AiResponse; import com.example.aitextservice.service.AiTextService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; RestController public class TextController { private final AiTextService aiTextService; public TextController(AiTextService aiTextService) { this.aiTextService aiTextService; } PostMapping(/api/process) public AiResponse processText(RequestBody AiRequest request) { String result aiTextService.processText(request.getMessage()); return new AiResponse(result); } }代码解释RestController组合了Controller和ResponseBody表示这个类的所有方法返回的数据直接写入 HTTP 响应体通常是 JSON。PostMapping(/api/process)将该方法映射到 HTTP POST 请求路径为/api/process。RequestBody AiRequest request将请求体中的 JSON 数据自动反序列化为AiRequest对象。控制器方法非常简单接收请求 - 调用服务 - 返回响应。这种清晰的分层有利于测试和维护。4. 运行验证与结果分析代码写完后的第一步不是直接上线而是在本地进行充分的验证。4.1 启动应用确保 Ollama 服务如果使用仍在运行 (ollama run llama3.1:8b)。在 IDE 中右键点击主应用类通常名为AiTextServiceApplication选择 “Run”。或在项目根目录下使用 Maven 命令mvn spring-boot:run观察控制台日志如果没有错误Spring Boot 应用应该成功启动在 8080 端口。4.2 测试 API 接口使用curl、Postman 或任何 HTTP 客户端工具进行测试。使用curl命令测试curl -X POST http://localhost:8080/api/process \ -H Content-Type: application/json \ -d {message: 请用一句话解释什么是人工智能}预期成功的响应{ result: 人工智能是计算机科学的一个分支旨在创造能够执行通常需要人类智能的任务的机器或软件。 }实际返回内容会因模型和随机性而略有不同4.3 验证关键检查点应用启动日志中无异常显示 “Started AiTextServiceApplication in X seconds”。依赖注入确保ChatClient和AiTextService被正确创建和注入没有BeanCreationException。API 可达性HTTP POST 请求返回 200 OK 状态码。业务逻辑正确性AI 返回了与输入问题相关的、连贯的文本内容。5. 常见问题排查踩坑记录在实际“Making”过程中几乎一定会遇到问题。以下是集成 Spring AI 和 Ollama 时常见的坑及其解决方案。5.1 连接与配置问题问题现象可能原因检查与解决步骤启动报错Connection refused1. Ollama 服务未启动。2.application.properties中的base-url配置错误。1. 检查 Ollama 进程是否运行 (ps aux启动报错Model llama3.1:8b not found1. 模型名称拼写错误。2. 模型未成功拉取到本地。1. 检查application.properties中的model名称确保与ollama list命令输出中的名称完全一致。2. 执行ollama pull llama3.1:8b重新拉取模型。调用 API 返回 500 错误日志显示Read timed outAI 模型生成响应时间过长超过默认 HTTP 超时时间。1.调整超时设置在application.properties中增加spring.ai.ollama.chat.options.timeout60s。2.优化提示词要求模型回复更简洁。5.2 依赖与版本问题问题现象可能原因检查与解决步骤Maven 构建失败无法解析spring-ai-*依赖1.spring-ai.version不正确或不存在。2. Maven 仓库如 Maven Central尚未同步该版本。1. 访问 Spring AI 官方文档 确认最新稳定版本号。2. 检查项目的pom.xml中spring-ai.version是否正确。3. 尝试使用mvn clean compile -U(-U强制更新快照依赖)。启动时报ClassNotFoundException或NoSuchMethodError依赖版本冲突特别是 Spring Boot 和 Spring AI 版本不兼容。1. 查阅 Spring AI 文档的版本兼容性矩阵。2. 使用mvn dependency:tree命令分析依赖树排除冲突的传递依赖。5.3 业务逻辑问题问题现象可能原因检查与解决步骤AI 回复内容不相关或质量差提示词User Message不够清晰或具体。1.优化提示词工程在AiTextService中构建更明确的指令。例如将userMessage修改为请作为一位技术专家用通俗易懂的语言解释以下概念 userMessage。2. 调整 AI 模型的参数如temperature控制随机性可通过ChatClient的选项设置。6. 从原型到生产最佳实践与扩展方向一个能在本地跑通的 Demo 离生产级应用还有很大距离。以下是在真实项目中必须考虑的事项。6.1 配置管理外置化问题将模型地址、API Key 等敏感信息硬编码在application.properties中是不安全的也无法适应不同环境开发、测试、生产。实践使用application-{profile}.properties或application.yml区分环境配置。将敏感信息放入环境变量或云平台的密钥管理服务如 AWS Secrets Manager, Kubernetes Secrets。示例在application-prod.properties中引用环境变量。spring.ai.ollama.base-url${OLLAMA_BASE_URL:http://localhost:11434} # 如果使用 OpenAI则 # spring.ai.openai.api-key${OPENAI_API_KEY}6.2 增强鲁棒性异常处理当前代码一旦 AI 服务不可用整个 API 会失败。应添加全局异常处理ControllerAdvice返回友好的错误信息而不是 500 状态码。熔断与降级使用 Resilience4j 或 Sentinel 等库当 AI 服务持续不可用时快速失败或返回预设的默认值避免雪崩效应。日志与监控添加详细的日志记录输入、输出、耗时并集成 Micrometer 将指标如请求量、延迟、错误率暴露给 Prometheus 和 Grafana 等监控系统。6.3 性能与成本优化缓存对于相同或相似的查询可以考虑将 AI 响应结果缓存起来使用 Redis 或 Caffeine在一定时间内直接返回缓存结果显著降低延迟和 AI API 调用成本。异步处理如果处理耗时很长可以考虑将请求放入消息队列如 RabbitMQ, Kafka立即返回一个“已接收”的响应然后由后台工作者异步处理并通过其他方式如 WebSocket通知客户端结果。6.4 扩展功能流式响应Streaming修改服务层和控制器支持 Server-Sent Events (SSE)让 AI 生成的内容可以像 ChatGPT 那样逐字返回提升用户体验。多模型路由根据请求的内容、复杂度或成本要求动态选择不同的 AI 模型如简单问题用便宜的小模型复杂问题用能力强的大模型。结构化输出利用 AI 模型的 Function Calling 或 JSON Mode 能力要求模型直接返回结构化的 JSON 数据便于程序后续处理。通过以上步骤我们完成了一个完整的“Making”周期从目标定义、技术选型、环境准备、代码实现、验证测试到问题排查和生产级优化思考。这个流程和其中蕴含的工程化思维可以复制到绝大多数软件开发项目中。真正的“Making”能力正是在这样一次次的实践、踩坑和总结中积累起来的。

相关新闻

Wayfinder Router:构建混合AI架构的智能路由解决方案

Wayfinder Router:构建混合AI架构的智能路由解决方案

当你需要在本地部署的模型和云端托管服务之间智能分配AI查询时,是否经常面临选择困难?本地模型虽然数据安全可控,但能力有限;云端服务功能强大,却存在延迟、成本和隐私顾虑。这种"本地还是云端"的二元选择&a…

2026/10/4 14:30:44 阅读更多 →
如何让老旧安卓电视重获新生:mytv-android电视直播软件的终极优化指南

如何让老旧安卓电视重获新生:mytv-android电视直播软件的终极优化指南

如何让老旧安卓电视重获新生:mytv-android电视直播软件的终极优化指南 【免费下载链接】mytv-android 使用Android原生开发的视频播放软件 项目地址: https://gitcode.com/gh_mirrors/my/mytv-android 还在为家中老旧智能电视播放卡顿而烦恼吗?my…

2026/10/7 6:19:02 阅读更多 →
Blender 3MF插件:5分钟搞定3D打印工作流优化 [特殊字符]

Blender 3MF插件:5分钟搞定3D打印工作流优化 [特殊字符]

Blender 3MF插件:5分钟搞定3D打印工作流优化 😊 【免费下载链接】Blender3mfFormat Blender add-on to import/export 3MF files 项目地址: https://gitcode.com/gh_mirrors/bl/Blender3mfFormat 还在为3D打印工作流中的格式转换烦恼吗&#xff1…

2026/10/10 21:23:00 阅读更多 →

最新新闻

YOLOv3电塔绝缘子检测权重:快速推理与微调实战

YOLOv3电塔绝缘子检测权重:快速推理与微调实战

简介:这是一份面向电力设施智能监测场景的YOLOv3电塔绝缘子检测训练成果包,适合目标检测初学者、电力巡检算法开发者及需要小目标检测参考方案的技术人员使用。资源包共990个文件,约520.07MB,包含json标注数据、png与jpg图像样本、…

2026/10/14 1:38:10 阅读更多 →
单链表头尾插法:带尾指针的工业级实现

单链表头尾插法:带尾指针的工业级实现

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

2026/10/14 1:38:10 阅读更多 →
编译原理试卷A考点剖析:从词法分析到代码生成的完整复习链路

编译原理试卷A考点剖析:从词法分析到代码生成的完整复习链路

简介:常州工学院《编译原理》试卷A是一份面向计算机专业学生的期末试题,覆盖词法分析、语法分析、中间代码生成等编译流程核心知识点,既适合考前自测、查漏补缺,也可供教师布置作业或命题时参考。资源为单个doc文档,体…

2026/10/14 1:38:10 阅读更多 →
从JSON到Parquet:半结构化数据全链路处理与优化实战

从JSON到Parquet:半结构化数据全链路处理与优化实战

1. 半结构化数据到底是什么,为什么大数据绕不开它1.1 从数据形态演变看半结构化数据的定位做了这几年大数据的人应该都有同感:早期大家张口闭口都是“结构化数据”,建表、写SQL、跑报表,一切井然有序。可随着业务系统越接越多&…

2026/10/14 1:38:10 阅读更多 →
普通人抓住国产大模型红利:从提示词工程到工作流自动化

普通人抓住国产大模型红利:从提示词工程到工作流自动化

简介:清华大学新闻与传播学院新媒体研究中心发布的DeepSeek实用指南,面向希望借助AI提升工作、学习与生活效率的普通用户。文档系统梳理了DeepSeek-R1的模型定位与核心能力,涵盖智能对话、文本生成、语义理解、代码补全、联网搜索与深度思考等…

2026/10/14 1:38:10 阅读更多 →
Lua面试题深度拆解:从数据类型到协程的性能避坑指南

Lua面试题深度拆解:从数据类型到协程的性能避坑指南

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

2026/10/14 1:37:10 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/12 18:11:17 阅读更多 →