news 2026/9/26 5:21:17

Spring Boot 3.2 + Spring AI + Ollama 本地大模型部署实战教程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot 3.2 + Spring AI + Ollama 本地大模型部署实战教程

上个月给一个内部管理系统加 AI 问答功能,本来打算直接调云端大模型 API,结果客户随口一句“数据能不能不出内网?”把我问住了。仔细一想,工单、合同、客户备注这些都是敏感信息,走云端确实不合适。于是我把目光转向本地部署方案,最后用 Spring Boot 3.2 + Spring AI + Ollama 把智能对话跑通了。这篇教程就是那两周实战踩坑的完整总结,适合想在内网或本机快速接入大模型,又不希望在代码里硬拼 HTTP 请求的 Java 开发者。


1. 为什么把大模型请回本地:从接口调用的痛点说起

1.1 云端大模型遇到的三道坎

先说结论:如果你的项目只是个人练手,调用云端 API 确实是最快的路。但一旦落到企业系统,尤其是数据敏感的业务系统,云端 API 往往会遇到三个比较现实的问题。

第一是数据边界。客户信息、财务数据、内部流程文本,只要发出请求,就必须经过第三方服务。哪怕对方承诺不留存,安全评审这一关也很难过。我接触的项目里,很多甲方直接要求“数据不能出当前环境”,这时候本地大模型就成了唯一选择。

第二是成本模型的不可控。云端 API 按 token 计费,看起来单价不高,但对话场景的特点是请求频繁、上下文越来越长。当系统里每个用户都在闲聊式提问时,账单会涨得比想象中快得多。本地部署则是一次性硬件投入,长期运行成本主要就是电费。

第三是延迟和可用性的不确定性。云端 API 的响应时间波动很大,高峰期可能要等好几秒才返回第一个 token。内网部署的大模型虽然绝对速度不一定快,但响应时间更稳定,不会因为某个云区域故障而集体超时。

1.2 为什么是 Ollama 而不是 LM Studio / llama.cpp 系

本地跑大模型的工具其实很多,除了 Ollama,还有 LM Studio、llama.cpp、vLLM 等。我最终选择 Ollama,主要是看中它对 Java 生态的友好程度。

Ollama 本质上是一个大模型运行时管理器,它把模型的下载、量化、推理封装成了一条条简单命令,比如ollama run qwen2.5:7b。它自带一个 HTTP API,监听在 11434 端口,任何语言都能通过 REST 调用。Spring AI 官方就提供了 Ollama 的集成模块,依赖一引入,配置一写,就能像操作RestTemplate一样操作大模型。

LM Studio 更适合桌面端那种“下载模型、点开聊天界面”的场景,它对开发者开放接口的能力弱一些。llama.cpp 能力很强,但需要自己编译、管理模型文件,还得处理各种权重格式,工程成本偏高。vLLM 则更偏向高并发推理服务,对硬件要求也更高,普通开发机跑起来有点浪费。

如果你只是想在一个 Spring Boot 项目里快速接上本地大模型,Ollama 是当前最省事的路径。

1.3 这套方案适合谁、解决什么问题

这套方案的定位很明确:让 Java 后端服务拥有一个可调用的本地大模型对话接口。它适合下面几类场景:

  • 企业内部知识库问答机器人,数据不出内网。
  • 个人开发者在本地电脑上做 AI 功能原型验证。
  • 低成本搭建一个私有化 ChatGPT 风格服务,供小团队内部使用。
  • 教学演示,需要在一台普通电脑上完整展示“后端接入大模型”的链路。

它不适合的场景也很清楚:如果单次对话量非常大、并发要求极高,或者需要训练微调,那么 Ollama 的性能和功能就不太够用了,应该考虑更专业的推理框架。


2. 环境准备:Ollama 安装与模型拉取的完整操作

2.1 各平台安装与版本选择

Ollama 的安装非常简单,它提供了 Windows、macOS、Linux 三种系统的安装包。Windows 版是 exe 安装包,双击后会自动安装在用户目录下,并注册开机启动服务。macOS 同样有 dmg 包,装上就能用。Linux 则是一条命令:

curl -fsSL https://ollama.com/install.sh | sh

如果服务器在内网,没有外网访问权限,也可以从官方 GitHub Releases 页面下载离线安装包,拷贝进去手动安装。手动安装的本质是下载解压二进制文件,放到/usr/local/bin下,再通过 systemd 启动服务。这一步没什么黑魔法。

安装完成后,通过ollama --version验证安装是否成功。然后还需要确认服务是否在运行。在 Windows 和 macOS 上,安装包已经自动启动了后台服务;Linux 上如果是手动下载的包,需要先执行:

ollama serve &

等到服务启动后,再用curl http://127.0.0.1:11434/api/tags测试 API 是否通了。返回一串 JSON 说明服务正常。

2.2 模型选型:qwen2.5、llama3.2、gemma2 怎么选

模型选型是很多人纠结的地方。搜“ollama本地部署大模型哪个模型最佳”,你能看到一堆推荐,但真正要考虑的只有两件事:硬盘空间和内存容量。

我个人的经验是,在普通开发机(16GB 内存左右)上,首推qwen2.5:7b。Qwen 系列对中文的理解明显好于同规模的 Llama 模型,而且 7B 参数配合 4bit 量化,大约需要 5-6GB 内存,普通电脑跑得动。Llama 3.2 系列如果要做英文为主的场景也可以选,但中文输出有时候会出现语序别扭的问题。Gemma 2 我试过,对中文的流畅度也一般。

如果你的内存有 32GB 以上,可以考虑qwen2.5:14b,对话质量会上一个台阶。但超过 14B 的模型,在纯 CPU 机器上基本没有实用性,每个 token 要等好几秒,体验很差。

如果只有 8GB 内存,只能选更小的模型,比如qwen2.5:3b或llama3.2:3b。这类小模型做摘要、提取关键词够用,但做有深度的对话还是差点意思。

2.3 国内下载慢的解决办法与模型文件校验

Ollama 默认从官方仓库拉模型,国内网络环境下经常出现“卡在 0% 或下载了几天都完不成”的情况。这里分享几个我试过有效的方法。

第一个办法是修改 OLLAMA_MODELS 环境变量,把模型存放目录指向磁盘剩余空间较大的位置。模型文件动辄 4-8GB,如果系统盘小,拉取很容易失败。Windows 用户可以在“系统属性 -> 环境变量”里新增:

OLLAMA_MODELS=D:\ollama_models

设置后重启 Ollama 服务。

第二个办法是使用国内镜像源。官方支持通过OLLAMA_HOST等环境变量调整服务地址,但对于模型下载,更实用的做法是看看你的网络环境有没有可用的镜像加速服务。不同地区情况不同,网上有整理好的镜像地址,把环境变量配置到 Ollama 的启动脚本里即可。我自己实测,用镜像后下载速度从“几 KB/s”提到了“几十 MB/s”。

第三个办法是下载模型变体的 GGUF 文件再导入。如果你有特殊渠道拿到模型的 GGUF 文件(比如从 ModelScope 下载),可以写一个 Modelfile:

FROM /home/user/qwen2.5-7b-instruct-q4_k_m.gguf

然后在同一目录下执行ollama create qwen2.5-local。这样就不走官方仓库下载,而是直接使用本地文件,速度取决于磁盘内拷贝。

2.4 用命令行快速验证模型可用性

模型下载完成后,先不要着急去写 Java 代码,先用命令行验证一下:

ollama run qwen2.5:7b

输入一句“你好”,如果模型能够正常回答,说明推理链路是通的。按 Ctrl+D 退出对话。然后测试一下 API 接口:

curl http://127.0.0.1:11434/api/generate -d '{ "model": "qwen2.5:7b", "prompt": "用一句话介绍 Spring Boot", "stream": false }'

返回的 JSON 里有一个response字段,内容就是模型生成的文本。这一步能排除掉“Ollama 装了但 API 没起”这种基础问题。等 API 通了,再进 Spring Boot 的阶段。


