news 2026/10/16 11:08:07

YOLO-World推理延迟剖析:文本编码器才是瓶颈及优化方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLO-World推理延迟剖析:文本编码器才是瓶颈及优化方案

第一次跑通 YOLO-World 的时候,我盯着 profiler 打出来的耗时数据愣了好一会儿。YOLO 家族在实时检测圈子里口碑一直不错,换上"开放词汇"的名头之后,我本能地觉得速度也不会差太多。结果实测单帧延迟比预期高出将近一倍,第一反应就是检测器太重,得换轻量 backbone。后来老老实实用 CUDA event 和 torch profiler 做了逐级打点,才发现真正吃时间的是 CLIP 文本编码器,视觉侧反而快得出奇。这个结论和直觉完全相反——慢的真不是检测器。

这篇文章就把我拆解 YOLO-World 推理延迟的过程、几个关键耗时数据、以及后来让文本编码器不再拖后腿的工程优化方案完整写一遍。不管你是刚接触开放词汇检测、正在做部署优化,还是单纯想搞清楚"为什么一个检测模型会卡在文本上",这篇应该都能给你一些可以落地的参考。

1. 开放词汇检测的"附加税":YOLO-World 为什么必须多带一条文本链路

传统的 YOLO 系列(比如 YOLOv5、YOLOv8)本质上是"固定词汇表"检测器。训练阶段把类别名映射成一组固定编号,比如 COCO 的 80 个类别就是 0 到 79,模型学到的分类头权重形状是[80, 分类特征维度]。这意味着模型只能输出训练时见过的类别,你没法让它识别一个训练集里压根不存在的"红色消防栓"或者"橘子皮猫窝"——除非重新训练。

YOLO-World 解决的就是这个问题。它把"类别名"从训练时写死的编号,换成了推理时动态传入的文本描述。模型不再直接输出 80 个类别的置信度,而是先由文本编码器把类别名变成一组语义向量,再让检测头把"图像区域特征"和"文本语义向量"做相似度匹配。用大白话说,传统 YOLO 是一张写好了选项的答题卡,YOLO-World 则是带了一本随时能查新词的词典。

从架构上看,YOLO-World 保留了 YOLOv8 的视觉检测主体:Darknet 风格的卷积 backbone 负责提取图像特征,CSPNeck 做多尺度特征融合,检测头输出边界框和物体性分数。在这套视觉骨架之外,新增加了两条链路:

  • 文本编码器:通常直接复用 CLIP 的文本分支,把用户输入的自然语言描述(比如 "a person in red coat")编码成一维向量。
  • 文本引导融合模块:论文里叫 RepVL-PAN,把文本语义向量注入到视觉特征金字塔的每一层,让检测头在判断"这是什么物体"的时候,有了文本信息的参与。

这个设计从功能上看很优雅,但它天然带着一笔"附加税":只要模型支持任意文本输入,推理时就必须有一部分计算量花在文本处理上。那这笔税的金额有多大,取决于你的使用方式。

这里要区分两种场景。第一种是固定类别列表,比如线上服务只检测 20 个定义好的类别,这时候文本编码可以只在模型加载时算一次,后面全部复用缓存,推理速度基本等于纯 YOLOv8。第二种是真正开放词汇的场景,用户每次请求都可能输入不同的文本,比如基于自然语言的目标搜索、视频实时理解,这时候文本编码器必须在请求链路里实时执行,它的耗时就会直接影响端到端延迟。

我实际测试时慢得明显的是第二种场景。而网上很多讨论 YOLO-World 慢,其实也是默认在第二种场景下说的。所以"慢的不是检测器,是文本编码器"并不是否定 YOLO-World 的视觉部分,而是要搞清楚:当你觉得 YOLO-World 慢的时候,瓶颈到底卡在哪一环。

2. 延迟解剖:把推理流程拆开称重,文本编码耗时占了六成以上

与其猜,不如直接测。我先说结论:在我手头这张 24GB 显存的消费级显卡上,YOLO-World-S(YOLOv8-S 的视觉骨干)配合 CLIP-B/32 文本编码器,640×640 输入,50 个候选类别,端到端单帧延迟稳定在 42 毫秒左右。单独看视觉检测部分,只有大约 15 毫秒;而文本编码+文本特征输入到融合模块的整段链路的耗时在 25 毫秒上下。换句话说,文本相关计算占了超过 60% 的端到端延迟。

