news 2026/9/12 10:01:48

《开源大模型食用指南》Kimi-VL-A3B-Thinking 多模态对话助手实战:基于 Flask 的前后端分离 Web 应用搭建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《开源大模型食用指南》Kimi-VL-A3B-Thinking 多模态对话助手实战:基于 Flask 的前后端分离 Web 应用搭建

《开源大模型食用指南》Kimi-VL-A3B-Thinking 多模态对话助手实战:基于 Flask 的前后端分离 Web 应用搭建

【免费下载链接】self-llm《开源大模型食用指南》针对中国宝宝量身打造的基于Linux环境快速微调(全参数/Lora)、部署国内外开源大模型(LLM)/多模态大模型(MLLM)教程项目地址: https://gitcode.com/GitHub_Trending/se/self-llm

导读

本文基于 self-llm 仓库中 01-Kimi-VL-对话助手.md 教程,完整讲解如何从零搭建一个基于 Moonshot AIKimi-VL-A3B-Thinking多模态大模型的前后端分离对话助手 Web 应用。你将掌握环境配置、ModelScope 模型下载、Flask 后端实现(含多模态输入处理、图像压缩、思考过程保留、多轮对话历史管理)以及前端交互设计(Canvas 图像压缩、参数实时调节、思考过程折叠展示),最终在浏览器中体验"图文双模态 + 可视化推理"的对话能力。

效果展示

整个应用围绕"文本 + 图像"双模态输入展开,界面支持一次上传最多 2 张图片并配以文本提问。下图依次展示了应用启动时的模型加载状态、用户上传双图提问以及模型给出的对比分析结果:

一、项目结构与技术选型

教程对应的完整参考代码位于仓库的 01-Kimi-VL-对话助手/app 目录,结构如下:

models/Kimi-VL/01-Kimi-VL-对话助手/app/ ├── app.py # Flask 后端:模型加载、推理、API 路由 ├── requirements.txt # 依赖清单 ├── templates/ │ └── index.html # 前端聊天界面 └── README.md # 应用说明文档

整体架构为典型的前后端分离模式:

  • 后端:Flask 提供GET /(渲染前端页面)、POST /api/generate(生成回复)、POST /api/clear_history(清空会话历史)三个核心路由;
  • 前端:原生 HTML + JavaScript,负责图片上传压缩、参数滑动条、聊天界面渲染与思考过程折叠展示;
  • 模型侧:通过 Hugging Face Transformers 生态的AutoProcessorAutoTokenizerAutoModelForCausalLM加载Kimi-VL-A3B-Thinking,其中AutoProcessor专门负责把"文本 + 图像"组合输入统一编码为模型可接受的张量。

关于该模型的架构细节,可进一步阅读仓库中的 02-Kimi-VL-技术报告解读.md。需要说明的是,本文聚焦对话助手的工程化搭建,Kimi-VL-A3B-Thinking 是 Moonshot AI 发布的多模态大模型,支持图像理解与文本推理,并会在回复中以◁think▷...◁/think▷标签输出思考过程。

二、环境准备

2.1 基础环境

教程给出的参考环境如下:

---------------- ubuntu 22.04 python 3.12 cuda 12.4 pytorch 2.6.0 ----------------

同时需要保证足够的 GPU 显存:在bfloat16精度下加载模型,参考显存占用约40GB,即最低要求为双卡 RTX 4090 或单卡 A6000。如果使用其他配置,需要自行评估显存是否满足device_map="auto"切分加载的需求。

2.2 安装依赖

教程建议先对pip换源加速下载,再逐项安装依赖包:

pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple pip install transformers==4.48.2 pip install accelerate==1.6.0 pip install flask==3.1.0 pip install blobfile==3.0.0 pip install pillow==10.4.0 pip install modelscope==1.22.3

参考代码目录下也提供了等价的 requirements.txt,可以直接执行pip install -r requirements.txt完成安装:

torch==2.6.0 transformers==4.48.2 accelerate==1.6.0 flask==3.1.0 blobfile==3.0.0 pillow==10.4.0 modelscope==1.22.3