3. Spring AI 接入前的关键认知:ChatModel 与 ChatClient 的关系

3.1 Spring AI 项目现状与版本选择

Spring AI 是 Spring 生态里专门做 AI 应用集成的项目。它目前还处于快速迭代阶段,版本号变化比较频繁,但基本思路已经很清晰:把各种大模型提供商(OpenAI、Ollama、智谱、通义等)抽象成统一的接口,让业务代码不依赖具体某个大模型。

我用的时候最新稳定版是1.0.0-M6,配 Spring Boot 3.2.x 是兼容的。如果以后版本更新,注意把spring.ai.version换成对应版本就行。版本匹配很重要,不然会出现NoSuchMethodError这类启动异常。

一个值得注意的趋势是,Spring AI 已经开始区分ChatModel和ChatClient。ChatModel是底层抽象,负责与大模型通信;ChatClient是一个更高层的工具类,提供了流式、记忆、工具调用等便捷方法。初学时不要被这两个类搞混,你只需要记住:业务代码主要和 ChatClient 打交道。

3.2 为什么需要 Spring AI 的 Ollama 模块,而不是手写 HTTP 调用

有人可能会说,Ollama 不是已经提供 REST API 了吗?我直接用RestTemplate或WebClient调不就行了吗?为什么还要引入 Spring AI?

这么想也没错,手写 HTTP 调用完全能跑通。但如果你要做的是对话接口,手写会遇到几个麻烦:流式输出需要处理 SSE 协议、上下文历史需要自己管理 token 拼接、prompt 模板要自己写解析逻辑、不同模型之间的参数调整要自己适配。这些重复工作,Spring AI 已经在框架层解决了一部分。

Spring AI 还提供了一套PromptTemplate机制,可以在 Java 里写类似“你现在是一个客服助手,用户问题是 {{question}}”这样的模板,然后通过PromptTemplate渲染变量。这样业务代码很干净,模型、prompt、服务逻辑各司其职。

所以我的建议是:如果你的场景只需要一个简单接口,手写没问题;如果打算做成相对完整的对话服务,用 Spring AI 会省心很多。

3.3 依赖引入与自动装配原理

在pom.xml中加入以下依赖(以 Maven 为例):

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama</artifactId> <version>1.0.0-M6</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0-M6</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

如果有些 Spring AI 的依赖还没有发布到 Maven Central,你需要在仓库配置中加入 Spring 的 Milestone 仓库:

<repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories>

引入依赖后,Spring Boot 的自动配置会扫描到spring-ai-ollama,读取配置文件中spring.ai.ollama前缀的属性,自动创建OllamaChatModel和ChatClient.Builder等 Bean。你不需要手动写多少配置类,只需要在application.yml里声明:

spring: ai: ollama: base-url: http://127.0.0.1:11434 chat: model: qwen2.5:7b options: temperature: 0.7

到这里,Spring Boot 已经把 Ollama 连接好了。接下来就是写代码。


4. 核心代码实现:从最简对话到可设定人设的接口

4.1 设计一个支持历史上下文的聊天控制器

业务需求通常是:用户发一句话,后端返回 AI 的回答。但聊天不能每次都没记忆。我推荐用一个ConversationService来管理会话历史,每个会话用sessionId区分。

先定义一个请求体:

public record ChatRequest(String sessionId, String message) {}

再定义一个响应体,流式接口需要多个响应,非流式则返回完整回答。为了简单,这里先做非流式:

@RestController @RequestMapping("/api/chat") public class ChatController { private final ChatClient chatClient; private final ConversationService conversationService; public ChatController(ChatClient.Builder builder, ConversationService conversationService) { this.chatClient = builder.build(); this.conversationService = conversationService; } @PostMapping public Map<String, String> chat(@RequestBody ChatRequest request) { String sessionId = request.sessionId(); String userMessage = request.message(); List<Message> history = conversationService.getHistory(sessionId); String answer = chatClient.prompt() .messages(history) .user(userMessage) .call() .content(); conversationService.append(sessionId, userMessage, answer); return Map.of("answer", answer); } }