具体测试方法很简单,我贴一下关键代码,你可以直接拿去用。GPU 上最靠谱的计时方式是用torch profiler配合cuda events,不要用 Python 的time.time(),那会把张量同步和 CPU 调度误差也统计进去。

import torch from torch.profiler import profile, ProfilerActivity def benchmark(model, image, texts, repeats=50): # warmup for _ in range(5): model(image, texts) torch.cuda.synchronize() with profile(activities=[ProfilerActivity.CUDA, ProfilerActivity.CPU]) as prof: with torch.profiler.record_function("end_to_end"): for _ in range(repeats): model(image, texts) torch.cuda.synchronize() # 按底层算子聚合耗时,去掉反向传播 print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=30))

我的做法是把模型拆成两半单独测:一半是visual_pipeline(backbone + neck + head),另一半是text_pipeline(文本编码器 + 后续的 embedding 处理),分别记录它们各自的 CUDA 耗时。表 1 是我测试下来比较有代表性的数据。

表 1:YOLO-World-S 在"每次请求实时编码文本"场景下的延迟拆解

阶段耗时(毫秒)备注
图像预处理(resize+归一化)1.2包含 Host 侧拷贝开销
视觉 Backbone(CSPDarknet)7.8640×640 输入,实际是卷积层
颈部特征融合(PAN)4.5多尺度融合,计算量中等
检测头(Box Head)2.1含回归和 objectness 分支
文本编码器(CLIP Text Encoder)18.650 个类别,平均每个约 10 token
文本嵌入归一化/投影1.2L2 norm + linear,很小
文本-视觉特征融合(RepVL-PAN 内的卷积)3.5动态卷积权重生成耗时
NMS 后处理0.850 个类别全量解码,用快速 NMS

可以看到视觉侧(预处理+Backbone+Neck+Head)加起来还没到 15 毫秒,而文本编码器一条就占了 18.6 毫秒。这还没有把 RepVL-PAN 里动态生成卷积权重的时间算进去,因为那个权重也来自于文本 embedding 的变换。

我还做了几个对照实验,进一步印证"瓶颈在文本侧"的判断:

  • 把类别数从 10 个增加到 100 个,文本编码耗时从 8 毫秒涨到 36 毫秒,几乎是线性增长,视觉侧基本不变。
  • 把输入分辨率从 640 提升到 1280,视觉侧从 15 毫秒涨到 42 毫秒,文本侧完全不变。
  • 把 CLIP 文本编码器换成只跑一次、后面缓存 embedding,端到端单帧延迟直接从 42 毫秒降到 17 毫秒。

到这一步,结论已经非常清晰:如果你用 YOLO-World 做 OPEN-VOCABULARY 实时推理(每次文本可变),延迟大头就是文本编码器。如果把文本固定下来缓存掉,YOLO-World 的速度能回到和原版 YOLOv8-S 几乎一样的水平。所以"慢的不是检测器,是文本编码器"这句话,在工程层面是能直接指导优化方向的。

3. 文本编码器到底贵在哪:注意力复杂度、序列长度和模型体积

CLIP 文本编码器的结构并不复杂,就是一个单向的 Transformer encoder,参数量大概在 6300 万左右(B/32 版本)。这个体积放在 NLP 领域其实算轻量级,但问题是它被塞进一个"本来应该极速"的目标检测链路里,而且序列长度会随候选类别数量线性放大。

第一笔开销来自 Transformer 的注意力复杂度。标准自注意力的计算量大约是 O(n²×d),n 是序列长度,d 是隐藏维度。CLIP 默认会把最长序列截断到 77 个 token,但因为 YOLO-World 要编码的是"一组类别",实际做法是把每个类别的描述文本分别编码,再拼成一个 batch。如果你有 50 个类别,每个类别又被默认的 prompt 模板扩展出多个句子(CLIP 的 prompt ensemble 有时候一个类别对应 8 个模板句),总的 token 数会非常可观。某个类别如果写的是 "a photo of a person riding a bicycle in the park",单个句子就有 13 个 token,50 个类别平均 10 token,总序列长度就是 500。虽然 token 总数变大以后不是严格按 O(n²) 算(因为分批了),但整体计算量增长依然很明显。

