尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

跨模型KV Cache复用:用闭式线性映射跳过Prefill,实现LLM推理加速

跨模型KV Cache复用:用闭式线性映射跳过Prefill,实现LLM推理加速 在多个大模型同时服务同一批用户请求的场景里预填充Prefill阶段算了好几遍相同的 Prompt这是实打实的算力浪费。KV Cache 一直是单模型私有的换一个模型就得全部重算。今天我们要看的方向是在同一个 LLM 家族里用闭式线性映射把一个小模型的 KV Cache 直接转给大模型用从而跳过第二个模型的整段 Prefill。这类技术一旦在推理框架里落地对模型路由、级联推理、多尺寸模型协同服务都有直接帮助。这个方向的核心卖点有三个第一它能跨模型复用 KV Cache而不是传统意义上只在单模型内部复用第二它使用闭式解Closed-Form不需要为每个请求迭代训练映射矩阵是一次性学习出来的第三它的作用域集中在“同一模型家族”内也就是共享 Tokenizer 和相似架构的模型之间比如同家族不同参数规模的 7B、13B、70B 模型。本文会从问题动机、KV Cache 机制、闭式线性映射的原理、可落地的部署流程、性能评估方法和常见坑位这几个方面展开。适合的读者画像比较明确正在做 LLM 推理加速、多模型路由、多租户推理服务或者对 Prefill 阶段算力浪费敏感的工程师。这篇文章不涉及具体某一家的实测曲线因为真正能用的数字必须绑定你的模型版本、输入长度和 GPU 环境我会把整个技术原理和可以照做的验证流程讲清楚。1. 核心能力速览在进入细节之前先给你一张速览表把这项技术的关键维度拉出来。能力项说明技术类型LLM 推理优化 / KV Cache 复用 / 跨模型缓存迁移解决的核心问题多个同家族 LLM 处理相同前缀时重复执行 Prefill 带来的算力浪费关键方法闭式线性映射Closed-Form Linear Mapping数学基础正交 Procrustes 问题通过 SVD 分解求解映射矩阵适用模型范围同一模型家族共享 Tokenizer架构相近的模型主要收益降低第二个模型 Prefill 的延迟减少前缀重复计算额外开销映射矩阵显存/内存占用以及一次线性变换的计算成本是否支持批量可以但对同一前缀的批量复用收益更明显落地门槛需要能同时加载或交替调度两个模型并管理跨模型的 KV Cache合规要求模型权重、转换后的缓存均需遵守对应开源协议与数据授权这里先澄清一个容易混淆的点跨模型 KV Cache 迁移不等于让两个模型共享同一个 Cache 文件。它是在一个小模型完成 Prefill 之后把产生的 KV Cache 经过一个线性变换变成另一个模型可以继续做 Decode 的初始状态。真正的难点在“语义空间对齐”也就是怎么让小模型的隐藏层状态与目标模型的隐藏层状态对应起来闭式线性映射就是用来解决这个对齐问题的。2. 为什么多模型服务场景越来越需要 Cross-Model KV Cache单模型推理时代KV Cache 的优化方向非常明确减少重复计算、降低显存占用、提高吞吐。多模型服务时代出现了一个新的重复计算来源——相同的前缀在多个模型里被反复 Prefill。典型场景有三种。模型路由Model Routing。系统先用一个小模型判断请求类型然后把请求交给大模型做正式回答。小模型已经读完了用户的全部对话历史和 System Prompt大模型又要把这同一段前缀从头 Prefill 一遍这里多出来的计算完全可以通过 KV Cache 复用优化掉。级联推理Cascade Inference。一个请求先给 7B 模型试答如果置信度不足再升级到 13B 或 70B 模型。三个模型都有各自的 KV Cache但前两轮已经算好的前缀历史并没有被第三个模型利用到。多尺寸模型协同服务。为了控制成本同一套对话系统会根据用户等级、问题难度分流到不同规模的模型上。如果用户中途被升级到更大的模型前面所有历史都要重算TTFT 会突然变得很难看。这三个场景有一个共同特点多个模型来自同一个家族。这里的“同一个家族”不是口头上的套近乎而是指共享 Tokenizer、类似的隐藏层宽度比例、一致的注意力头划分逻辑。比如 Llama-2-7B 和 Llama-2-13B 就是这样的一组家族模型。正因为共享了底层语言表征跨模型的 KV Cache 迁移才在数学上变得可行。传统方案怎么处理这些问题要么硬算把所有模型的前缀 Prefill 全部跑一遍要么在路由层做提示词压缩把历史对话简化后再给新模型但压缩会损失信息。Cross-Model KV Cache Transfer 提供的是另一条路保留小模型的推理结果通过数学映射直接进入大模型的 Decode 阶段。3. KV Cache 与 Prefill / Decode 机制回顾要理解跨模型 KV Cache 迁移先要把单模型内部的 KV Cache 机制看清楚。3.1 Prefill 阶段做什么当用户输入一段 Prompt 时模型并不是马上就生成第一个 token而是先把整段 Prompt 的所有 token 并行处理一遍。这个阶段叫 Prefill。在 Prefill 过程中模型会为每个 token 计算 Query、Key、Value 三个向量其中 Key 和 Value 被保存在缓存里供后续 Decode 阶段使用。Prompt 长度越长Prefill 的计算量越大。以 32 层 Transformer 为例一段 2000 token 的 Prompt在每一层都会产生 2000 个 token 的 Key 和 Value。这些缓存就是 KV Cache。3.2 Decode 阶段为什么依赖 KV Cache进入逐 token 生成阶段后模型每步只生成一个新 token。这个新 token 的 Query 需要与历史所有 token 的 Key 做 Attention并加权求和历史所有 token 的 Value。如果没有 KV Cache模型每次都要把历史 token 重新算一遍代价从“算一个 token”退化成“把整段历史重算一遍”。KV Cache 的本质就是拿显存换算力。它把已经算过的历史冻结在缓存里让 Decode 阶段只需要关心当前生成的 token。这也是为什么生成任务越长KV Cache 占用的显存越大。3.3 KV Cache 为什么绑定模型KV Cache 不是独立于模型的一种通用数据。它是由某个模型每一层的权重矩阵计算出来的Key 和 Value 的数值分布依赖模型的注意力头数量、线性投影矩阵以及各层的归一化参数。同一个 token在 Llama-2-7B 和 Llama-2-13B 中计算出来的 Key、Value 完全不在同一个语义空间里。直接拿 7B 模型的 KV Cache 塞给 13B 模型做 Attention结果大概率是乱码。要让这个 Cache 被目标模型接受就必须先做空间变换。这就是 Cross-Model KV Cache Transfer 要解决的映射问题。3.4 同一家族模型为什么存在迁移可能性同一个 LLM 家族的模型通常在以下几个层面高度一致对齐维度说明Tokenizer共享同一套词表token ID 语义一致层数结构层数和隐藏层宽度按比例扩展Attention 结构一致语义空间底层对语言的理解存在可对齐的线性关系训练数据往往共享大规模预训练语料基于这些一致性研究者可以尝试学习一个线性映射把一个家族的“小模型 KV 空间”转换到“大模型 KV 空间”。并不是说 7B 和 13B 的所有层都严格对应而是数学上存在一个合理的低参数变换能完成跨尺寸的语义迁移。4. 核心技术闭式线性映射如何实现 Prefill Reuse接下来是整篇文章的核心。为什么这个方向可以用闭式解而不是训练一个小型神经网络来转换 KV Cache4.1 核心思路整个方案在推理时可以拆成三步小模型读前缀用较小的模型对公共前缀执行 Prefill得到每一层的 KV Cache。线性映射转换通过预先学习好的映射矩阵把小模型的 KV Cache 转换到目标大模型的“语义空间”。大模型直接 Decode目标大模型不再执行 Prefill而是直接用转换后的 KV Cache 作为初始状态开始生成。这里最关键的是第二步。映射矩阵不针对单个请求而是从一组对齐样本中学习得到属于一次训练、长期使用。4.2 为什么选择线性映射选择线性映射有非常现实的工程理由。非线性映射比如小型 MLP 或 Transformer block虽然表达能力更强但存在三个问题训练开销大、推理转换开销高、可解释性差。线性映射的表达能力是否够用在同家族模型之间保留的线性关系往往已经足够。Llama-2-7B 和 Llama-2-13B 在相同输入下产生的语义表征在很多情况下近似满足线性变换关系。这给闭式解提供了基础。4.3 闭式解怎么求正交 Procrustes 问题闭式线性映射的数学本质是正交 Procrustes 问题的变体。简化后的目标函数如下。给定对齐样本矩阵X小模型在某个阶段输出的特征矩阵形状为 (样本数, 特征维度)Y大模型在对应阶段输出的特征矩阵形状为 (样本数, 特征维度)我们需要找一个线性映射矩阵 R使得 X 经过变换后尽可能接近 Yminimize || X R - Y ||_F^2 subject to R^T R I其中 || * ||_F 表示 Frobenius 范数约束条件要求 R 是正交矩阵。这个约束非常重要因为正交变换保持向量范数和内积结构不会在转换过程中把数值分布拉歪。这个问题的闭式解是令 M X^T Y 对 M 做 SVD 分解M U Σ V^T 则 R U V^T也就是说只需求解一次 SVD就能得到最优的正交线性映射不需要梯度下降也不需要多轮迭代训练。这个性质让它非常适合在推理基础设施里落地。4.4 实际要映射的是什么严格意义上来讲KV Cache 由多个注意力头各自的 Key 和 Value 组成。不同模型的注意力头数量可能不同直接对所有头做矩阵映射不一定是最优的。在具体实现中通常会选择映射带有全局语义信息的中间特征再通过目标模型自身的投影矩阵生成 KV Cache。这一步需要结合具体模型结构设计不是简单把任意两层的 K、V 拿来做 SVD 就能得到最好的效果。一个可选的做法是在小模型中选择若干层提取处理完前缀后的隐藏层状态。在大模型的对应层以相同前缀的隐藏层状态作为监督目标。对这些成对样本求解闭式映射矩阵。部署时先让小模型执行 Prefill取对应层的隐藏状态做线性映射再用目标模型的注意力投影矩阵换算成 Key、Value 进入 Decode。这样做的好处是映射对象是“更高层、更语义化”的表征而不是直接映射 Multi-Head Attention 内部每个头的 K、V工程上更容易对齐不同头数的模型。5. 从训练到部署一套可参考的落地流程下面给出一套不依赖具体框架的落地流程帮助你在自己的推理服务里复现这个方向。这里的代码是参考实现实际环境需要按你的模型结构做调整。5.1 构造对齐数据你需要一批公共前缀文本分别用小模型和大模型执行 Prefill保存对应层的输出。# 参考伪代码实际请替换为对应推理框架的接口 from small_model import SmallModel from large_model import LargeModel small_model SmallModel.load(family-7b) large_model LargeModel.load(family-13b) prefix_texts [ 你是一个智能助手请帮助用户解决问题。\n, 以下是系统提示词和用户历史对话记录。\n, # 尽量覆盖真实服务的前缀分布 ] small_states [] large_states [] for text in prefix_texts: tokens tokenizer.encode(text, return_tensorspt) s_state small_model.extract_layer_state(tokens, layer20) l_state large_model.extract_layer_state(tokens, layer20) small_states.append(s_state) large_states.append(l_state)收集到的样本对越多、覆盖的 Prompt 分布越接近线上真实场景映射矩阵的质量越高。这里有个容易被忽略的问题映射矩阵应该针对线上真实前缀分布训练而不是随机文本。5.2 求解闭式线性映射矩阵import torch def compute_linear_mapping(X, Y): X: 小模型特征矩阵形状 (N, D) Y: 大模型特征矩阵形状 (N, D) 返回: 正交映射矩阵 R形状 (D, D) X X.float() Y Y.float() # 中心化提升数值稳定性 X_mean X.mean(dim0, keepdimTrue) Y_mean Y.mean(dim0, keepdimTrue) X_centered X - X_mean Y_centered Y - Y_mean # 闭式解对 X^T Y 做 SVD M X_centered.T Y_centered U, _, Vt torch.linalg.svd(M) R U Vt return R, X_mean, Y_mean # 使用示例 R, X_mean, Y_mean compute_linear_mapping(small_states, large_states) # 训练集上验证转换效果 mapped_states (small_states - X_mean) R Y_mean similarity torch.cosine_similarity(mapped_states, large_states, dim-1).mean() print(f平均余弦相似度: {similarity.item():.4f})训练集上的余弦相似度是一个重要的参考指标。如果这个数值很低说明该层不适合做直接映射你可以尝试其他层或者换成 K、V 输出层做映射。5.3 推理时替换 Prefill推理服务的整体流程可以这样设计。def generate_with_cross_model_cache(request, small_model, large_model, mapping): # 1. 小模型处理公共前缀 prefix request.common_prefix prefix_tokens tokenizer.encode(prefix) # 用小模型走一遍 Prefill small_kv_cache small_model.prefill(prefix_tokens) # 2. 取出小模型指定层状态做线性映射 source_state small_model.get_layer_state(prefix_tokens, layer20) mapped_state (source_state - mapping.X_mean) mapping.R mapping.Y_mean # 3. 用目标模型的投影把映射后的状态转成初始 KV Cache large_initial_kv large_model.project_to_kv(mapped_state) # 4. 大模型跳过 Prefill直接 Decode 新 token output large_model.decode( request.new_tokens, initial_kv_cachelarge_initial_kv ) return output这段参考代码本质上是把“映射”和“KV Cache 生成”解耦了。实际工程中第 2 步和第 3 步可以合并进大模型的注意力模块里避免一次性把所有层的映射矩阵都加载到显存里而是按层逐步转换。5.4 一次训练、多处复用映射矩阵只需要训练一次。只要模型权重不变、家族架构不变这个矩阵可以持续使用。你可以把 R、X_mean、Y_mean 保存为一个权重文件在每次启动推理服务时提前加载。{ source_model: family-7b, target_model: family-13b, mapped_layer: 20, mapping_matrix_shape: [4096, 4096], precision: fp16, creation_note: 该矩阵仅在对应模型版本下有效 }这里要特别提醒映射矩阵绑定的是具体模型版本。如果小模型或者大模型发布了新版本哪怕只改了几层 MLP 结构旧的映射矩阵也必须重新计算。不要尝试跨家族复用映射矩阵Llama 家族学到的映射放到 Qwen 家族上数字上可能跑得通但生成质量大概率不可控。6. 接口 API 与批量任务视角这个方向虽然是一种推理优化技术但它同样可以用“接口能力”的视角来审视。推理框架如果要支持跨模型 KV Cache 复用可以考虑暴露以下三类接口。6.1 Prefill 服务接口小模型或共享前缀模块作为一个独立服务对外提供“前缀 KV Cache”产出能力。# 参考接口设计 POST /v1/prefix/prefill Content-Type: application/json { prefix: 你是一个智能助手以下是历史对话记录……, target_model: family-13b }返回内容是一个缓存引用 ID而不是 KV Cache 本身。这样做的好处是显存不经过网络传输缓存留在推理节点的显存里后续 Decode 请求只需要带上这个 ID 就能找到对应的 Cache。6.2 Cache 映射与转换服务当小模型的 KV Cache 需要转给大模型时调用映射转换服务。{ cache_id: prefix-abc-123, source_model: family-7b, target_model: family-13b, mapping_layer: 20 }转换服务内部加载对应的线性映射矩阵逐层完成 KV Cache 转换并把转换后的缓存注册到目标模型的缓存池中。这个过程应当有严格的超时控制和错误处理如果缓存转换失败应该回退到普通 Prefill 流程而不是返回一个空缓存。6.3 批量 Prefix 复用池多用户场景下相同 System Prompt 和相同 few-shot 示例会被大量小模型重复 Prefill。可以把这些公共前缀的 Prefill 结果做成一个缓存池按前缀 hash 做索引命中后直接走映射转换。# 示例缓存池查询逻辑 cache_pool {} def get_or_create_mapped_cache(prefix_text, small_model, large_model, mapping): prefix_hash hash(prefix_text) if prefix_hash in cache_pool: return cache_pool[prefix_hash] # 未命中 执行小模型 Prefill 线性映射 source_kv small_model.prefill(prefix_text) target_kv apply_mapping(source_kv, mapping) cache_pool[prefix_hash] target_kv return target_kv批量任务的收益在这里体现得最明显。如果一批请求共用同一个 System Prompt改造前每个模型都要将 System Prompt Prefill 一次改造后小模型只算一次大模型所有请求都通过缓存直接进入 Decode。6.4 回退与降级策略任何缓存技术在真实环境里都要有降级路径。推荐的做法是转换动作异步执行Decode 请求同步等待如果转换超时立即回退到全量 Prefill。不要为了让引入新功能而牺牲用户请求的成功率。7. 性能收益怎么评估关键指标与实验设计判断 Cross-Model KV Cache Transfer 是否值得引入不能只看论文里的曲线要在自己的推理服务上建立一套标准评估流程。7.1 需要观察的核心指标指标说明观测方式TTFT首 token 返回时间记录 Prefill 开始到第一个 token 生成的时间Prefill 延迟前缀处理耗时单独打点记录 Prefill 阶段耗时KV Cache 显存缓存占用的显存大小通过 nvidia-smi 或推理框架显存统计接口观察映射转换耗时线性映射本身的延迟单独统计映射矩阵计算耗时输出质量生成结果是否漂移用评测集做转写一致性、内容相似度校验命中率前缀缓存命中比例统计缓存池命中次数与总请求数之比7.2 一套可复现的实验设计建议做三组对照实验。对照组 A不做任何复用。大模型直接 Prefill 完整 Prompt记录 TTFT。对照组 B只做单模型 KV Cache 复用。小模型和大模型各自维护自己的 KV Cache 池但跨模型之间不共享。实验组 C启用跨模型映射。小模型 Prefill 后通过线性映射生成大模型的 KV Cache记录 TTFT、转换耗时的额外开销和生成质量。输入长度建议做分级512 token、1024 token、2048 token、4096 token。前缀越长小模型 Prefill 节省的时间在总延迟中的占比越大跨模型映射的绝对收益也就越明显。7.3 容易看到的数值特征在映射质量稳定、前缀较长的理想情况下通常可以观察到以下特征大模型 TTFT 中原本占比很高的 Prefill 耗时被显著压缩成一次线性映射的耗时。小模型 Prefill 耗时与大模型 Prefill 耗时差异越大收益越大。如果小模型本身就是同尺寸的蒸馏模型收益会变小因为 Prefill 成本基本一样。映射转换耗时会随着前缀长度线性增长因为转换矩阵作用在每个 token 的特征上。前缀越长映射耗时越高。显存占用会额外增加一份映射矩阵。一个 4096 维的 fp16 方阵大约是 32MB对整体显存影响非常小可以忽略。这些数字需要以你的实际环境为准但理解趋势比记住具体数字更重要。跨模型 KV Cache 迁移最大的收益区间是“小模型 大模型 长前缀 高命中率”的组合。8. 挑战、局限与合规边界这个方向仍然有非常明确的限制不能无脑引入。8.1 模型家族的硬性约束映射建立在同一个模型家族之上。不同家族之间的 tokenizer、隐藏层结构、注意力机制差异过大线性映射的精度会明显下降。如果强行跨家族复用最好在前置评估阶段跑一轮质量测试确认生成结果没有明显劣化再上线。8.2 层粒度对齐的精度问题不同尺寸模型在层数上并不是简单的一一对应。7B 模型 32 层13B 模型 40 层你选哪一层做映射映射后又如何让目标模型按自己的层结构正确生成 KV Cache都需要针对具体模型做消融实验。层选择不当最直接的表现是生成结果语义漂移或者长文生成不稳定。8.3 精度对映射效果的影响KV Cache 在推理中通常使用 fp16 或 bf16 存储。线性映射矩阵的精度越低转换过程中的数值误差越大。实际落地时要注意训练映射矩阵时用 fp32 提升数值稳定性。推理转换时可以用 fp16/bf16但要观察目标模型输出是否出现异常。如果映射矩阵在 fp16 下精度损失明显可以尝试把矩阵分块高敏感维度保留 fp32。8.4 合规与安全使用跨模型映射本质上是基于两个模型权重的二次派生操作。需要注意几点模型权重的开源协议允许这类派生映射吗如果模型只允许商用不允许修改映射矩阵是否被视为修改需要咨询法务。小模型产出的 KV Cache 可能包含用户对话历史缓存池的持久化和跨进程传递必须做权限隔离。涉及私有数据的 Prefill不能因为“只是一个向量缓存”就放松数据安全要求。KV Cache 依然能从生成结果反推出输入语义必须按输入数据同等安全级别管理。9. 常见问题与排查方法问题现象可能原因排查方式解决方案映射后生成结果严重偏离预期映射层级选择不当或映射矩阵训练数据不覆盖当前前缀分布对比映射前后同前缀输出的余弦相似度换映射层或用更接近线上分布的数据重训映射矩阵大模型 Prefill 没被跳过推理框架中的 KV Cache 初始化逻辑未接入映射结果检查大模型 Prefill 阶段是否仍然执行了完整 Prompt 计算在框架层把映射后的缓存作为初始 KV Cache设置跳过 Prefill 的开关映射转换耗时过高映射矩阵维度大逐 token 转换开销高统计单 token 映射耗时合并线性变换到注意力投影层减少中间张量搬运显存出现额外峰值小模型的 KV Cache 与映射后的大模型 KV Cache 同时驻留显存观察 nvidia-smi 显存曲线小模型 Prefill 完成后及时释放源缓存用 CPU 或共享内存暂存映射矩阵批量请求命中率低缓存池索引设计不合理或前缀包含时间戳等动态内容统计缓存池命中率拆分稳定前缀与动态变量只缓存稳定前缀部分映射后长文生成不稳定映射矩阵只在 Prefix 层做了对齐但 Decode 阶段新 token 与映射后的 KV Cache 没有良好对齐观察不同层 Attention 输出是否异常增加验证集覆盖长上下文生成场景或对特定层做精细映射调整精度从 fp32 换成 fp16 后效果变差线性映射矩阵对高维向量中的小数值敏感对比 fp32 与 fp16 下的映射误差分块存储映射矩阵敏感块保留 fp3210. 最佳实践与使用建议结合多模型服务和 KV Cache 优化项目的通用经验给出几条工程化建议。第一先把前缀命中率跑出来再上跨模型映射。如果线上请求的前缀重复率极低那么这套方案的收益会非常有限。先做前缀统计用数据判断值不值得投入。第二缓存池要区分稳定的公共前缀和动态用户历史。System Prompt、few-shot 示例这些可以长期缓存用户历史一旦变化缓存就需要废弃。不要试图把整段对话都塞进缓存池。第三映射矩阵要纳入版本管理。它和模型权重一样是决定生成质量的关键资产。映射矩阵和模型权重配套记录版本号和创建时间每次模型升级后重新生成。第四建立完整的回退链路。哪怕映射转换完全失败也要保证请求能回退到传统 Prefill 流程。建议在接口层加入熔断机制映射失败比例超过阈值时自动关闭跨模型复用。第五评估不能只看 TTFT。跨模型映射可能引入生成质量的细微漂移。建议在每次引入新映射矩阵时跑一份固定测试集对比原始模型直接生成的输出记录相似度变化趋势。如果漂移范围在可接受区间内再放量上线。第六注意显存峰值管理。小模型 Prefill 产生的 KV Cache 和映射后的大模型 KV Cache 不要同时常驻显存。映射完成后立刻释放源缓存的显存尤其在 8GB、12GB 这类显存相对紧张的环境上这个操作能明显降低 OOM 概率。11. 总结与下一步这个方向真正值得关注的不是“KV Cache 能跨模型用了”这个结论而是它提供了一条低成本实现 Prefill Reuse 的技术路径。闭式线性映射不需要复杂训练不要求引入额外的重型模型只要在同一模型家族内找对应层、学一个正交矩阵就能在推理时跳过大模型的整段 Prefill。对正在建设多模型路由、级联推理和统一推理网关的团队来说这是一个值得写进技术预案的优化项。最先要验证的永远是同一个问题我的线上请求到底有多少公共前缀可以被复用。这部分数据决定了后续所有优化的天花板。最容易踩的坑是映射层选错导致生成质量劣化还不自知。建议在正式改框架之前先用离线方式统计小模型与大模型在同前缀上的隐藏层相似度再决定是映射 MLP 输出、映射 K/V 输出还是按层分段做混合映射。后续可以继续扩展的方向包括把映射矩阵从固定版本升级为按领域自适应在批量推理框架中把前缀缓存池与调度器打通以及研究同一家族内多尺寸模型之间的多级映射链让 7B 的缓存可以被 13B、30B、70B 逐级复用。这个方向如果发展成熟多模型推理的整体 Prefill 成本有机会下降到接近单模型的水平。
返回列表