这段代码最关键的点是.messages(history)。如果你不给它历史消息,模型就是“金鱼记忆”状态,每次对话都不记得刚才说了什么。这一行把我踩的第一个坑填平了,后面会细讲。

4.2 用 ChatClient 做系统人设与 Prompt 模板

很多时候我们不想让 AI 随便回答,而是要它扮演某个角色。Spring AI 提供了system方法,可以直接传入系统提示词:

ChatClient chatClient = builder .defaultSystem("你是一个严谨的 Java 技术顾问,回答问题时优先考虑代码安全性和性能,语气简洁直接。") .build();

如果系统提示词需要动态插入参数,使用PromptTemplate:

String template = """ 你是{{role}}。 用户的问题是:{{question}} 请用不超过200字回答。 """; PromptTemplate promptTemplate = new PromptTemplate(template); Map<String, Object> params = Map.of("role", "Java 技术顾问", "question", userMessage); Prompt prompt = promptTemplate.create(params); String answer = chatClient.prompt(prompt).call().content();

这种方式适合做一些规则化的问答,比如“用一句话解释”“翻译成英文”“提取关键词”等。把 prompt 模板抽成常量或者配置项,后面调整 AI 行为时就不用改 Java 代码了。

4.3 接入流式响应:SSE 推送让对话不卡顿

非流式接口有个体验问题:如果模型生成速度慢,前端要等好几秒才收到完整结果。更好的方案是用 SSE(Server-Sent Events)把 token 一个个推送出去,前端边收边显示,体感上快很多。

Spring AI 原生支持流式调用,把.call().content()换成.stream().content():

@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> streamChat(@RequestBody ChatRequest request) { return chatClient.prompt() .messages(history) .user(request.message()) .stream() .content(); }

返回类型从String变成Flux<String>,Spring Boot 会自动用 SSE 协议输出。前端用EventSource或fetch的ReadableStream就能逐段渲染。

需要注意的一点是,流式接口没办法在返回前自动保存历史消息。因为回答是分段产出的,历史记录必须在全部流结束后再统一追加。更优雅的做法是服务端持久化Flux,在前端断开时仍然继续保存。不过小项目里,也可以选择在流完成后调用doOnComplete去保存上下文,这样实现起来简单得多。

4.4 上下文历史的存储与裁剪策略

我的ConversationService用了很简单的内存 Map 存储:

@Component public class ConversationService { private final Map<String, List<Message>> conversations = new ConcurrentHashMap<>(); public List<Message> getHistory(String sessionId) { return conversations.getOrDefault(sessionId, new ArrayList<>()); } public void append(String sessionId, String userMsg, String assistantMsg) { List<Message> history = conversations.computeIfAbsent(sessionId, k -> new ArrayList<>()); history.add(new UserMessage(userMsg)); history.add(new AssistantMessage(assistantMsg)); // 裁剪:只保留最近10轮 if (history.size() > 20) { history.subList(0, history.size() - 20).clear(); } } }

内存存储只适合单机、演示项目。如果生产环境有多实例部署,建议把历史消息放到 Redis 里。消息的裁剪很重要,因为大模型的上下文窗口是有限制的。比如 qwen2.5:7b 支持 32K 上下文,但太长会导致响应速度变慢。一般我只保留最近 10 轮对话,既能维持上下文连贯,又不会拖慢速度。

4.5 从 Controller 方法到完整可运行的最小闭环

把所有文件准备完之后,启动 Spring Boot 项目,用 curl 试一下:

curl -X POST http://localhost:8080/api/chat \ -H "Content-Type: application/json" \ -d '{"sessionId": "s1", "message": "Spring Boot 3.2 有什么新特性?"}'

返回的 JSON 里就是模型的回答。如果这一步通了,说明整个链路已经打通。接下来可以接前端页面、做 API 鉴权、或者增加更多 prompt 场景。


5. 实测调优:参数配置、并发控制与资源管理

5.1 temperature、top_p 和 num_predict 怎么配

很多人直接跳过参数配置,用默认值跑。模型本身能用,但回答质量会飘。temperature控制在 0 到 1 之间,值越高回答越发散,值越低越保守。技术问答类场景我习惯设0.7,如果是写诗、头脑风暴可以设0.9以上。

num_predict控制生成最大 token 数。默认可能很长,业务接口往往不需要长篇大论,我一般限制在 500 以内,防止响应时间过长。

在application.yml中示例:

spring: ai: ollama: chat: model: qwen2.5:7b options: temperature: 0.7 top-p: 0.9 num-predict: 512

Ollama 本身还有OLLAMA_NUM_PARALLEL环境变量,用来设置并发请求处理数量。默认值是 1 或 4 取决于显存大小。如果多用户同时访问,这个参数建议调到 4 以上。但注意,并发太高会吃满 GPU 或 CPU,反而所有请求都变慢。

5.2 在纯 CPU 机器上提高响应速度的经验

如果没有 NVIDIA 显卡,只能靠 CPU 推理,那就要做好“慢”的心理准备。7B 模型在 CPU 上大概每秒只能生成 5~10 个 token,一个回答可能要等 20 秒以上。这时候有几个技巧可以救急:

  • 换更小的模型,比如 3B 模型,速度能快一倍。
  • 使用量化程度更高的模型,例如 Q4_K_M 比 Q8_0 快不少。
  • 开启 Ollama 的OLLAMA_FLASH_ATTENTION=1环境变量,部分模型能加速。
  • 尽量用流式输出,让用户看到第一个 token 出现的时间尽可能地早,心理上就没那么卡。

我在这台 MacBook Pro(M1 Pro)上测试,qwen2.5:7b走 Metal 加速,速度大概 30 token/s,体验已经接近云端了。如果你用的是公司老旧台式机,建议还是买个带 CUDA 的显卡,或者用 Mac 系列。

5.3 模型量化等级 Q4、Q8 和 F16 如何取舍

Ollama 在拉取模型时通常会默认选择量化版本。量化就是减少每个权重的位数,来交换内存占用和推理速度。Q4_K_M 是我最常用的,体积小、速度好、质量损失在大多数场景下感知不到。Q8_0 质量更好但体积接近翻倍。F16 是完整精度,除非内存非常宽裕,否则不推荐用于对话场景。

有一句经验总结:**能跑 Q4 就跑 Q4,不要迷信精度。**大模型的能力瓶颈更多来自模型大小和训练数据,而不是最后几位小数。


6. 避坑实录:我在整合过程中踩过的五个坑

6.1 版本不匹配导致 NoSuchMethodError

我第一次引入 Spring AI 时没有使用 BOM,直接写了spring-ai-ollama的版本号,和 Spring Boot 版本对不上,启动时报了NoSuchMethodError: 'void org.springframework.util.MultiValueMap.add(...)'。

后来才发现,Spring AI 的版本与新版本 Spring Boot 耦合很紧密。解决方法是使用官方 BOM,并保持spring-ai-bom和spring-ai-ollama版本一致。如果升级 Spring Boot,也要同步升级 Spring AI 版本。这个坑在官方文档里写了,但很多人会忽略。

6.2 模型名带标签导致 404 或 400

当我从命令行拉取模型时,模型全名可能是qwen2.5:7b-instruct-q4_K_M。在 Spring AI 配置里如果直接把带特殊字符的 tag 写进去,有时候 API 返回not found。原因是 Ollama API 要求模型名是唯一的,带标签时需要写全,但一些特殊字符可能在配置解析时被转义。

建议做法是:在 Ollama 里先把模型重命名成简短的名字:

ollama cp qwen2.5:7b-instruct-q4_K_M my-chat-model

然后在 Spring AI 配置里使用my-chat-model。这样既稳定又不会把超长 tag 暴露在代码里。

6.3 中文输出乱码或截断

Spring Boot 返回 JSON 时中文乱码,通常不是模型的问题,而是响应头缺charset=UTF-8。在 Controller 里可以显式指定:

@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8")

另外,有些模型在上下文较长之后会出现输出截断。这不是 bug,而是 token 数达到num_predict上限。把配置文件里的num-predict调大,或者裁剪历史消息,问题就能缓解。

6.4 Ollama 服务没有自动启动

Linux 服务器上手动安装 Ollama 后,经常出现重启机器后服务不启动的情况。安装脚本一般会注册 systemd 服务,如果你使用手动解压的方式,则需要自己创建 service 文件:

[Unit] Description=Ollama Service After=network-online.target [Service] ExecStart=/usr/local/bin/ollama serve User=ollama Group=ollama Restart=always RestartSec=3 [Install] WantedBy=default.target

然后执行systemctl daemon-reload && systemctl enable ollama。此后再也不用担心重启后 11434 端口没反应。

6.5 Spring Security / 网关引起的路径拦截

如果你的项目里已经有 Spring Security,那么/api/chat会被拦截,前端调用直接返回 401。我一开始没注意,前端反馈“接口报错”,排查半天才发现是 Security 配置把所有/api/**都拦截了。

如果这个接口允许匿名访问,需要在 Security 配置里放行:

http.authorizeHttpRequests(auth -> auth .requestMatchers("/api/chat/**").permitAll() .anyRequest().authenticated() );

如果是微服务架构,还要注意网关转发时别把/api/chat/stream的 SSE 响应缓冲掉。部分网关默认缓冲响应,会导致流式效果失效,需要在网关层关闭缓冲。


这套方案我目前已经跑了快一个月,稳定性还是不错的。如果只是做内部工具或学习演示,用 Spring Boot + Spring AI + Ollama 是非常合适的组合——它把模型管理、上下文处理、流式输出这些脏活都封装好了,让 Java 开发者能专注于业务逻辑。最后分享一个小技巧:生产环境可以把 Ollama 单独部署在一台 GPU 服务器上,Spring Boot 应用跑在另一台机器上,通过base-url指向 Ollama 服务器的 IP 地址,两者互不影响。这样后续扩展成多服务共享同一个本地大模型,也只是改一行配置的事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 5:21:11

WebSocket集群消息不丢:Spring Boot集成RabbitMQ广播实践

1. 从单体到集群&#xff1a;WebSocket推送为什么会"丢消息"先说一个我实际踩过的坑&#xff1a;项目早期是一个单体Spring Boot应用&#xff0c;用的spring-boot-starter-websocket加STOMP&#xff0c;前端连上来就完事&#xff0c;服务端要推消息直接SimpMessaging…

作者头像 李华
网站建设 2026/9/26 5:20:00

LogViewPro中文版:超大文本文件秒开与日志排查实战指南

简介&#xff1a;LogViewPro中文版是一款专为超大文本文件场景设计的日志查看与分析工具&#xff0c;面向系统管理员、运维工程师和开发人员&#xff0c;解决普通编辑器打开大日志卡顿、搜索缓慢的常见痛点。它优化了大文件读取机制&#xff0c;即使面对几GB乃至更大的文本也能…

作者头像 李华
网站建设 2026/9/26 5:19:29

DeepSeek V4.1 Flash 接入实战:API、本地部署与代码助手配置

1. 从一次真实的接入翻车说起上周帮一个朋友调试他的代码助手工作流&#xff0c;他信誓旦旦跟我说“DeepSeek V4.1 Flash 我已经接好了&#xff0c;API 也能通”&#xff0c;结果我打开他的 VS Code 一看&#xff0c;Continue 插件里报了一长串cc switch local proxy failed wh…

作者头像 李华
网站建设 2026/9/26 5:19:29

Docker容器化部署实战:从镜像管理到场景化运维指南

1. 容器到底是什么&#xff1a;先拆掉认知门槛搞 Docker 的人经常遇到一种尴尬&#xff1a;跟同事说“用容器跑一下”&#xff0c;对方第一反应是“哦&#xff0c;虚拟机吧”。这是最大的误区。容器不是虚拟机&#xff0c;它是一个运行在宿主操作系统之上的隔离进程&#xff0c…

作者头像 李华
网站建设 2026/9/26 5:19:25

大数据开发能力图谱:从考试题库反向构建工程能力

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

作者头像 李华