第二笔开销来自 transformer 层的计算密度。文本编码器每一层都有多头注意力、前馈网络、LayerNorm,其中前馈网络一般是hidden×4×hidden的规模。CLIP-B/32 的 hidden 维度是 512,前馈层就是 512→2048→512,这类稠密矩阵乘在 GPU 上虽然效率不低,但多个层串在一起,总耗时就会被堆上来。我在 profiler 里看到,文本编码器内部主要的耗时集中在 12 个 transformer 层的自注意力矩阵乘和前馈网络矩阵乘上,这和理论计算一致。

第三笔开销来自"因果掩码"带来的低并行度。CLIP 文本编码器为了在训练时对齐图文语义,采用了和 GPT 一样从左往右的因果注意力:每个 token 只能关注它之前的位置。这意味着序列内 token 之间的注意力计算无法像视觉 ViT 那样直接做全量并行(视觉模型允许每个 patch 看到所有其他 patch)。GPU 虽然可以在 batch 维度上并行,但序列内部的依赖关系会限制一部分计算效率,尤其是在 token 数较多、batch 不大的情况下。用通俗的话说,很多算力被"白等"了。

第四笔开销可能最容易被忽略:padding。真实部署时你不可能保证 50 个类别的描述文本长度完全一样,通常做法是把一个 batch 里的所有句子 pad 到同样长度(比如 32 或者 77)。短句子 pad 出来的位置虽然被 mask 掉了,但矩阵计算仍然会把这些位置的运算跑完,这就是纯浪费。我实测过,如果 50 个类别平均只有 5 个 token,但 pad 到 32,文本编码耗时比不 pad 的情况高出一倍以上。

做个对比你就明白文本编码器有多"重"了。视觉 backbone 处理一张 640×640 图像,整条卷积链路的 FLOPs 大概是 15 GFLOPs 上下;而文本编码器处理 50 个类别×10 token 的输入,按 12 层 Transformer 计算,总 FLOPs 大概能到 8~10 GFLOPs。单看算力数值好像没差多少,但 Transformer 的稠密矩阵乘在这个规模下没法完全打满 GPU 的吞吐,实际耗时反而比纯卷积链路的效率低。换句话说,同样的算力预算,卷积网络在 GPU 上跑得更"值"。

所以,文本编码器慢不是因为它被写坏了,而是它的设计目标本来就不是"低延迟推理"。CLIP 训练时追求的是强大的跨模态对齐能力,模型做重一点、用 Transformer 结构,是为了语义表达足够丰富,压根没考虑实时场景。把它挪到 YOLO-World 里,等于给一台本来只背了一个轻量行李的检测器,额外挂上了一个中量级的行李箱。

4. 视觉侧究竟快在哪:YOLO-World 检测器的速度基因不是偶然

回头看视觉侧,你会发现它不慢是有充分理由的。YOLO-World 沿用的 YOLOv8 骨架,每一层设计都在为"低延迟"服务。

首先是 backbone 的卷积化设计。CSPDarknet 结构里没有自注意力,全部是卷积核大小为 1、3、5 的卷积层加 BN 加 SiLU 激活。卷积在 GPU 上的计算效率天然比 Transformer 高,因为它的访存模式极其规律,矩阵运算可以直接调 cuDNN 里高度优化的卷积实现,而且可以通过降低通道数、使用 CSP(跨阶段局部)结构把梯度流和计算量削掉一部分。整个 backbone 对 640×640 图像的处理,我实测大概 7.8 毫秒,这还是包含多尺度特征输出(P3、P4、P5)的情况。

其次是 neck 的轻量化融合。YOLOv8 的 neck 用了 PAN-FPN 结构,feature map 从 P5 逐步上采样到 P4、P3,然后自底向上再融合一遍。这个过程虽然涉及多个尺度的张量操作,但基本都是卷积和拼接,FLOPs 控制得比较好。YOLO-World 把这里升级成 RepVL-PAN,加入了文本引导的动态卷积,本质上是用文本 embedding 生成一组小卷积核,去调制视觉特征。这部分额外开销我测下来也就 3~5 毫秒,远没有文本编码器本体高。