其中transformersaccelerate负责模型加载与多卡切分,modelscope用于国内环境下拉取模型权重,flask承载 Web 服务,pillow用于图像解码与缩放,blobfile为 Kimi-VL 模型加载所需的依赖。

三、模型下载

教程推荐使用modelscopesnapshot_download函数下载模型,该方法对国内用户非常友好。新建model_download.py文件并写入以下代码:

# model_download.py from modelscope import snapshot_download model_dir = snapshot_download('moonshotai/Kimi-VL-A3B-Thinking', cache_dir='请修改我!', revision='master') print(f"模型下载完成,保存路径为:{model_dir}")

运行python model_download.py即可开始下载。参数说明:

  • 第一个参数:模型名称moonshotai/Kimi-VL-A3B-Thinking
  • cache_dir:模型下载到本地的保存路径,必须修改为实际路径,例如/data/models/Kimi-VL-A3B-Thinking
  • revision:分支版本,此处固定为master

注意:请记得修改cache_dir为你自己的模型下载路径。下载完成后会打印模型保存路径,后续后端配置中需要用到。

四、后端实现详解

后端代码 app.py 是教程的核心,下面按照功能模块逐段剖析。

4.1 全局状态与默认参数

from flask import Flask, request, jsonify, render_template, session import torch from transformers import AutoTokenizer, AutoModelForCausalLM, AutoProcessor import gc import re import uuid import json import base64 import logging from io import BytesIO from PIL import Image app = Flask(__name__) app.secret_key = "kimi-chatbot-secret-key" # 用于session加密 # 修改为合理的值:最大100MB app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 限制上传文件大小 app.config['MAX_CONTENT_PATH'] = None # 全局变量存储预加载的模型和tokenizer MODEL_ID = "请修改我!!!" tokenizer = None model = None processor = None # 用于存储对话历史的字典 chat_histories = {} # 默认值设置 DEFAULT_MAX_NEW_TOKENS = 1024 DEFAULT_MAX_HISTORY_LENGTH = 10

要点:

  • MODEL_ID需要修改为上一步snapshot_download返回的本地模型路径(或直接填写模型仓库名);
  • tokenizer / model / processor三个全局变量在应用启动时预加载,避免每次请求重复加载模型;
  • chat_histories是一个以chat_id为键的字典,用于服务端保存各会话的多轮对话历史;
  • 默认生成长度 1024 token,默认保留最近 10 轮对话历史;
  • MAX_CONTENT_LENGTH将单次请求体限制为 100MB,防止超大上传拖垮服务。

4.2 模型预加载

def load_model(): global tokenizer, model, processor print("正在加载模型和tokenizer,请稍候...") # 加载processor (用于处理图像和文本) processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True) # 加载tokenizer tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True) # 加载模型 model = AutoModelForCausalLM.from_pretrained( MODEL_ID, device_map="auto", torch_dtype=torch.bfloat16, trust_remote_code=True ) print("模型加载完成!")

关键配置:

  • trust_remote_code=True:Kimi-VL 依赖仓库中的自定义代码,必须开启;
  • device_map="auto":由 accelerate 自动将模型分配到可用设备(单卡或多卡均可),这正是 40GB 显存需求可以被双卡 4090 满足的原因;
  • torch_dtype=torch.bfloat16:以 bfloat16 半精度加载权重,显著降低显存占用。

__main__中,模型加载被放进独立线程启动,避免阻塞 Flask 主线程的启动:

if __name__ == '__main__': # 在另一个线程中预加载模型 import threading threading.Thread(target=load_model).start() app.run(debug=True, host='0.0.0.0', port=5000, use_reloader=False)

注意use_reloader=False:Flask debug 模式的自动重载会重启进程,与线程预加载模型冲突,必须关闭。

4.3 图像处理:base64 解码与压缩

前端通过 base64 字符串传递图片,后端需要将其还原为 PIL 图像并做压缩优化:

def base64_to_image(base64_str): if "base64," in base64_str: base64_str = base64_str.split("base64,")[1] try: image_bytes = base64.b64decode(base64_str) image = Image.open(BytesIO(image_bytes)) # 获取原始尺寸 original_width, original_height = image.size # 压缩大图片,如果宽度或高度超过1500像素,则按比例缩小 max_size = 1500 if original_width > max_size or original_height > max_size: if original_width > original_height: new_width = max_size new_height = int(original_height * (max_size / original_width)) else: new_height = max_size new_width = int(original_width * (max_size / original_height)) image = image.resize((new_width, new_height), Image.LANCZOS) # 如果是RGBA模式(带透明通道),转换为RGB if image.mode == 'RGBA': background = Image.new('RGB', image.size, (255, 255, 255)) background.paste(image, mask=image.split()[3]) image = background return image except Exception as e: # 出错时返回默认灰色图像,不中断整体流程 return Image.new('RGB', (100, 100), color=(200, 200, 200))

该函数做了三件事:

  1. 剥离data:image/...;base64,前缀后解码;
  2. 对超过 1500 像素的长边按比例缩放(使用Image.LANCZOS高质量重采样),控制送入模型的图像分辨率;
  3. 将带透明通道的 RGBA 图像合成到白色背景上转为 RGB,避免透明区域被模型误解。

4.4 响应清理:保留思考过程、剔除特殊 token

Kimi-VL-A3B-Thinking 的输出包含◁think▷...◁/think▷思考标签,以及<|eot|><|im_end|>[EOS]等特殊结束标记。clean_response的作用是保留思考标签本身,同时清理标签内外所有特殊标记

def clean_response(text): """清理模型响应中的特殊token""" # 先清理常见的结束标记,包括<|im_end|>和[EOS] text = re.sub(r'<\|im_end\|>(\s*\[EOS\])?', '', text) text = re.sub(r'\[EOS\]', '', text) # 保留思考标签 thinking_pattern = r'◁think▷([\s\S]*?)◁/think▷' if re.search(thinking_pattern, text): # 思考部分的内容 def clean_thinking_content(match): thinking_content = match.group(1) thinking_content = re.sub(r'<[\|/]?eot[\|]?>', '', thinking_content) thinking_content = thinking_content.replace('<|eot|>', '') thinking_content = re.sub(r'<\|im_end\|>(\s*\[EOS\])?', '', thinking_content) thinking_content = re.sub(r'\[EOS\]', '', thinking_content) return f'◁think▷{thinking_content}◁/think▷' # 先处理思考标签内的内容 text = re.sub(thinking_pattern, clean_thinking_content, text) # 再处理剩余文本中的特殊标记 remaining_text = re.sub(thinking_pattern, '', text) cleaned_remaining = re.sub(r'<[\|/]?eot[\|]?>', '', remaining_text) cleaned_remaining = cleaned_remaining.replace('<|eot|>', '') cleaned_remaining = re.sub(r'<\|im_end\|>(\s*\[EOS\])?', '', cleaned_remaining) cleaned_remaining = re.sub(r'\[EOS\]', '', cleaned_remaining) # 替换原文中的思考标签后的部分 text = re.sub(r'◁/think▷[\s\S]*', f'◁/think▷{cleaned_remaining}', text) return text.strip() else: # 无思考标签时,统一剔除各类特殊标记 patterns = ['<|eot|>', '<|im_end|>', '[EOS]'] for pattern in patterns: text = text.replace(pattern, '') text = re.sub(r'<[\|/]?eot[\|]?>', '', text) # 匹配 <eot>, </eot>, <|eot|> 等 return text.strip()

思考标签保留下来后,前端会将其渲染为可折叠的"查看思考过程"区域,让用户直观看到模型的推理步骤,而正文回复则单独展示,互不干扰。

4.5 核心推理接口:/api/generate

POST /api/generate同时支持两种输入模式:

  • 多模态模式:前端通过chat_history表单字段以 JSON 数组传递含图片的消息内容(OpenAI 风格type: "image"/type: "text"结构);
  • 纯文本模式:直接通过user_input字段传递文本,向后兼容纯文字对话。
