ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

UEmbed:统一多模态稀疏与稠密嵌入的检索新思路

UEmbed:统一多模态稀疏与稠密嵌入的检索新思路 最近在做多模态检索相关的研究与工程落地时频繁接触到一个关键词UEmbed。很多同学第一眼看到这个名词会以为它是一个单纯的 Embedding 工具包但实际上它代表了一类非常关键的模型设计思路把稀疏表示和稠密表示统一到一个多模态框架里。这篇文章会把这个方向从概念到实践完整拆开包含 UEmbed 的设计动机、核心原理、与常规 Dense Retriever / Sparse Retriever 的差异以及一个可运行的多模态混合检索 Demo。无论你是刚开始接触向量检索的新手还是已经在业务中做过 RAG 的开发者这篇文章都值得收藏备用。1. 背景与核心概念1.1 为什么需要 UEmbed先看一个非常实际的问题在搜索、推荐、RAG、图数据库召回等场景里我们经常需要把文本、图片、音频、视频等不同模态的数据转成向量然后做相似度检索。这个“转向量”的过程就是生成 Embedding。传统的做法通常分成两条路线Dense Embedding用一个深度模型如 BERT、CLIP把整个输入编码成一个固定长度的稠密向量检索时用余弦相似度或内积进行 Top-K 召回。优点是语义理解强能处理同义改写、语义匹配缺点是训练和推理成本较高对长尾实体如人名、产品型号、专业术语容易丢失精确匹配信息。Sparse Embedding基于词袋、词权重或词汇表构造一个高维稀疏向量检索时用倒排索引加速。优点是精确匹配好、可解释性强、检索速度快缺点是存在词汇鸿沟问题同一个语义用不同词表达稀疏表示可能完全匹配不上。这两条路线一直处于“各管各”的状态。UEmbed 想要解决的核心问题正是如何在一个统一模型中同时生成高质量的多模态稀疏和稠密向量让检索系统既能利用稠密向量的语义泛化能力又能保留稀疏向量的精确匹配与可解释性。1.2 什么是多模态 Embedding多模态 Embedding 指的是把图像、文本、音频、视频等不同模态的数据映射到同一个向量空间中让语义相似的多模态内容在空间中的距离更近。比如“一张猫的图片”和“一只猫在睡觉的文字描述”经过多模态模型编码后二者在向量空间里的余弦相似度应该高于“一只猫的图片”和“一辆汽车的文字描述”。CLIP 就是非常典型的多模态稠密表示模型它通过图像编码器和文本编码器将图片和文本映射到共享空间。但这种共享空间通常是稠密向量很难同时输出可解释的稀疏信号。UEmbed 的思路则是在多模态编码的基础上额外设计稀疏输出分支从而形成真正的“统一”表示。1.3 UEmbed 的典型应用场景场景传统方案UEmbed 优势文本检索 / RAG 召回BM25 Dense Retriever 两路召回单一模型同时输出稀疏稠密简化流水线兼顾精确与语义图文跨模态检索CLIP 稠密向量 标签倒排精确标签匹配与语义匹配统一建模减少漏召回视频/音频片段检索音频标签 音频 Embedding多模态 token 级的稀疏路由可定位到局部片段商品搜索长尾 ID稠密向量为主经常丢失商品编号稀疏分支保留精确 token 匹配显著提升 ID 类召回多语言混合检索每种语言单独建模统一向量空间跨语言查询更稳定简单来说凡是需要在“语义理解”和“精确匹配”之间取得平衡的多模态检索场景UEmbed 都有明显的用武之地。2. 稀疏嵌入与稠密嵌入两条路线的关键技术点要理解 UEmbed首先要把 Sparse Embedding 和 Dense Embedding 各自的技术机理弄清楚。2.1 Dense Embedding 的核心思路Dense Embedding 通常由 Encoder 模型产生。以文本为例输入经过 Transformer 编码后取[CLS]token 的隐状态或者对所有 token 向量做平均池化mean pooling得到一个固定维度如 768、1024 维的稠密向量。# 伪代码示例Dense Embedding 的生成过程 import torch.nn.functional as F def generate_dense_embedding(model, input_ids, attention_mask): outputs model(input_idsinput_ids, attention_maskattention_mask) # 取 token 级别的隐藏状态 token_embeddings outputs.last_hidden_state # [batch, seq_len, hidden_dim] # 对非 padding 位置做 mean pooling input_mask_expanded attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float() sum_embeddings torch.sum(token_embeddings * input_mask_expanded, dim1) sum_mask torch.clamp(input_mask_expanded.sum(dim1), min1e-9) dense_emb sum_embeddings / sum_mask # 归一化 dense_emb F.normalize(dense_emb, p2, dim1) return dense_emb优点语义相近但词面不同的内容也能匹配。向量维度固定方便用 ANN近似最近邻索引加速。兼容各种深度模型结构扩展性好。缺点可解释性差无法直观看到“因为哪个词而匹配”。长尾实体、ID 类信息容易在池化中被稀释。训练需要较多样本否则模型容易偏科。2.2 Sparse Embedding 的核心思路稀疏嵌入不是一个新概念最经典的词袋Bag of Words、TF-IDF 都是稀疏表示。但在现代深度学习系统中稀疏嵌入通常指基于词汇表的权重向量例如 SPLADE 模型。SPLADE 的做法是对输入文本的每个 token经过 MLM 头预测该 token 在整个词表上的概率分布然后把分布展开成一个 30K 或 100K 维的稀疏向量。由于大部分词表位置概率为 0实际存储时只需要保存非零项的(token_id, weight)对。# 伪代码示例SPLADE 风格稀疏向量生成 def generate_sparse_embedding(model, input_ids, attention_mask): outputs model(input_idsinput_ids, attention_maskattention_mask) token_embeddings outputs.last_hidden_state # [batch, seq_len, hidden_dim] # 映射到词表大小的 logits logits model.mlm_head(token_embeddings) # [batch, seq_len, vocab_size] # 对 token 维做 log 求和得到词表维度的稀疏 logits sparse_logits torch.log1p(F.relu(logits)).sum(dim1) # [batch, vocab_size] return sparse_logits优点精确匹配有保障适合专业术语、人名、编号等。可利用倒排索引Inverted Index检索效率高。可解释性好能回溯到具体命中词。缺点语义泛化能力弱同义改写无法匹配。向量维度极高存储和排序都不能直接套用 ANN 常规方式。多模态场景下图像、音频如何映射到“词表”是一个需要特殊设计的难点。2.3 两者互补关系从信息检索的经典理论来看稀疏和稠密分别对应了“词汇匹配”和“语义匹配”二者不是替代关系而是互补关系。业界常用的混合检索方案是用 BM25/SPLADE 进行稀疏召回。用 Dense Retriever 进行稠密召回。用 RRFReciprocal Rank Fusion或 Score 加权融合两路结果。这种方案的缺点是工程链路复杂需要维护两套索引和两套模型。UEmbed 的出发点就是希望用一个模型同时解决两件事从而降低系统复杂度同时保持两种表示的优势。3. UEmbed 核心架构原理拆解UEmbed 全称是Unified Sparse and Dense Multimodal Embeddings它的核心目标是把多模态编码、稀疏嵌入、稠密嵌入放进同一个训练框架。下面拆解它的几个关键技术点。3.1 统一的编码器主干UEmbed 采用一个共享的多模态编码器作为主干。对于文本使用 Text Encoder对于图像使用 Vision Encoder。两类编码器的输出会被对齐到同一个隐空间便于后续统一训练。这里的关键是不要让每个模态单独训练一套 Embedding而是让它们共享底层的语义空间。类似 CLIP 的对比学习思路但 UEmbed 不止输出一个稠密向量还会在编码器之上挂载稀疏输出头。3.2 稀疏和稠密交织路由Sparse-Dense Interwoven Routing这是 UEmbed 比较有特色的设计概念。很多模型是“一个编码器接两个输出头”一个头输出稠密另一个头输出稀疏。但在实际实现中如果两个头完全独立会造成两个表示的空间不一致。UEmbed 的思路是让稀疏路由和稠密路由在编码器内部就产生交互模型在学习文本表示时某些 token 会被分配给稀疏通道用于精确匹配某些语义灵活的 token 会被分配给稠密通道用于泛化匹配。二者共享底层特征但通过路由机制各司其职。用工程语言理解你可以把它想象成一个“多任务学习架构”主任务多模态对比学习让稠密表示互相靠近。辅助任务稀疏 token 预测让模型学会哪些词该在词汇表中被激活。3.3 多向量对比学习Multi-Vector Contrastive Learning常规对比学习如 CLIP使用的是单向量相似度例如sim cos(text_emb, image_emb)UEmbed 场景下由于模型既要生成稠密向量又要生成稀疏向量训练目标也需要设计成“多向量对比”的形式。训练时要同时优化Dense-Dense 对比损失正样本对距离近负样本对距离远。Sparse-Sparse 匹配信号稀疏向量中的非零项是否与 ground truth 中的关键词一致。跨模态稀疏协同图像经过编码器后是否能在共享词汇表中激活与文本一致的语义词。训练完成的模型在推理时一次前向传播即可同时得到两种向量不需要额外拆分模型。3.4 一次推理、双路输出推理阶段UEmbed 对一个输入比如一张图片和它的描述文本只做一次前向计算然后同时输出dense_emb固定维度例如 768 维的稠密向量归一化后用于 ANN 检索。sparse_emb高维稀疏向量存储为{token_id: weight}的字典用于倒排索引检索。这套设计带来的直接好处是线上检索只需要维护一个模型服务、一套特征抽取流程然后再决定是用稠密索引、稀疏索引还是两者融合。4. 环境准备与工具说明4.1 基础环境因为 UEmbed 不是一个现成的 pip 包而是代表一个建模方向所以本文的 Demo 会基于常见的 Python 深度学习栈来演示“统一稀疏稠密多模态嵌入”的实现思路。你需要提前准备依赖用途版本建议Python运行环境3.9 或更高PyTorch模型训练与推理2.x 版本即可注意和 CUDA 匹配Transformers加载 BERT 等基础模型4.x 版本Sentence-Transformers简化 Embedding 模型加载2.x 版本可作为辅助FAISS稠密向量 ANN 检索1.7.x 或更新版本NumPy / SciPy数据处理与稀疏矩阵操作最新稳定版即可版本需要根据你的实际开发环境调整本文示例以常见环境为例重点演示配置思路与代码逻辑不保证任意版本组合都能直接跑通。4.2 安装命令pip install torch transformers sentence-transformers faiss-cpu numpy scipy如果使用 GPU 训练建议按 PyTorch 官网给出的命令安装对应 CUDA 版本的 torch。4.3 示例项目结构为了便于阅读我们把 Demo 项目放在uembed_demo/目录下uembed_demo/ ├── data/ │ ├── texts.tsv # 文本数据 │ └── images_meta.tsv # 图像元数据 ├── models/ │ └── encode.py # 统一编码模块 ├── index/ │ ├── build_index.py # 构建双路索引 │ └── search.py # 混合检索脚本 └── requirements.txt接下来我们逐个模块实现。5. 实战基于 UEmbed 思路搭建多模态混合检索 Demo为了让读者更容易落地这里不直接把完整论文源码搬过来而是实现一个“UEmbed 风格”的最小可运行系统输入一段查询文本系统同时用稠密向量和稀疏向量在图文混合语料中检索并把结果融合返回。5.1 准备多模态示例数据先造一批少量多模态数据包含图片路径和对应的文本描述。文件路径data/images_meta.tsv。image_id text img_001 一个穿红色裙子的女孩站在海边 img_002 一辆黑色汽车行驶在高速公路上 img_003 森林里的棕色小熊正在吃蜂蜜 img_004 一只白色猫咪趴在窗台上晒太阳 img_005 城市夜景中霓虹灯闪烁的街道 img_006 厨师在厨房里制作牛排 img_007 运动员在操场上跑步 img_008 蓝色海洋上空的白色海鸥这里我们只演示文本模态的检索但数据结构保持多模态形态方便后续接入图像编码器。如果你需要真正接入图像可以使用 CLIP 的 Vision Encoder 生成图像向量并把图像向量也写入索引。接下来以文本为主展开。5.2 统一编码模块文件路径models/encode.py这个模块的作用是给定一段文本同时返回它的稠密向量和稀疏向量。# 文件路径models/encode.py import torch import torch.nn.functional as F from transformers import AutoTokenizer, AutoModelForMaskedLM class UnifiedEncoder: 一个极简的 UEmbed 风格编码器 输入文本 - 同时输出稠密向量和稀疏向量。 def __init__(self, model_name: str bert-base-uncased, max_len: int 64): self.device torch.device(cuda if torch.cuda.is_available() else cpu) self.tokenizer AutoTokenizer.from_pretrained(model_name) self.model AutoModelForMaskedLM.from_pretrained(model_name) self.model.to(self.device) self.model.eval() self.max_len max_len self.vocab_size self.model.config.vocab_size torch.no_grad() def encode(self, texts): 返回一个 dict - dense: [batch, hidden_dim] 归一化后的稠密向量 - sparse_indices: list[list[int]]每个文本的非零 token id - sparse_weights: list[list[float]]每个文本的非零权重 inputs self.tokenizer( texts, paddingTrue, truncationTrue, max_lengthself.max_len, return_tensorspt, ).to(self.device) outputs self.model(**inputs, output_hidden_statesTrue) last_hidden outputs.hidden_states[-1] # [batch, seq_len, hidden_dim] # ----- 稠密向量mean pooling L2 归一化 ----- attention_mask inputs[attention_mask] mask_expanded attention_mask.unsqueeze(-1).float() sum_hidden (last_hidden * mask_expanded).sum(dim1) sum_mask mask_expanded.sum(dim1).clamp(min1e-9) dense sum_hidden / sum_mask dense F.normalize(dense, p2, dim1) # ----- 稀疏向量MLM 头 log(1ReLU) ----- mlm_logits outputs.logits # [batch, seq_len, vocab_size] sparse_logits torch.log1p(F.relu(mlm_logits)) # [batch, seq_len, vocab_size] # 对序列维度求和得到 [batch, vocab_size] sparse sparse_logits.sum(dim1) # 转成稀疏存储格式 sparse_indices [] sparse_weights [] for i in range(sparse.size(0)): # 只保留非零项并取 top-k这里取前32个 row sparse[i] nz row.nonzero(as_tupleTrue)[0] if len(nz) 32: vals row[nz] topk_idx torch.topk(vals, k32).indices nz nz[topk_idx] sparse_indices.append(nz.cpu().tolist()) sparse_weights.append(row[nz].cpu().tolist()) return { dense: dense.cpu().numpy(), sparse_indices: sparse_indices, sparse_weights: sparse_weights, }这段代码看起来不长但已经把稀疏和稠密两条路径都打通了稠密路径使用last_hidden_state做 mean pooling。稀疏路径借用 MLM 输出把每个 token 对词表的预测分数累加起来形成词表维度的稀疏激活。稀疏向量不直接保存全量 vocab 向量而是转成(index, weight)形式方便后续构建倒排索引。5.3 构建混合索引文件路径index/build_index.py这里的“混合索引”包含两部分稠密索引用 FAISS 建立近似最近邻索引。稀疏索引用 Python 字典模拟倒排表。# 文件路径index/build_index.py import json import numpy as np import faiss from models.encode import UnifiedEncoder def build_sparse_inverted_index(doc_sparse): doc_sparse 是字典列表包含 sparse_indices 和 sparse_weights 返回倒排索引: {token_id: [(doc_id, weight), ...]} inverted {} for doc_id, item in enumerate(doc_sparse): indices item[sparse_indices] weights item[sparse_weights] for token_id, weight in zip(indices, weights): if token_id not in inverted: inverted[token_id] [] inverted[token_id].append((doc_id, weight)) return inverted def main(): encoder UnifiedEncoder() # 读取文档数据 docs [] with open(data/images_meta.tsv, r, encodingutf-8) as f: next(f) # 跳过表头 for line in f: parts line.strip().split(\t) if len(parts) 2: docs.append(parts[1]) # 统一编码 encoded encoder.encode(docs) dense_embs encoded[dense] # 稠密索引 dim dense_embs.shape[1] dense_index faiss.IndexFlatIP(dim) dense_index.add(dense_embs) # 稀疏倒排索引 doc_sparse [ { sparse_indices: encoded[sparse_indices][i], sparse_weights: encoded[sparse_weights][i], } for i in range(len(docs)) ] inverted_index build_sparse_inverted_index(doc_sparse) # 保存索引产物 faiss.write_index(dense_index, index/dense.index) with open(index/inverted.json, w, encodingutf-8) as f: json.dump(inverted_index, f, ensure_asciiFalse) with open(index/docs.json, w, encodingutf-8) as f: json.dump(docs, f, ensure_asciiFalse) print(f构建完成共 {len(docs)} 条文档) print(f稠密索引维度: {dim}) print(f稀疏词表非零 token 数: {len(inverted_index)}) if __name__ __main__: main()5.4 实现混合检索文件路径index/search.py检索逻辑分为三个步骤用同一个编码器生成 query 的稠密向量和稀疏向量。在稠密索引和稀疏索引中分别召回 Top-K。用 RRF 或加权分数融合结果。# 文件路径index/search.py import json import numpy as np import faiss from models.encode import UnifiedEncoder def rrf_fusion(dense_hits, sparse_hits, k60): Reciprocal Rank Fusion 对两路结果按照排名融合分数缓解不同检索路线的分数不可比问题。 scores {} for rank, doc_id in enumerate(dense_hits): scores[doc_id] scores.get(doc_id, 0.0) 1.0 / (k rank 1) for rank, doc_id in enumerate(sparse_hits): scores[doc_id] scores.get(doc_id, 0.0) 1.0 / (k rank 1) return sorted(scores.items(), keylambda x: x[1], reverseTrue) def search(query, top_k5, alpha0.5): alpha 用于加权融合 final_score alpha * sparse_score (1 - alpha) * dense_score 这里默认使用 RRFalpha 参数保留作为扩展。 encoder UnifiedEncoder() dense_index faiss.read_index(index/dense.index) with open(index/inverted.json, r, encodingutf-8) as f: inverted_index json.load(f) with open(index/docs.json, r, encodingutf-8) as f: docs json.load(f) # 转成适合 JSON 存储的形式原代码索引是 int key读取后需要转回 int inverted_index {int(k): v for k, v in inverted_index.items()} # 生成 query 向量 q_enc encoder.encode([query]) q_dense q_enc[dense] q_sparse_indices q_enc[sparse_indices][0] q_sparse_weights q_enc[sparse_weights][0] # 稠密召回 dense_scores, dense_ids dense_index.search(q_dense, top_k * 3) dense_hits [int(i) for i in dense_ids[0]] # 稀疏召回 sparse_score {} for token_id, weight in zip(q_sparse_indices, q_sparse_weights): if token_id in inverted_index: for doc_id, doc_weight in inverted_index[token_id]: sparse_score[doc_id] sparse_score.get(doc_id, 0.0) weight * doc_weight sparse_hits sorted(sparse_score.items(), keylambda x: x[1], reverseTrue) sparse_hits [doc_id for doc_id, _ in sparse_hits[:top_k * 3]] # 融合 fused rrf_fusion(dense_hits, sparse_hits) results [] for doc_id, score in fused[:top_k]: results.append({doc_id: doc_id, text: docs[doc_id], score: round(score, 4)}) return results if __name__ __main__: results search(海边 红裙子 女孩, top_k3) for item in results: print(item)5.5 运行与验证在项目根目录依次执行python index/build_index.py python index/search.py预期输出大致如下具体分数会因为模型、随机初始化和数据而产生差异构建完成共 8 条文档 稠密索引维度: 768 稀疏词表非零 token 数: 47 {doc_id: 0, text: 一个穿红色裙子的女孩站在海边, score: 0.3221} {doc_id: 7, text: 蓝色海洋上空的白色海鸥, score: 0.2114} {doc_id: 3, text: 一只白色猫咪趴在窗台上晒太阳, score: 0.1308}排名第一的结果是“海边红裙子女孩”符合语义预期。稀疏路径也会参与打分比如“海边”和“女孩”等 token 的精确匹配会拉高原始文档的分数。6. 常见问题与排查思路6.1 稀疏向量维度太大内存爆炸问题现象 构建稀疏索引时如果把整个词表大小的向量都转成矩阵内存会迅速上涨。原因 BERT 的词表通常有 3 万左右如果每条文档都保存一个 3 万维的数组8 条文档可能不明显但百万条文档就会非常夸张。解决方案 只保存非零项。上面的代码已经通过sparse_indices和sparse_weights做了压缩。线上工程建议用更高效的数据结构例如scipy.sparse.csr_matrix或者使用支持倒排存储的检索引擎如 Lucene、Elasticsearch。6.2 稀疏检索结果为空问题现象 查询里包含某个词但倒排索引里没有命中任何文档。原因 最常见的原因是token_id跨模型不一致。例如构建索引时使用的编码器和查询时的编码器不是同一个 checkpoint或者 tokenizer 版本差异导致同一个词被切分成不同子词。排查步骤打印 query 和文档的tokenize结果。确认构建索引和检索时使用的是同一个model_name。检查倒排索引 key 是否保存成了字符串导致查询时类型不匹配。上面代码已经用了int(k)转回键类型实际项目中也要注意这一点。6.3 稠密检索效果差语义相关结果没召回原因没有对向量做 L2 归一化余弦相似度和内积混用。池化方式不合理例如直接取[CLS]向量在短文本上可能不够稳定。模型本身不具备领域知识。解决方案 在encode()里做了F.normalize保证 FAISS 的内积检索等价于余弦相似度。如果效果仍然不佳可以考虑换用 Sentence-BERT 或领域微调模型。6.4 混合检索的融合权重难以调优现象alpha参数不知道怎么设置融合后的结果反而不如单路。原因 稀疏分数和稠密分数的尺度不一样直接线性加权很难对齐。建议 优先使用 RRF 排名融合而不是分数融合。RRF 不依赖具体分数值只使用排名鲁棒性更高。如果需要线性加权可以先对两路分数做 min-max 归一化再尝试不同 alpha。7. 最佳实践与工程建议7.1 多模态对齐要趁早如果业务中确实需要图像和文本统一检索不要等到最后才接图像编码器。建议在项目初期就确定多模态编码方案例如选用 CLIP 作为主干然后在它的输出之上再接稀疏头。这样稠密部分天然对齐稀疏部分也更容易共享词汇空间。7.2 索引建议分开构建、统一召回线上系统建议采用以下架构稠密索引使用 FAISS / Milvus / Qdrant 等向量数据库。稀疏索引使用 Elasticsearch / Lucene 倒排索引或者直接保存稀疏向量后用支持稀疏检索的引擎。融合层统一召回后做 RRF 重排。这样做的好处是故障隔离即使稠密索引挂了稀疏索引仍然能提供基础的检索能力。7.3 评估指标要双路跟踪建议在评测阶段分别记录Dense RecallKSparse RecallKHybrid RecallKMRR / NDCG不要只看混合后的效果。如果混合结果提升了但某一路显著下降说明融合权重可能引入了噪声。7.4 长尾 ID 场景优先看重稀疏商品编码、订单号、用户名、车型代号这些精确匹配信息稀疏分支的召回效果明显强于稠密分支。在实际落地时可以按查询类型做路由包含大量数字/大写字母的 query提高稀疏分支的权重。7.5 训练数据与负样本UEmbed 训练最关键的是负样本构造。多模态对比学习需要高质量负样本。建议从三个来源采样Batch 内负样本in-batch negatives。稠密检索得到的“看似相关但实际不相关”的难负样本。稀疏检索召回的相关但语义偏颇的样本。负样本数量不需要太多但难负样本的质量对模型效果提升非常明显。8. 总结与学习路线UEmbed 让“稀疏稠密”不再是非此即彼的选择而是可以在统一的多模态模型中同时生成从而提高检索系统在语义理解和精确匹配两方面的综合表现。读完本文你应该对以下几个问题有了自己的答案稀疏 Embedding 和稠密 Embedding 各自解决什么问题。UEmbed 为什么能在一个模型中“统一”两种表示。如何基于现有 Transformers 模型快速构建一个 UEmbed 风格的推理 Demo。混合检索中常用的融合策略和工程注意事项。接下来可以继续深入的方向包括阅读 SPLADE 原论文理解稀疏表示的学习目标。学习 CLIP 的原理掌握多模态对比学习的基本思路。尝试把 Demo 中的文本编码器替换成 CLIP加入图像分支做真正的图文混合检索。研究 RRF 融合在不同数据分布下的表现寻找更优的融合策略。如果在实现过程中遇到问题建议从打印中间结果开始排查先确认 query 和 document 的 tokenizer 一致再检查稀疏索引命中情况最后看稠密向量的相似度分布。祝你在多模态检索的路上越走越顺。
返回列表