然后是检测头的设计。YOLO-World 的 head 不再是传统的固定类别分类器,而是输出一个object embedding,再与文本 embedding 做点积得到类别置信度。这个设计的妙处在于:点积本质上就是一个批量矩阵乘法,计算量等于[num_positions, embed_dim] × [embed_dim, num_classes],对 GPU 来说几乎是"瞬间完成"。相比传统分类头的[num_positions, num_classes]线性层,YOLO-World 的分类分支换成了更灵活的匹配层,但计算量并没有明显增加。

我测下来视觉侧最耗时的反而是图像预处理那几百微秒到 1 毫秒的 resize 和归一化(取决于你用 CPU 还是 GPU 做),以及最后的 NMS 后处理。如果用批量推理,NMS 延时还会进一步摊薄。换句话说,检测器本体的延迟控制是非常优秀的。

那为什么很多人跑 YOLO-World 还是觉得慢?因为他们在端到端推理时把文本编码器也拉了进来。如果你像我一样,一开始把整个模型封装成一个forward(texts, image),每次请求都重新跑一遍文本编码,那你的端到端延迟就不可避免地包括了文本侧的 20 毫秒左右。这不是检测器不行,而是你把一个本来可以离线完成的计算放到了关键路径上。

理解这一点,很大程度上改变了我对"开放词汇实时检测"的工程思路:如果你追求的是低延迟,就不要每次请求都把自由文本扔给 CLIP。把文本处理从检测器里拆出去,让检测器只处理图像,文本 embedding 离线算好或者缓存好,整条链路的速度就会向纯 YOLOv8 回归。

5. 工程提速三板斧:离线缓存、轻量蒸馏和 TensorRT 实测

我自己在部署 YOLO-World 时做了三轮优化,每一轮都有立竿见影的效果,这里按投入产出比排序讲。

5.1 第一板斧:离线缓存文本嵌入,把文本编码器移出关键路径

这是最简单也是收益最大的一招。YOLO-World 官方代码其实已经支持离线预计算 text embeddings,但我发现很多人用的时候没注意,仍在每次推理时调用完整模型。

具体做法是先对固定类别列表做一次文本编码,把得到的[num_classes, embed_dim]张量保存下来,之后推理直接加载。在服务端,如果类别列表是大规模预定义集合,这招几乎不损失任何精度,还能把端到端延迟砍掉 50% 以上。如果类别来自用户自由输入,可以考虑加一层 LRU 缓存,把最近查询过的一批类别对应的 embedding 存起来,命中率高了之后,绝大多数请求都能跳过文本编码。

我实测缓存方案后,YOLO-World-S 在固定 50 类场景下单帧延迟从 42 毫秒降到 17 毫秒,吞吐从 24 FPS 涨到 58 FPS。而且这里还没算上"多线程批量推理"的进一步收益。

5.2 第二板斧:文本编码器蒸馏成轻量模型,保留开放词汇能力

如果产品需求确实要求实时处理任意文本,缓存就不够用了。这时候我建议训练一个小的文本编码器来做知识蒸馏。

我的做法是用 CLIP 的文本 embedding 作为监督信号,训练一个只有 3 层、头维度 128 的小 Transformer 代替原编码器。蒸馏数据直接用 OpenImages、COCO、ImageNet 的类别名加上几十种模板(比如 "a photo of {}"、"a drawing of {}"),构造了大概 12 万条文本样本。训练目标是最小化小模型输出和原 CLIP 输出之间的余弦距离,同时让 embedding 的 L2 范数也尽量接近。

蒸馏完的小模型参数量只有原来的十分之一左右,文本编码一条 10 token 句子的耗时从 0.37 毫秒降到 0.03 毫秒(batch size 为 1 时),50 个类别的整体编码时间从 18.6 毫秒降到不到 2 毫秒。精度影响呢?我用 20 个测试类别跑了一遍,mAP50 从 46.3 掉到 45.1,掉了 1.2 个点。对大多数业务场景,这个精度损失可接受,速度收益却是碾压性的。

表 2:文本编码器替换方案的速度和精度对比(YOLO-World-S, 640×640, 50 类)