多模态处理路径
# 如果前端通过JSON传递了完整的历史记录(包含图像) chat_history_json = request.form.get('chat_history') if chat_history_json: received_history = json.loads(chat_history_json) # 取最后一条用户消息 user_message = received_history[-1] user_message_content = user_message.get('content', []) # 遍历消息项,分离图像与文本 images = [] processed_content = [] for item in user_message_content: if isinstance(item, dict) and item.get('type') == 'image' and 'image' in item: image = base64_to_image(item['image']) images.append(image) processed_content.append({'type': 'image', 'image': f'image_{len(images)-1}'}) elif isinstance(item, dict) and item.get('type') == 'text' and 'text' in item: processed_content.append({'type': 'text', 'text': item['text']}) # 使用processor处理多模态输入 messages = [{"role": "user", "content": processed_content}] text = processor.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt") inputs = processor(images=images, text=text, return_tensors="pt", padding=True, truncation=True).to(model.device) with torch.no_grad(): generated_ids = model.generate(**inputs, max_new_tokens=max_new_tokens) # 裁剪掉输入部分,仅保留生成的token generated_ids_trimmed = [ out_ids[len(in_ids):] for in_ids, out_ids in zip(inputs.input_ids, generated_ids) ] response = processor.batch_decode( generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False )[0] cleaned_response = clean_response(response)

这段代码的调用链可以这样理解:

  1. processor.apply_chat_template把消息列表格式化为模型的对话模板文本;
  2. 再把模板文本与images列表一起交给processor,得到input_idspixel_values等输入张量;
  3. model.generatetorch.no_grad()下自回归生成;
  4. 通过输入输出 token 长度差裁剪出新增的生成部分并解码。
纯文本处理路径
# 传统文本输入处理(向后兼容) if not has_input: if not user_input and not request.form: return jsonify({"error": "请输入问题或上传图片"}), 400 if chat_id not in chat_histories: chat_histories[chat_id] = [] chat_histories[chat_id].append({"role": "user", "content": user_input}) # 从历史记录构建消息列表,使用前端传递的历史长度 messages = chat_histories[chat_id][-max_history_length*2:] # 用户和助手消息各算一条 inputs = tokenizer.apply_chat_template(messages, add_generation_prompt=True, return_tensors="pt", return_dict=True) with torch.no_grad(): outputs = model.generate(**inputs.to(model.device), max_new_tokens=max_new_tokens) response = tokenizer.batch_decode(outputs[:, inputs["input_ids"].shape[-1]:]) cleaned_response = clean_response(response[0]) chat_histories[chat_id].append({"role": "assistant", "content": cleaned_response})

两种路径生成完成后都会:

  • 将助手回复追加到chat_histories[chat_id]
  • 若历史条数超过max_history_length * 2(用户与助手各占一半),只保留最近max_history_length * 2条,防止上下文无限膨胀;
  • 调用torch.cuda.empty_cache()gc.collect()释放显存缓存与 Python 对象。
参数校验

接口接收前端传入的max_new_tokensmax_history_length,并做范围约束:

max_new_tokens = max(256, min(max_new_tokens, 2048)) max_history_length = max(2, min(max_history_length, 20))

即生成长度限制在256 ~ 2048,历史轮数限制在2 ~ 20

4.6 会话与历史管理

每个浏览器会话通过 Flask 的session分配唯一uuid作为chat_id

@app.route('/') def home(): if 'chat_id' not in session: session['chat_id'] = str(uuid.uuid4()) chat_id = session['chat_id'] if chat_id not in chat_histories: chat_histories[chat_id] = [] return render_template('index.html', chat_id=chat_id)

清空历史接口则直接置空对应会话的列表:

@app.route('/api/clear_history', methods=['POST']) def clear_history(): data = request.json chat_id = data.get('chat_id', session.get('chat_id')) if chat_id and chat_id in chat_histories: chat_histories[chat_id] = [] return jsonify({"success": True, "message": "聊天历史已清除"}) else: return jsonify({"success": False, "error": "无效的会话ID"}), 400

