资讯动态

推荐系统双塔召回:结构原理与工程实践全解析

发布时间:2026/10/27 11:55:09 来源:尧图企业网站定制
1. 为什么要单独聊召回以及双塔为什么能成主流先说个可能是很多刚入行的人会忽略的事实决定推荐系统体验上限的往往不是精排模型而是召回。我见过不少团队花几个月调精排模型AUC涨了0.003线上指标却纹丝不动。后来查了半天问题出在召回侧——真正用户喜欢的item压根没进候选集精排再怎么排也是巧妇难为无米之炊。召回承担的任务是从千万甚至亿级的item池里快速捞出几百到几千个用户可能感兴趣的候选。它有两个硬约束第一是速度要够快精排可以对几百个item做深度交叉特征计算但召回要对全量item做一遍初筛这个量级下任何复杂的特征交互都扛不住第二是召回率要够高把用户真实感兴趣的item尽量捞进来否则下游全是白搭。双塔召回模型Two-Tower Retrieval Model能成为这个环节的主流方案核心就一句话用向量检索代替规则匹配用内积代替复杂模型推理。它把用户和item分别映射到同一个向量空间里。线上服务的时候item向量全部预先算好存进向量索引用户请求来了只算一次user塔得到用户向量然后去索引里做ANN近似最近邻搜索取topK个向量对应的item。整个过程两个塔各算各的互不依赖这就把“全量item过一遍模型”的代价变成了“一次向量检索”的代价计算复杂度直接降了几个数量级。双塔不是唯一能做召回的方案像Youtube DNN、FM类模型也能做但双塔因为结构干净、训练稳定、上线部署友好逐渐成了工业界默认的召回底座。后面几节我会从结构、样本、训练、调优到落地把整个链路拆开讲清楚。2. 双塔结构拆解两个塔各自学什么以及它们怎么“握手”2.1 结构总览User塔、Item塔和交互层先上一张结构图给大家一个整体感。严格说双塔结构分三块User塔输入用户侧特征包括用户ID、历史行为序列、人口属性、上下文信息等经过几层全连接网络输出一个固定维度的向量称为user embedding。Item塔输入Item侧特征包括Item ID、类目、价格、品牌等同样经过几层全连接网络输出与user向量同维度的item embedding。交互层在线阶段用内积计算user与item的相似度得分离线训练时通常用负采样构造样本对训练目标是让正样本的交互得分高于负样本。两个塔最核心的设计动机是什么是解耦。因为item塔可以离线把全量item全部算成向量存起来所以线上只需要算user塔。你想想如果这个模型不是双塔而是一个把user特征和item特征拼接起来过同一个网络的联合模型在线推理时每个候选item都要重新算一遍完整网络那个计算量根本扛不住亿级item的实时检索。2.2 两个塔在结构上要不要完全对称实践中很多人会默认两个塔结构一模一样其实没必要。User侧特征通常更多、维度更宽Item侧如果特征相对简单塔可以浅一些。我自己常用的配置是侧输入特征Embedding维度隐藏层User塔user_id、行为序列、年龄、性别、地域32~64[256, 128, 64]Item塔item_id、类目、品牌、价格分桶32~64[128, 64, 64]隐藏层设计的原则是最终输出维度必须一致这样才能算内积中间的宽度和深度可以不一样。有一个细节值得注意如果某一侧特征特别稀疏比如新item几乎没有行为数据那这个塔的embedding会学得很差需要靠类目、品牌这类泛化特征来兜底。2.3 交互函数为什么选内积而不是MLP如果不考虑性能完全可以让两个向量拼起来再过一层MLP计算得分这会让模型表达能力更强。但双塔模型在线上要把item向量提前存库内积满足一个非常重要的性质——它的计算结果可以在离线阶段就确定下来不依赖user侧的任何信息。只要user向量出来了内积就是一个常数时间的计算。而一旦交互函数变成了MLP传输到向量检索阶段时打分函数本身就成了一个复杂模型这时只能做暴力全量计算无法用基于向量的ANN索引做近似检索。内积配合MIPS最大内积搜索在工程上非常成熟这才是双塔选择内积的根本原因。还有一个实际细节内积对向量的模长很敏感如果某个item特征的embedding向量模长特别大它的得分会被天然抬高这可能带来热门item的过度曝光。这个问题的对策后面单独讲。3. 训练样本构造双塔模型效果的命门很多人觉得双塔模型结构简单难不到哪去。结构确实简单但真正决定效果的是样本。双塔召回本质是个“判别式”模型——它靠区分正样本和负样本来学习向量表征。如果样本构造不好模型学到的就不是“相似度”而是“bias”甚至是“随机噪声”。3.1 正样本怎么选常用正样本有三类曝光未点击、点击、转化下单/购买/加购。点击样本是最常用的量大、信号明确但缺点是点击不完全等于喜欢标题党也会被点。转化样本信号强但稀疏直接拿它做主训练样本会导致数据量不够模型学不饱。曝光未点击是很多团队忽略的“免费负样本”它代表了“推荐了但用户没兴趣”对抑制过度曝光有用。实践中的折中方案以点击为主正样本用转化做加权曝光未点击作为一部分负样本参与训练。加权怎么做可以简单地对转化样本提高loss权重比如点击样本权重1转化样本权重3~5。我踩过的坑是直接用曝光日志里的点击样本当正样本结果模型学出来的向量被“用户看过但没点”的大量平庸item主导因为用户行为日志里曝光大多是系统推荐的本身就存在position bias。后来在训练前加了采样修正效果才正常。3.2 负样本的几种构造方式与采样比例负样本是双塔里最容易影响效果的一环而它恰恰是论文里讲得最少、实际工程里坑最多的地方。负样本来源常规几种全局随机负采样从全量item池里随机抽。优点是简单无偏缺点是随机负样本和真实正样本区分度太大导致决策边界太“松”学出来的向量区分不开难分样本。Batch内随机负采样当前batch内其他样本的正样本作为当前样本的负样本。FB的经典做法省去了额外采样成本但有一个问题——热门item出现在batch里的概率更高导致它们被当作负样本的次数更多会造成对热门item的“过度打压”。曝光未点击负样本推荐了但没点直接拿来当负样本。这个偏置在于曝光本身不是随机产生的而是系统推荐策略决定的直接用会学入position bias。混合负样本随机负样本曝光未点击样本难负样本按比例混合。工程上混合负样本几乎是标配。我自己常用的比例大约是随机负样本曝光未点击负样本难负样本 5:3:2。难负样本可以来自同品类但没点击的item或者上一版召回模型拿出来的topK中未点击的item。负样本数量控制上一般正负比例在1:100到1:1000之间是有意义的太少模型学不到什么太多训练时间暴涨且收益衰减。一个直观的类比是你让一个人从几百个鱼目和两颗珍珠里挑珍珠他学得快但你让他从一百万颗鱼目里挑一颗珍珠他可能直接摆烂最后看到什么都像鱼目。3.3 Sampling Bias与温度系数的关系这里插入一个重要的知识点批量负采样有一个理论问题batch内负样本的采样概率正比于该item在batch中出现的频率频率高的item会被更频繁地当作负样本这种bias会让模型对热门item的向量进行过度惩罚最终热门item的embedding模长被压缩长尾item的模长则偏大。解决方式是给内积打分加一个温度系数score(u, v) u, v / tautau越大打分分布越平滑对不同负样本的区分越不敏感可以缓解batch内负采样带来的bias。论文里建议tau的取值在0.05~0.2之间但实际需要根据batch大小和负样本比例调。我个人的经验法则是batch越大或负样本比例越高tau适当调小让训练对难负样本更敏感反之tau调大避免梯度消失。4. 从零搭建一个可用的双塔召回完整代码与训练流程这一节直接给一套能跑的代码基于TensorFlow实现一个Mini双塔召回让没上过手的人建立一个完整闭环。数据集用MovieLens 100K就够了重点是观察整个训练流程。4.1 数据准备与特征构造import pandas as pd import numpy as np from tensorflow.keras import layers, Model import tensorflow as tf # 加载数据 ratings pd.read_csv(ml-100k/u.data, sep\t, names[user_id, item_id, rating, timestamp]) # 只看有交互行为的样本作为正样本 ratings ratings[ratings[rating] 4] # 构造id映射表 user_ids ratings[user_id].unique() item_ids ratings[item_id].unique() user2idx {u: i for i, u in enumerate(user_ids)} item2idx {i: j for j, i in enumerate(item_ids)} n_users len(user_ids) n_items len(item_ids) print(f用户数: {n_users}, 物品数: {n_items})MovieLens数据比较简单只有u-i对没有很多side info用起来正好可以聚焦在双塔核心逻辑上。4.2 双塔模型定义class TwoTowerModel(Model): def __init__(self, n_users, n_items, dim32): super().__init__() self.user_emb layers.Embedding(n_users, dim) self.item_emb layers.Embedding(n_items, dim) # user塔 self.user_mlp tf.keras.Sequential([ layers.Dense(64, activationrelu), layers.Dense(32, activationrelu), layers.Dense(dim, activationNone) ]) # item塔 self.item_mlp tf.keras.Sequential([ layers.Dense(64, activationrelu), layers.Dense(dim, activationNone) ]) def call(self, user_ids, item_ids): # 取embedding u_emb self.user_emb(user_ids) i_emb self.item_emb(item_ids) # 过各自塔 u_vec self.user_mlp(u_emb) i_vec self.item_mlp(i_emb) return u_vec, i_vec def predict_score(self, user_ids, item_ids): u_vec, i_vec self.call(user_ids, item_ids) return tf.reduce_sum(u_vec * i_vec, axis-1)这里有个容易踩的细节是最后输出的向量维度dim不一定等于embedding维度。用户原始的ID embedding可以是64维经过MLP最后输出32维向量这两个不是一回事。最终两个塔的向量维度必须一致但中间过程完全自由。4.3 负采样与训练循环训练时构造正样本对和负样本对正样本是用户真实交互过的item负样本是随机抽的不相关item。def sample_batch(df, batch_size256): rows df.sample(batch_size) user_idx rows[user_id].map(user2idx).values item_idx rows[item_id].map(item2idx).values # 为每个正样本采样5个负样本 neg_item_idx np.random.randint(0, n_items, size(batch_size, 5)) return user_idx, item_idx, neg_item_idx tf.function def train_step(model, optimizer, u_idx, i_idx, neg_idx): with tf.GradientTape() as tape: u_vec, i_vec model(u_idx, i_idx) # 正样本得分 pos_score tf.reduce_sum(u_vec * i_vec, axis-1) # 负样本得分user向量和每个负item向量内积 _, neg_vec model(u_idx, tf.reshape(neg_idx, [-1])) neg_vec tf.reshape(neg_vec, [tf.shape(u_idx)[0], -1, tf.shape(i_vec)[-1]]) u_expand tf.expand_dims(u_vec, 1) neg_scores tf.reduce_sum(u_expand * neg_vec, axis-1) # hinge loss: 正确分的差要大于margin loss tf.reduce_mean(tf.maximum(0.0, 1.0 - pos_score tf.reduce_max(neg_scores, axis-1))) grads tape.gradient(loss, model.trainable_variables) optimizer.apply_gradients(zip(grads, model.trainable_variables)) return loss model TwoTowerModel(n_users, n_items, dim32) optimizer tf.keras.optimizers.Adam(learning_rate1e-3) for epoch in range(10): total_loss 0 for _ in range(1000): u_idx, i_idx, neg_idx sample_batch(ratings) loss train_step(model, optimizer, u_idx, i_idx, neg_idx) total_loss float(loss) print(fepoch {epoch}, loss: {total_loss / 1000:.4f})这个Mini版本没有做batch内负采样用的是纯随机负采样是为了演示方便。实际工程中建议用batch内负采样随机负样本混合既要效率也要质量。4.4 线上部署时向量索引的接入方式模型训练完后两个塔的权重需要分开导出。Item塔用来离线推理全部item的向量User塔留在线上服务实时计算。导出item向量# 全量item过item塔 all_item_ids np.arange(n_items) _, item_vecs model(user_idsnp.zeros(n_items, dtypenp.int32), item_idsall_item_ids) # item_vecs就是全量item的向量存到向量数据库或ANN索引线上流程是User塔实时算用户向量比如用一个用户ID 上下文特征得到u_vec。拿u_vec去ANN索引如Faiss、Milvus里检索topK个最相似的item embedding。把这topK个item作为候选集传给精排。有一次我陪一个朋友调线上问题发现item向量没归一化线上全量item打分普遍偏高回想起来其实就是归一化问题——不归一化时向量模长差异会直接影响内积排序结果。5. 工业落地必踩的坑在线一致性、向量对齐与冷启动5.1 训练-服务不一致问题模型特征必须完全同源双塔最容易翻车的地方不在模型结构而在线上特征和离线训练特征不一致。比如离线训练时user的历史行为序列是完整序列但线上由于链路延迟序列只更新到某一个时间点这就造成了训练分布和在线分布偏移模型在线表现会显著下降。解决方式是训练样本构造时做“特征时间截断”即保证离线特征只使用预测时刻之前的信息。这个需要一套特征日志系统配合保证训练时的特征取的是某个时间点的“快照”而不是事后补全。另一个常见问题是特征编码方式不一致。比如item的类目ID离线是用全局映射表上线时如果映射表没同步线上embedding查表就会查出一个错的向量。这类问题排查起来非常痛苦我的建议是在模型导出前加一个特征检索的回归测试直接用一批真实线上请求回放比大小更对齐。5.2 向量归一化和模长问题前面提到过内积对向量模长敏感。如果对向量不做归一化热门item的向量模长通常偏大因为训练时正样本多梯度更新次数多会导致它们在检索排序中天然靠前——即使它们和当前用户并不那么相关。其他团队的解法是训练时对两塔输出做L2归一化让内积等价于余弦相似度。好处是分数域稳定不受模长影响坏处是引入了非线性。我的经验是如果你是用训练好的向量直接做内积检索建议在训练时就加L2归一化如果你后续还要接一个校准层比如一个简单的LR根据得分再做一次微调那可以不加归一化保留模长信息让校准层自己学。两种方案都有团队在用关键是一致性。5.3 冷启动问题新用户和新item该怎么办冷启动是召回模型最无力的环节。新用户没有任何行为历史user塔输入只有ID和少量静态特征学出来的用户向量基本是一个“平均用户”向量召回结果完全靠item侧热门度在撑着。新item则连item embedding都没训练充分。我常用的兜底策略新item用基于类目/品牌相似的已有item向量做平均初始化。新用户直接用热门item召回作为“探索候选”同时用一个单独的规则/树策略给新用户推冷门但高质量的item。等积累了几次行为后再切换为双塔向量召回。这个思路的本质是“向量召回负责承接已有兴趣规则/统计策略负责探索未知兴趣”两者不是替代关系而是互补关系。5.4 评估指标怎么定双塔召回在线下最直接的指标是RecallK用户真实交互的item是否出现在召回的topK里。我建议训练和评测时都使用这个指标不要用Loss下降来推断线上表现。还有一个容易被忽视的评估维度召回的多样性。如果模型只记住了热门itemRecallK可能不低但用户很快会审美疲劳。所以我在评估时通常加上一个类目熵的指标——topK结果中类目分布的熵值越大说明多样性越好。6. 双塔的进阶优化多向量、Side Info融合与自监督6.1 多向量双塔一个用户多个embedding标准双塔是“一塔一向量”但用户的兴趣往往是多维度的有人既喜欢科技数码又喜欢美食烹饪一个向量很难同时表达这两种差异巨大的兴趣。直观解法是多向量双塔让user塔输出多个向量每个向量对应一个兴趣簇item塔输出的item向量分别跟它们做内积取最大得分。实现时每个向量可以对应不同的正样本簇比如按item类目分簇或者用聚类算法把用户历史行为分成几簇然后为每个簇学一个向量。多向量的代价是存储量翻倍检索时也要做多次查询所以实践中一般是2~4个向量再多就得不偿失了。6.2 加入Side Information的必要性纯ID特征的双塔在数据稀疏时非常吃亏。一个只出现过几次的新itemID embedding基本是个随机向量召回效果约等于瞎猜。这时side info就显得很重要。加入类目、品牌、价格、标题文本的embedding之后新item即使ID向量没学好类目和品牌向量也能撑起一部分召回质量。这相当于把一个“冷启动问题”降维成了一个“泛化学习问题”。文本信息怎么做可以用一个简单的TextCNN或者直接用预训练语言模型产出句向量然后把句向量作为item塔的一个输入特征。6.3 自监督信号在双塔中的应用很多团队在正负样本之外还会引入自监督信号来增强表征学习。一个常用的思路是对item做对比学习对同一item做两次不同的数据增强比如随机mask掉部分类目/价格特征让模型学到的向量对噪声有鲁棒性。这个增强的具体做法可以是对item特征随机dropout一部分对文本特征做随机的词mask对价格做随机扰动加一个小噪声。然后构造对比学习loss拉近同一item的两个增强版本的向量推远不同item的向量。这可以在主任务之外额外注入表征能力对长尾item尤其有帮助。7. 深入分析双塔能表达什么关系以及它不能表达什么关系7.1 双塔学到的其实是“独立关系的组合”双塔结构的表达力上限是显而易见的user和item的交互只有一次内积。这意味着它能够拟合的交互模式本质上都是可以分解成“user向量各维度”和“item向量各维度”内积之和的关系。换句话说双塔能学会的是“如果用户偏好A且item具备属性A则得分高”这一类可分解关系。它很难直接表达类似“用户喜欢科技但只在评测类内容里喜欢”这种需要条件和上下文交互的复杂关系——因为那需要user的一个特征和item的一个特征做交叉。举一个具体例子一个用户整体不太喜欢短视频但非常喜欢某个创作者做的科普类短视频。双塔模型可能很难同时拟合“整体不喜欢”和“对特定作者的内容喜欢”这两个矛盾信号只能取一个折中的embedding。7.2 双塔模型的“欠拟合”是否可控所以如果有人跟你说“用双塔替代精排”基本可以判断他对推荐系统的理解还停留在表层。双塔的定位是召回用可分解的关系快速过滤候选集把那些明显的相关item捞出来。精排负责做更精细的关系建模它的输入是双塔召回结果的子集可以上更重的交叉特征。这个“分工”非常有价值——双塔牺牲一点表达力换取了无法替代的性能优势。没有双塔精排面对千万级候选时根本没法在线推理没有精排的深度特征交互双塔只能给出粗粒度的相关度。两者是流水线上下游的关系而不是互斥的替代关系。7.3 什么时候双塔可能不是最优选择双塔虽然主流但在以下场景里可以考虑换成别的方案如果item池很小比如几十万级别可以用更重的联合模型做全量离线打分在线近邻检索的混合方案。如果召回结果的排序直接影响收入且item池能承受在线全量计算可以牺牲延迟换取更强的交互建模。如果用户行为极其稀疏双塔的训练信号不足可能需要先靠规则、热度、类目召回积累足够数据之后再上向量召回。我自己的原则是不要因为“大家都在用”就无脑上双塔先想清楚当前业务阶段真正卡脖子的是什么。冷启动阶段的花生米——热度召回和规则召回反而更管用有了稳定行为数据之后双塔才是放大器。8. 线上调优实战从“能跑”到“比之前好”的完整链路8.1 先做模型还是先做样本质量很多人拿到一个新业务第一反应是“我要训练一个双塔”。这通常是错误的第一步。正确顺序是先把数据链路行为日志、特征管道、样本生成理顺再谈双塔。样本数据质量差的模型训练完线上还跑不过一个热门规则召回。这个“先数据后模型”的次序我是花了几个月踩坑校正过来的。具体判断数据链路是否就绪的方法很简单随便拿一个item看它的正样本数量分布。如果90%的item每个月只有一两次交互那不管模型结构多花哨也不可能学到有意义的向量。这时候先调整曝光策略比如给长尾item更多探索流量等数据分布不再极端偏斜再训练双塔才有意义。8.2 调参优先级排序我自己的调参顺序已经固定为优先级调整项说明1负样本构成与采样比例影响最大通常能带来10%~30%的Recall提升2温度系数tau配合负采样bias处理难易样本平衡3特征侧增强side info改善冷启动和泛化4塔结构宽度深度数据量没到一定量级加深塔不如加特征5向量维度64维和128维差异不大除非有知识蒸馏需求负样本组合策略我建议用一个小的离线实验矩阵去筛而不是拍脑袋定。做一个2x2x2的简单网格负样本来源纯随机 vs 混合、比例1:100 vs 1:500、tau0.1 vs 0.2用Recall100做评估指标一次实验跑完基本能确定方向。8.3 线上AB实验的观察窗口和建议指标双塔模型上线后的AB实验观察周期不要太短。召回链路改动的影响是滞后的因为用户对推荐的反馈需要几天才能传导到行为日志里新的行为数据又需要回流训练才能继续优化模型。我见过最快能看出明显差异的也要一周。建议核心观察指标组合曝光点击率CTR是否提升了推荐精准度人均点击/使用时长是否提升了用户体验总量召回评价用线上日志模拟离线评估RecallK监控是否发生明显波动多样性指标是否出现固化的热门item循环有一点需要提醒的是如果CTR掉了但人均使用时长短长可能说明双塔太保守只推荐用户明确表达过的兴趣而忽略了探索带来的新鲜感。这时需要调温度系数或负样本中的难负样本比例让模型不过度收敛到已知兴趣。8.4 模型更新频率的取舍双塔模型的item向量是可以离线算好全量缓存的所以item侧的更新频率取决于你多久重算一次item向量。对于新闻/视频这类内容变化快的场景建议分钟级或小时级更新对于电商/商品这类相对稳定的item日级更新就够了。更新的粒度上还有个技巧可以只更新“增量item”的向量而不重算全量。这样能大幅降低算力开销但需要注意新增的item向量应当经过相同时期的item塔计算避免模型版本不一致导致向量空间漂移。9. 双塔 vs 其他召回方案什么时候用双塔什么时候该换9.1 与Youtube DNN、FM召回、GCN召回的对比这里放一张对比表方案优点缺点适用场景双塔训练快、部署简单、在线延迟低表达力有限难建模强交叉大多数工业场景默认选择Youtube DNN本质上就是双塔的一种加了时长加权基于用户历史序列cold start更弱视频推荐等强序列依赖场景FM召回二阶特征交叉能处理稀疏特征无法建模高阶非线性扩展性弱特征规模不大的中小业务GCN召回能利用图结构信息用户-item关系更丰富训练重、部署复杂、在线预测链路更长社交关系/知识图谱场景一句话总结双塔是目前“性价比最高的通用解”但它在表达上限上有天花板。如果你的业务有强社交关系、强序列依赖或者需要更复杂的多跳关联那么在图模型或序列模型上做增强是值得的。9.2 双塔其他模型的混合召回架构工业界更常见的做法是用多路召回不同召回通道产出不同候选合并后去重、截断、送入精排。举个例子一个电商App的召回层可能是这样的向量召回双塔用户主要兴趣召回类目召回用户最近常逛的品类下的热销item协同过滤召回相似用户的行为合并规则召回新品、促销、本地生活服务多路召回的每一路都负责不同的人群/场景/目标双塔只是其中一路但通常是最重要的一路。后续的精排统一对多路候选打分排序。实践提示多路召回有很多人被“合并截断”这一步整懵了。不同路召回的得分域不一致不能直接比较需要做分数归一化或用精排统一打分。双塔的得分如果直接和其他规则召回合并要么高得离谱要么低得离谱都会影响最终结果。9.3 如果item池特别大双塔还够用吗item池从千万到亿这个量级双塔依然够用但工程细节会变向量索引从单机Faiss变成分布式Milvus/ES向量索引。训练时的batch负采样容易因为batch太小没有区分度需要加大batch size或引入“全局负采样batch负采样”混合。item塔更新频率要跟上数据回流避免item向量过期。还有一个细节是item池越大随机负样本与正样本的区分度越高——因为大部分随机item本来就不相关模型很容易“偷懒”只学粗略的相关性而放弃了更精细的排序能力。对策就是前面说的难负样本刻意把那些“看起来可能相关但实际不相关”的item塞进负样本里逼着模型学更细的边界。10. 一点踩坑心得与最后的建议双塔召回模型连续做了几年最大的感受是模型结构文档里半小时能看完真正拉开差距的是那些文档里不会写的细节。样本负采样策略决定了模型能力的上限结构只决定能否逼近这个上限。训练和线上特征一致性比模型本身的调参更值得花时间。评估不要只盯RecallK要同时看多样性和用户长期留存。热度bias处理不到位再漂亮的向量召回也只会变成“热门item的搬运工”。最后分享一下我个人比较推荐的落地路径如果你是从零开始先用规则召回把推荐系统跑通积累数据确认日志准确无误再上双塔。双塔不是银弹但当你把数据链路理清、负样本策略调好后它带来的提升往往是最稳定的。如果你已经在跑双塔但效果不理想优先检查的是样本构成负样本比例和来源、特征一致性离线在线是否对齐、tau是否需要调整而不是急着换模型结构。这三板斧用熟绝大多数“双塔效果差”的case都能找到原因。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