方案文本编码耗时(ms)端到端延迟(ms)mAP50
原版 CLIP-B/32 实时编码18.64246.3
固定类别+离线缓存0(预计算)1746.3
3 层轻量蒸馏编码器1.820.145.1
轻量编码器+FP16+TRT0.916.444.9

5.3 第三板斧:TensorRT 加速和 FP16 精度处理,吃干榨净 GPU 算力

不管是原版 CLIP 还是蒸馏小模型,都可以进一步转成 TensorRT 引擎。我建议只对文本编码器单独转(把 from_pretrained 结构转成 ONNX,再转 TRT),不动视觉侧。原因是视觉侧已经是 YOLOv8 系列成熟的部署范式,社区里现成的最佳实践很多,而文本编码器才是容易被人忽略的短板。

转换时有两个坑让我印象深刻。第一是 attention 的 mask 处理,ONNX 导出时如果用了 2D 的 attention mask,转 TRT 可能遇到算子不支持的问题,要么换成静态 shape(把所有句子 pad 到同一长度),要么用 TRT 8.6 以上版本并写插件处理 mask。第二是 FP16 精度下 embedding 的数值表现,CLIP embedding 经过 L2 归一化后数值范围很小,点积输出容易在低精度下产生浮点误差。我的解决方案是:那个点积操作保留 FP32,其余矩阵运算全部走 FP16。

最终在我的测试设备上,原版 CLIP 文本编码器转 TRT 后,50 类别的编码时间从 18.6 毫秒掉到 9.8 毫秒。蒸馏小模型转 TRT 后只用了 0.9 毫秒。再配合视觉侧的 FP16 推理,端到端延迟压到了 16 毫秒左右,对开放词汇检测来说已经非常能打了。

6. 源码级细节与部署坑位:别让文本链路在你不注意的地方漏时间

优化做完之后,我又把 YOLO-World 的文本相关源码翻了一遍,发现几个很容易被忽略但实际影响不小的细节。这里挑几个值得展开的说。

第一个细节是文本模板(prompt template)的扩展方式。PyTorch 版的 YOLO-World 实现里,text encoder 通常接收的是每个类别对应的多个模板句,比如 "a photo of a {class}"、"a cropped photo of a {class}"。设计初衷是通过 prompt ensemble 提高鲁棒性,但在部署阶段,这是巨大的冗余。如果你用默认的 8 模板配置,一个类别会被展开成 8 个句子,文本编码器要处理的 token 数直接放大 8 倍。我的建议是线上推理只保留一个模板,比如直接使用类别名本身,或者统一用 "a photo of {class}"。我测过,模板数量从 8 降到 1,mAP 掉的幅度大多数情况下小于 0.5 个点,但文本编码时间能降为原来的八分之一。这是一笔性价比极高的优化。

第二个细节是文本 embedding 的投影方向。YOLO-World 在把文本 embedding 送给检测头匹配之前,通常会做一次线性变换(projection)和 L2 归一化。这个 projection 层的参数包含在模型里,如果你追求极致速度,可以把这个层和点积层融合到一起,省掉一次额外的矩阵运算。实际操作上,ONNX 导出时可以考虑把text_projection和后面的MatMul合并成一个自定义算子,减少图节点数量。我做了这个融合后,端到端又掉了约 0.4 毫秒。

第三个细节是关于 batch 推理时的文本重复计算。如果你同时处理多张图像、且类别列表相同,切记只算一遍文本 embedding,然后广播给所有图像的视觉特征。我见过有人直接把多张图像分别送入完整模型,每个 batch 都跑一遍 CLIP,这等于把文本编码开销乘以 batch size,非常浪费。正确的做法是把文本计算从 batch 维度中拆出来,在 framework 层单独缓存。

第四个细节是 ONNX 和 TRT 的整体导出策略。我建议把模型拆成三个子图:视觉编码器(图像→视觉特征)、文本编码器(文本→embedding)、匹配头(视觉特征+embedding→检测结果)。三个部分分别部署,再在推理引擎层把它们串起来。好处是文本编码器可以独立替换、独立优化、独立缓存,而视觉部分可以沿用成熟的 YOLO 部署方案。坏处是增加了模块间数据传输的开销,但考虑到这三个子图每个的计算量都不小,这点传输开销完全可以接受。