服务端会话机制天然支持多用户同时使用,互不干扰。

五、前端实现要点

前端页面位于 templates/index.html,以原生 HTML/CSS/JavaScript 实现,核心要点如下。

5.1 Canvas 图像压缩

上传图片后,前端先用 Canvas 做一次压缩再发送,与后端base64_to_image的二次处理形成"双重优化":

async function compressImage(file, maxWidth = 1200, maxHeight = 1200, quality = 0.7) { // 读取文件 -> 加载为 Image -> 计算缩放尺寸 -> 绘制到 Canvas // 根据文件大小动态调整压缩质量: // 大于3MB -> quality = 0.5 // 大于1MB -> quality = 0.6 // PNG 含透明像素时保留 PNG 格式,否则一律转 JPEG // 压缩后若仍大于1MB,再用 0.4 质量二次压缩 }

前端的压缩策略:长边超过 1200 像素即缩放;按文件大小分档降低 JPEG 质量;压缩后仍超 1MB 则强制降至 0.4 质量,保证大图也能顺利上传。同时还有如下约束:

  • 单张图片超过10MB拒绝上传;
  • 一次最多上传2 张图片;
  • 发送前若图片总大小超过 5MB,会弹出确认提示。

5.2 思考过程折叠展示

前端通过正则从回复中提取◁think▷标签内容,将"思考过程"渲染为可点击折叠的区域:

const thinkPattern = /◁think▷([\s\S]*?)◁\/think▷([\s\S]*)/; const match = content.match(thinkPattern); if (match) { const thinkContent = match[1].trim(); // 思考过程 const realResponse = match[2].trim(); // 正式回复 // 创建 "查看思考过程" 折叠头部 + 可展开的思考内容 + 正式回复正文 }

点击"查看思考过程"即可展开模型的推理步骤,正式回复始终单独展示。

5.3 模型加载状态轮询

模型在后台线程加载期间,前端通过轮询checkModelStatus()探测就绪状态:

function checkModelStatus() { fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ user_input: '', chat_id: chatId }) }) .then(response => { if (response.status === 503) { setTimeout(checkModelStatus, 5000); // 模型仍在加载,5秒后再查 } else { // 模型已就绪:隐藏橙色通知条,启用输入框/上传/发送按钮 } }) .catch(() => setTimeout(checkModelStatus, 5000)); }

这与后端"模型未加载时返回 503"的逻辑呼应:模型加载完成前,顶部橙色通知条"模型正在加载中,请稍候..."持续显示,所有输入控件保持禁用。

5.4 参数滑动条与请求发送

页面提供两个滑动条,值随拖动实时更新,并在发送请求时随FormData一并提交:

  • 模型生成长度上限(max_new_tokens):范围 256~2048,步长 128,默认 1024;
  • 最大对话记忆轮数(max_history_length):范围 2~20,步长 1,默认 10。

发送时前端只携带最近maxHistoryLength * 2条历史,并设置 120 秒的请求超时(AbortController),超时或网络错误均有对应的中文提示文案。

六、运行应用

6.1 启动

python app.py

应用将在http://localhost:5000上运行。启动后模型会在后台自动加载,可能需要 1~2 分钟。期间界面顶部会显示"模型正在加载中"的橙色提示,加载完成后提示条消失、输入框与按钮才可用。

6.2 使用方法

  1. 在浏览器中打开 http://localhost:5000;
  2. 等待模型加载完成(顶部的橙色通知条消失);
  3. 按需调整参数滑动条:生成长度上限(256~2048)、历史记录长度(2~20);
  4. 点击"上传图片"选择图片(支持 JPG、PNG 等常见格式,最多 2 张),或仅输入文本;
  5. 在输入框中输入问题(例如"请描述这个图片中的内容"),点击"发送"或按 Enter;
  6. 等待模型生成回复(含图像时耗时更长);
  7. 若回复含"查看思考过程"链接,可点击展开查看模型的详细分析步骤;
  8. 继续多轮对话,模型会记住上下文;
  9. 点击"清除对话历史"开始新会话。

6.3 参数速查表

参数默认值允许范围作用
max_new_tokens1024256 ~ 2048单次回复生成的最大 token 数,越大回复越长、耗时与显存占用越高
max_history_length102 ~ 20保留的对话轮数(用户与助手各算一条),越大上下文越完整、输入越长
MODEL_ID需修改本地模型路径或 ModelScope 模型名
MAX_CONTENT_LENGTH100MBFlask 请求体上限,对应单张图片不超过 10MB、一次最多 2 张
模型精度bfloat16加载精度,参考显存占用约 40GB

6.4 推荐试用的示例问题

类型示例问题
图像描述"请详细描述这张图片中的内容"
视觉分析"这张图片中有哪些物体?它们各自的特点是什么?"
图像比较上传两张图后:"比较这两张图片的异同点"
内容识别"图片中的文字内容是什么?"
场景理解"这个场景可能是在什么地方?为什么?"
情感分析"图片中人物的情绪如何?基于什么判断?"
视觉推理"根据图片内容,推测这可能是什么场合或事件?"

七、注意事项与常见问题

  • 显存要求:bfloat16 精度下参考显存占用约 40GB,建议双卡 4090 或单卡 A6000;显存不足时模型无法通过device_map="auto"完整加载;
  • 必须修改两处model_download.py中的cache_dir,以及app.py中的MODEL_ID
  • 图片限制:前端单张图片不超过 10MB、一次最多 2 张、发送前总大小超过 5MB 会提示确认;后端MAX_CONTENT_LENGTH为 100MB;
  • 请求超时:含图像请求的浏览器侧超时时间为 120 秒,响应过慢请尝试上传更小的图片;
  • 历史长度:默认保留最近 10 轮对话(可通过滑动条调整为 2~20),防止上下文过长导致显存与耗时上升;
  • 内存管理:每次生成后调用torch.cuda.empty_cache()gc.collect()释放缓存;生成全程使用torch.no_grad()减少内存占用;
  • debug 模式:必须保持use_reloader=False,否则 Flask 自动重载会打断后台模型加载线程。

结语

通过本文,你已经在 self-llm 仓库的教程基础上,完整掌握了一条"多模态大模型 + Web 工程化"的实践链路:从 ModelScope 拉取 Kimi-VL-A3B-Thinking 权重,到 Flask 后端处理图文混合输入、保留思考过程、维护多轮会话,再到前端 Canvas 压缩与参数实时调节。整个应用同时支持图文双模态与纯文本对话,为你后续在此基础上扩展更多多模态应用(如自定义视觉问答、图像对比分析工具)提供了可直接复用的参考实现——完整代码位于 01-Kimi-VL-对话助手/app,可随时对照 README.md 查阅更细致的功能说明。

【免费下载链接】self-llm《开源大模型食用指南》针对中国宝宝量身打造的基于Linux环境快速微调(全参数/Lora)、部署国内外开源大模型(LLM)/多模态大模型(MLLM)教程项目地址: https://gitcode.com/GitHub_Trending/se/self-llm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

培训信息管理系统开题答辩全流程实战指南

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

作者头像 李华
网站建设 2026/9/12 9:59:43

10分钟跑通你的第一个pytest测试:Python测试框架手把手教程

10分钟跑通你的第一个pytest测试&#xff1a;Python测试框架手把手教程 【免费下载链接】pytest The pytest framework makes it easy to write small tests, yet scales to support complex functional testing 项目地址: https://gitcode.com/GitHub_Trending/py/pytest …

作者头像 李华
网站建设 2026/9/12 9:58:51

结构化提示技术在代码语义分析与系统重构中的应用

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

作者头像 李华
网站建设 2026/9/12 9:58:29

CMSIS-FreeRTOS源码静态审计:嵌入式RTOS可信度深度解析

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

作者头像 李华