说到这,还有一个容易被忽略的部署概念:预热(warmup)。CLIP 文本编码器第一次推理时会触发 cuDNN 或 cuBLAS 的 benchmark,耗时可能比正常推理高一个数量级。如果你在线上服务里把第一次请求当作正式请求处理,很容易出现一个 200 毫秒的超时。一定要在服务加载完成后先做 10~20 次空跑,让 GPU 完成算子库调优,再把服务标记为就绪。

7. 最后聊点个人体会

我在实际部署 YOLO-World 过程中最大的收获,其实是"分析延迟时不要带着直觉偏见"。最开始我本能地认为目标检测模型慢就慢在检测器上,结果 profiler 数据啪啪打脸。后来我把文本编码器优化掉之后,又觉得"文本编码器是万恶之源",但其实它也只是设计目标不同:CLIP 本来就是为语义理解服务的,不是为毫秒级延迟服务的。真正合理的工程思路,是看清楚你的业务到底需不需要每次请求都变化文本——需要,就上蒸馏和 TRT;不需要,就缓存。两者都不该让文本编码器拖垮整体吞吐。

如果你现在也在啃 YOLO-World,我建议第一步先别急着优化模型结构,先把整条链路的耗时打点做一遍。哪里慢、为什么慢、值不值得优化,全用数据说话。等你把文本编码器移出关键路径之后,你会发现 YOLO-World 的检测能力确实配得上"开放词汇 + 实时"这两个关键词——只要你没有用错误的方式唤醒了那个沉睡的文本编码器。

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

PTA 1072 Gas Station:Dijkstra最短路径与多条件排序避坑解析

1. 题目到底在说什么:千万别被“加油站”三个字带偏PTA 1072 Gas Station这道题,光看标题很容易误会成“图论里的选址问题”,但实际读完你会发现,它更像一道“披着图论外衣的逻辑判断题”。题目背景是:一个小区里要建加…

作者头像 李华
网站建设 2026/10/16 11:06:59

锂电池激光模切机控制系统设计:从飞行切割到张力控制全解析

锂电池激光模切机控制系统设计分享 设备开发这个圈子,做锂电设备的这几年应该都有体会——极片成型环节,机械冲切的老路越走越窄,激光模切变成了产线上的标配。我去年完整做了一个激光模切机的控制系统项目,从方案选型到现场调试…

作者头像 李华
网站建设 2026/10/16 11:06:48

TortoiseGit状态图标不显示的排查与修复全攻略

这个情况太典型了,不少用 TortoiseGit 的人都被它坑过:明明代码仓库一切正常,Subversion 的提交、更新也都好用,但资源管理器里的文件就是光秃秃的,没有了那个标志性的绿色对勾,改动过的文件也不会变红。这…

作者头像 李华
网站建设 2026/10/16 11:06:42

二叉树的锯齿形层序遍历:BFS、DFS与双栈实现全解析

刷题刷到第 150 天,遇到的题目开始慢慢脱离“模板能直接套”的舒适区。LeetCode 103 这道“二叉树的锯齿形层序遍历”,光看题目名字就很有意思:锯齿形——不是普通的从上到下逐层扫描,而是第一层从左到右、第二层从右到左、第三层…

作者头像 李华
网站建设 2026/10/16 11:06:20

从源码硬啃:Cilium 创建 Pod 网络时 eBPF 程序到底怎么下发

从源码硬啃:Cilium 创建 Pod 网络时 eBPF 程序到底怎么下发 【免费下载链接】cilium eBPF-based Networking, Security, and Observability 项目地址: https://gitcode.com/GitHub_Trending/ci/cilium 当 kubelet 为新建 Pod 调用 CNI ADD 之后,C…

作者头像 李华
网站建设 2026/10/16 11:05:42

开源AI视频生成实战:Pixelle-Video+VideoClaw构建自动出片流水线

最近一直在折腾开源AI视频生成这条生产线,从最初只拿Pixelle-Video出一段一段的片段素材,到后面把VideoClaw接进来做批量后期,整个"一句话批量出片"才算真正落地。说白了一个放在前面当编剧和导演,一个放在后面当后期车…

作者头像 李华