
1. 项目概述为什么我们需要STAPO最近在折腾LLM智能体训练的朋友估计都绕不开一个核心痛点样本效率低得令人发指。你辛辛苦苦搭好环境设计好奖励函数跑起来一看智能体要么在任务初期就卡住不动要么学了一堆乱七八糟的动作离你想要的目标差了十万八千里。更头疼的是传统的强化学习RL方法比如PPO近端策略优化在面对LLM智能体这种动作空间巨大可以理解为每一步都能生成一段文本、状态转移复杂前一步的回复会影响后续的对话走向的场景时常常显得力不从心。它像个“老好人”对所有经历过的轨迹Trajectory一视同仁好的坏的都拿来更新模型结果就是有用的信号被大量噪声稀释训练过程缓慢且不稳定。这就是“STAPO: Selective Trajectory-Aware Policy Optimization”这个工作试图破局的关键。我第一次看到这个标题时就被两个词抓住了“Selective”选择性和“Trajectory-Aware”轨迹感知。这直接戳中了当前LLM Agent训练的两个核心挑战。简单来说STAPO不是一个从零开始的全新算法而是一个精巧的优化框架。它的核心思想是我们不应该平等地对待智能体在环境中探索产生的每一条轨迹即从任务开始到结束的一系列状态、动作、奖励序列。相反我们应该像一个有经验的教练有选择地挑出那些蕴含高学习价值的轨迹片段并让模型更“深刻”地感知和理解这些片段中的状态-动作依赖关系从而进行更高效、更稳定的策略更新。想象一下教一个新手下棋。你不会把他每一盘棋从头到尾的每一步都拿来复盘那样太费时间而且很多昏招看了也没用。你会重点挑出几盘棋中的几个关键回合——比如他本可以将军却错过了的机会或者他走出的那步精妙的防守——来深入讲解。STAPO干的就是类似的事情选择性复盘重点教学。它通过一套机制自动筛选出高优势Advantage、低不确定性或者能带来关键转折的轨迹片段然后针对这些片段进行强化学习更新。这样做的好处显而易见减少了无效数据的干扰放大了高质量经验的影响从而有望大幅提升训练收敛速度和最终策略的性能。对于正在研究或应用LLM Agent的开发者、研究员来说理解STAPO意味着掌握了一种可能显著提升你智能体训练效率的“加速器”。无论是构建复杂的对话助手、游戏NPC还是自动化工作流Agent训练成本和时间都是硬门槛。STAPO提供了一种思路让我们能更聪明地利用有限的交互数据让大模型智能体更快地学会“做事”。2. STAPO核心设计思路拆解从“全盘接收”到“择优而教”要理解STAPO我们不能只看它做了什么更要理解它为什么要这么做。这背后是对标准策略优化方法尤其是PPO在序列决策场景中局限性的深刻洞察。2.1 标准PPO在序列生成中的困境在LLM Agent的场景下策略Policy就是那个接收当前状态比如当前的对话历史、任务描述、环境观察然后输出一个动作即生成下一段文本或选择一个工具调用的大语言模型。PPO通过鼓励模型多采取能获得高奖励的动作同时限制每次更新的幅度通过重要性采样和裁剪来稳定地提升策略。但问题出在“轨迹”上。一个完整的任务轨迹可能包含几十甚至上百个生成步骤。在标准的PPO中这个长轨迹会被切成一个个状态动作奖励三元组然后一股脑儿地用于计算损失函数。这里有几个关键问题奖励稀疏与延迟很多任务只有最终成功或失败时才有显著奖励中间步骤的奖励信号很弱甚至是零。整条轨迹中可能只有末尾几步才真正有价值。动作依赖性被忽略PPO的更新通常假设每个时间步的动作是相对独立的。但在文本生成中前一个词的选择会极大地影响后续所有词的概率分布和最终结果。这种强依赖性在标准的、按时间步拆分的更新中被严重弱化了。噪声干扰智能体在探索初期会产生大量随机、无效的轨迹。平等地使用这些数据就像在嘈杂的集市里听人讲话有用的信息很容易被淹没。因此我们需要一种方法能够识别并聚焦于轨迹中那些真正对任务成功有贡献的关键决策点。2.2 STAPO的双重核心机制STAPO的解决方案可以概括为两个核心机制这也是其名称的由来机制一选择性Selective—— 筛选高价值经验这是STAPO的“过滤器”。它不会使用整条轨迹的所有时间步而是根据某种准则筛选出一个子集。常见的筛选准则包括基于优势函数Advantage优势函数A(s, a)衡量了在状态s下采取动作a相比平均情况有多好。STAPO可以设定一个阈值只保留优势值显著为正比如大于某个百分位数的s, a对。这确保了更新主要针对那些被证明是“好”的决策。基于不确定性或新颖性在探索阶段智能体需要尝试新东西。STAPO也可以选择那些模型预测不确定性高例如通过集成多个模型或计算熵的状态-动作对鼓励探索。基于轨迹片段回报不按单步而是按片段比如连续的几个步骤计算累计回报选择回报高的片段。这更适合处理奖励延迟的问题。机制二轨迹感知Trajectory-Aware—— 在上下文中优化策略这是STAPO的“放大器”。仅仅筛选出关键步骤还不够我们还需要让模型在更新时能“看到”这个关键步骤所处的局部轨迹上下文。具体来说当我们要更新在时间步t采取的动作a_t时标准的PPO可能只考虑当前的状态s_t。而轨迹感知的更新会显式地将之前若干步的状态和动作s_{t-k}, a_{t-k}, …, s_{t-1}作为条件或上下文输入到策略网络中。这样做有什么好处它让模型学习到的是“在这样一个具体的对话或任务进展脉络下做出这个好动作”的模式而不是孤立地学习“在某个抽象状态下做出某个动作”。这更符合序列决策的本质尤其是对于LLM这种基于自回归生成的模型其下一个token的生成本就严重依赖于历史上下文。注意这里的“轨迹感知”实现起来有不同方式。一种直接的方式是在策略网络的输入中拼接历史状态可能需要编码。另一种更精巧的方式是修改优势函数或价值函数的估计使其依赖于一段历史而不仅仅是当前状态。STAPO框架本身定义了这种思想具体实现可以根据任务调整。2.3 STAPO的整体训练流程结合以上两点一个典型的STAPO训练循环可能如下所示采样阶段让当前策略LLM Agent在环境中运行收集一批完整的任务轨迹数据。评估与筛选阶段对每条轨迹计算每个时间步或每个片段的优势值或其他筛选指标。根据预设的筛选策略如“选择优势值最高的前30%的时间步”得到一批被选中的高价值状态动作对以及它们对应的局部轨迹上下文。优化阶段使用这批筛选后的、附带上下文的数据构造一个“轨迹感知”的损失函数。这个损失函数的基础仍然是PPO的 clipped surrogate objective但其输入和优势估计都考虑了历史信息。执行一步或多步梯度下降更新LLM的策略参数。循环重复步骤1-3。这个过程的核心思想是数据质量重于数据数量。通过精心挑选训练数据并让模型在更丰富的上下文中学习每一次参数更新的“含金量”都更高了。3. 核心细节解析与实操要点理解了STAPO的思想框架后我们来看看把它落地到实际LLM Agent训练项目中需要关注哪些核心细节和实操要点。纸上谈兵容易真正调通一个训练流程细节决定成败。3.1 如何定义与计算“价值”——优势函数的选择与陷阱筛选高价值经验首要问题就是如何定义“价值”。优势函数A(s, a) Q(s, a) - V(s) 是最常用的标尺但它本身的计算就是一门学问。广义优势估计GAE这几乎是当前RL实践中的标准工具。GAE通过引入一个λ参数在估计的偏差和方差之间做折中能更平滑、更有效地估计优势值。在STAPO中使用GAE来计算每个时间步的优势值是合理的起点。# 伪代码示例计算GAE def compute_gae(rewards, values, gamma0.99, lam0.95): # rewards: 轨迹的奖励序列 [T] # values: 价值网络预测的状态值序列 [T] # 返回优势值序列 advantages [T] advantages [] gae 0 next_value 0 # 假设轨迹结束后的价值为0 for t in reversed(range(len(rewards))): delta rewards[t] gamma * next_value - values[t] gae delta gamma * lam * gae advantages.insert(0, gae) next_value values[t] return advantages实操心得lam参数很关键。lam接近1时优势估计更依赖于长期回报方差大但偏差小lam接近0时则更依赖于单步TD误差。对于LLM Agent这种长序列任务我通常从0.95开始尝试如果发现训练不稳定优势值波动过大可以适当调低至0.9或0.85。价值网络Critic的准确性至关重要优势函数计算依赖于价值网络V(s)的预测。如果Critic训得不好V(s)不准那么计算出来的优势值就是“失灵的指南针”会引导STAPO筛选出错误的数据。因此必须确保Critic网络有足够的容量例如一个与策略网络共享底层Transformer编码器但顶层独立的MLP头并且得到充分的训练。一个常见的技巧是在更新策略Actor之前先对Critic进行多轮更新使其价值估计相对稳定。针对LLM的调整标准的GAE假设状态是马尔可夫的。但在LLM生成中当前隐藏状态已经编码了历史信息。因此直接使用Transformer最后一层隐藏状态对应的价值估计通常就能较好地工作。更高级的做法可以尝试让Critic也是一个小型语言模型输入完整的上下文来预测状态价值。3.2 “选择性”策略的设计阈值、比例还是排序确定了价值的度量标准后下一步就是制定筛选策略。这里有几个常见的方案Top-K 比例筛选最简单直接。例如只保留优势值排名在前30%的时间步数据用于策略更新。这种方法实现简单但比例需要调参。比例太高则筛选效果弱比例太低可能导致有效数据过少更新方差变大。绝对阈值筛选只保留优势值大于某个阈值如A(s,a) 0.5的数据。这种方法更直观但阈值的设定非常依赖任务奖励的尺度泛化性较差。通常需要先跑几轮实验观察优势值的分布范围。基于分位数的动态阈值结合了以上两者的优点。例如每轮训练都计算当前批次优势值的70%分位数只使用优势值高于这个分位数的数据。这样阈值能自适应数据分布的变化。片段级筛选不以单步为单位而是将轨迹划分为固定长度如5步的片段计算片段的累计回报或平均优势然后筛选高价值的片段。这对于奖励稀疏的任务特别有用能确保被选中的片段至少是“整体向好”的。我的经验对于LLM Agent的初期探索我推荐从Top-K比例筛选例如20%-40%开始。它的效果相对稳定参数意义明确。可以先设一个保守的值如40%观察训练曲线。如果训练稳定但缓慢可以尝试降低比例以加速如果训练不稳定策略性能剧烈波动则可能需要提高比例或检查Critic网络和优势估计是否可靠。3.3 实现“轨迹感知”上下文窗口与网络结构如何让策略更新时感知到历史轨迹这里有两种主流的实现思路各有优劣。方案A扩展策略网络输入模型外拼接这是最直观的方法。在构造训练数据时对于每一个被选中的时间步t我们不仅提供当前的状态表示s_t如当前对话回合的文本还将之前k步的状态s_{t-k}, …, s_{t-1}也作为额外的上下文一起输入给策略网络LLM。如何实现可以将历史k步的文本直接拼接在当前输入之前用特殊的标记如[SEP]分隔。或者将历史状态通过一个编码器得到向量再与当前状态向量拼接。优点实现简单易于理解。对于基于Transformer的LLM增加上下文长度是其原生能力。缺点计算开销大输入长度增加了k倍导致自注意力计算复杂度呈平方增长训练速度会显著下降。可能干扰当前决策过长的、不相关的历史信息可能会“稀释”当前状态的重要性模型需要学会自己从长上下文中提取相关信息这增加了学习难度。k的选择k是一个超参数。设得太小感知不到足够的历史设得太大效率低下且可能引入噪声。方案B在优势估计中引入轨迹依赖模型内隐式感知这种方法不改变策略网络的输入而是改变目标函数。我们让优势函数A不仅依赖于当前状态s_t还依赖于之前的状态动作历史H_t (s_0, a_0, …, s_{t-1}, a_{t-1})。即我们计算的是A(H_t, s_t, a_t)。如何实现这通常意味着我们需要一个不同的Critic网络它能够处理序列输入并输出一个依赖于历史的价值估计。例如可以使用一个RNN或另一个Transformer来编码整个轨迹片段然后为每个时间步输出一个条件价值估计V(H_t, s_t)。优势值则通过这个条件价值来计算。优点更符合理论在部分可观测或非马尔可夫环境中真正的优势本就应依赖于历史。可能更高效策略网络Actor的输入保持不变计算开销小。复杂的轨迹建模工作交给了专门的Critic。缺点实现复杂需要设计并训练一个序列Critic这本身就是一个挑战。训练稳定性同时训练序列Critic和策略网络可能使整个训练系统更复杂、更不稳定。实操建议对于大多数希望快速验证STAPO效果的团队我建议从方案A开始并设置一个较小的k例如3-5。这样可以在不过度增加计算负担的情况下初步验证“轨迹感知”带来的收益。如果效果显著再考虑是否值得投入资源去实现更复杂的方案B。4. 实操过程与核心环节实现现在让我们抛开理论进入实战环节。我将以一个具体的场景为例拆解如何将STAPO整合到一个基于PPO的LLM Agent训练流程中。假设我们的任务是训练一个基于文本的网页导航智能体目标是根据用户指令如“找到价格低于100元的蓝牙耳机”通过点击网页上的链接或按钮最终完成任务。4.1 环境搭建与数据流设计首先我们需要一个模拟环境。这里可以使用MiniWoB或WebShop等基准环境或者自己用Playwright/Selenium封装一个简单的模拟器。环境接口需要提供reset(instruction): 重置环境返回初始状态如网页HTML或截图指令。step(action): 执行动作如click(‘id_123’)或type(‘search_box’, ‘bluetooth耳机’)返回新的状态、奖励、完成标志。数据流管道设计是STAPO高效运行的基础。一个典型的数据流如下[策略LLM] --(生成动作)-- [环境] --(状态、奖励)-- [经验缓冲区] | v [GAE计算 轨迹筛选] -- [批量轨迹数据] -- [定期从缓冲区采样] | v [轨迹感知数据打包] | v [PPO更新步骤] | v [更新后的策略LLM]关键是要有一个经验缓冲区Replay Buffer它按轨迹episode存储数据。每一轮训练我们从缓冲区采样一批完整的轨迹而不是零散的时间步。这是后续进行轨迹级筛选和上下文构建的前提。4.2 STAPO模块的集成与代码框架假设我们有一个基础的PPO训练代码。集成STAPO主要需要修改两个地方数据采样后处理和损失函数构造。步骤1收集轨迹并计算优势import torch import numpy as np def collect_trajectories(agent, env, num_episodes): 收集多条完整轨迹 all_trajectories [] for _ in range(num_episodes): state env.reset() done False states, actions, rewards, values, log_probs [], [], [], [], [] while not done: # agent.act 返回动作、对数概率、状态价值 action, log_prob, value agent.act(state) next_state, reward, done, _ env.step(action) states.append(state) actions.append(action) rewards.append(reward) values.append(value) log_probs.append(log_prob) state next_state trajectory { states: states, actions: actions, rewards: rewards, values: values, log_probs: log_probs } all_trajectories.append(trajectory) return all_trajectories def compute_advantages_and_returns(trajectories, gamma0.99, lam0.95): 为所有轨迹计算GAE优势值和回报 for traj in trajectories: rewards traj[rewards] values traj[values] advantages [] gae 0 next_value 0 # 轨迹结束 for t in reversed(range(len(rewards))): delta rewards[t] gamma * next_value - values[t] gae delta gamma * lam * gae advantages.insert(0, gae) next_value values[t] traj[advantages] advantages traj[returns] [a v for a, v in zip(advantages, values)] # 回报 优势 价值 return trajectories步骤2实施选择性筛选def selective_sampling(trajectories, selection_ratio0.3): Top-K比例筛选返回被选中的(s, a, log_prob, adv, return)及上下文 # 将所有时间步的数据打平 all_states, all_actions, all_log_probs, all_advantages, all_returns [], [], [], [], [] all_traj_indices, all_timesteps [], [] # 记录来源用于构建上下文 for traj_idx, traj in enumerate(trajectories): T len(traj[states]) for t in range(T): all_states.append(traj[states][t]) all_actions.append(traj[actions][t]) all_log_probs.append(traj[log_probs][t]) all_advantages.append(traj[advantages][t]) all_returns.append(traj[returns][t]) all_traj_indices.append(traj_idx) all_timesteps.append(t) # 转换为Tensor以便排序 advantages_tensor torch.tensor(all_advantages) # 计算要保留的数据量 k int(len(advantages_tensor) * selection_ratio) # 获取优势值最大的前k个索引 _, topk_indices torch.topk(advantages_tensor, k) # 根据索引筛选数据 selected_data { states: [all_states[i] for i in topk_indices], actions: [all_actions[i] for i in topk_indices], old_log_probs: torch.stack([all_log_probs[i] for i in topk_indices]), advantages: advantages_tensor[topk_indices], returns: torch.tensor([all_returns[i] for i in topk_indices]), traj_indices: [all_traj_indices[i] for i in topk_indices], timesteps: [all_timesteps[i] for i in topk_indices] } return selected_data, trajectories步骤3构建轨迹感知的输入数据def build_trajectory_aware_inputs(selected_data, trajectories, context_k3): 为选中的数据点构建包含历史上下文的输入 augmented_states [] for idx, (traj_idx, t) in enumerate(zip(selected_data[traj_indices], selected_data[timesteps])): traj trajectories[traj_idx] # 获取当前状态 current_state selected_data[states][idx] # 获取历史上下文最多前context_k步 start_idx max(0, t - context_k) history_states traj[states][start_idx:t] # t时刻的状态不包含在内 # 构建增强输入这里简单地将历史文本拼接在前面 # 假设state是文本字符串 if history_states: # 使用特殊标记连接历史例如 [SEP] context_text [SEP] .join(history_states) augmented_state context_text [SEP] current_state else: augmented_state current_state augmented_states.append(augmented_state) selected_data[augmented_states] augmented_states # 注意selected_data[states] 仍然保留原始状态可用于Critic输入如果需要 return selected_data步骤4修改PPO更新步骤现在我们使用筛选并增强后的数据来进行更新。关键点在于策略网络Actor的输入从原始的state变成了augmented_state。def ppo_update_step(agent, selected_data, clip_epsilon0.2, value_coef0.5, entropy_coef0.01): 使用筛选后的轨迹感知数据进行PPO更新 # 解包数据 augmented_states selected_data[augmented_states] actions selected_data[actions] old_log_probs selected_data[old_log_probs] advantages selected_data[advantages] returns selected_data[returns] # 使用增强后的状态让策略网络Actor重新计算动作概率 # agent.get_action_log_prob 现在接收的是 augmented_states new_log_probs, entropy agent.get_action_log_prob(augmented_states, actions) state_values agent.get_value(selected_data[states]) # Critic可能仍用原始状态或也可用增强状态 # 计算策略损失PPO裁剪目标 ratio torch.exp(new_log_probs - old_log_probs) surr1 ratio * advantages surr2 torch.clamp(ratio, 1 - clip_epsilon, 1 clip_epsilon) * advantages policy_loss -torch.min(surr1, surr2).mean() # 计算价值损失MSE value_loss torch.nn.functional.mse_loss(state_values, returns) # 计算熵正则项鼓励探索 entropy_loss -entropy.mean() # 总损失 total_loss policy_loss value_coef * value_loss entropy_coef * entropy_loss # 反向传播与优化 agent.optimizer.zero_grad() total_loss.backward() torch.nn.utils.clip_grad_norm_(agent.parameters(), max_norm0.5) # 梯度裁剪 agent.optimizer.step() return total_loss.item(), policy_loss.item(), value_loss.item()通过以上四个步骤我们就在一个标准的PPO训练循环中嵌入了STAPO的核心思想。每一轮训练我们只使用优势值最高的那部分经验并且这些经验在用于更新策略时都带上了最近几步的历史上下文。5. 常见问题与排查技巧实录在实际实现和调试STAPO的过程中你几乎一定会遇到下面这些问题。我把它们和我的排查经验记录下来希望能帮你少走弯路。5.1 训练不稳定策略性能剧烈波动或崩溃这是RL训练尤其是结合LLM时最常见的问题。在引入STAPO后问题可能被放大。可能原因1筛选比例过于激进。现象初期训练曲线还能看但很快策略得分骤降智能体开始输出无意义内容或重复动作。诊断检查被筛选出的数据量。如果每轮只有极少量的数据点比如少于100个被用于更新那么更新的方差会非常大策略容易“学偏”。解决逐步提高selection_ratio例如从0.5开始观察训练是否变得平稳。也可以实现一个动态比例根据当前批次优势值的分布自动调整确保每轮有足够的最小数据量。可能原因2Critic价值网络训得不好。现象优势值计算出现极端数值非常大或非常小的正/负数或者持续为负导致筛选出的数据没有代表性。诊断绘制价值网络预测值V(s)和实际回报Returns的曲线。在训练初期两者应该大致相关。如果V(s)严重偏离实际回报说明Critic没学好。解决增加Critic更新频率在更新Actor策略之前先对Critic进行多轮如10-20轮梯度更新使其快速拟合当前数据。使用更稳定的价值损失尝试Huber损失代替MSE它对异常值不那么敏感。归一化优势值在每批数据内部对计算出的优势值进行减均值、除标准差的归一化。这能稳定策略更新的尺度。advantages (advantages - advantages.mean()) / (advantages.std() 1e-8)可能原因3上下文窗口k引入噪声。现象加入轨迹感知后效果反而变差。诊断检查构建的augmented_states。如果历史信息与当前决策无关甚至矛盾就会干扰模型。解决减小context_k从1开始尝试或者实现更智能的上下文选择。例如不一定是严格的最近k步而是选择优势值较高的历史步骤作为上下文。5.2 训练速度明显变慢可能原因1轨迹感知导致输入序列变长。现象每个训练迭代的时间显著增加。诊断这是预期之中的代价。使用context_k5意味着策略网络每次前向传播处理的平均文本长度增加了数倍。解决权衡取舍评估性能提升是否值得速度损失。对于简单任务可能不需要很长的上下文。优化实现对历史状态进行压缩编码。例如用一个轻量级的编码器如一个小型LSTM或平均池化将历史状态序列编码成一个固定长度的向量再与当前状态拼接而不是拼接原始文本。梯度累积如果因为筛选导致有效批量大小batch size变小可以通过梯度累积来模拟更大的批量这可能更高效。可能原因2筛选过程本身的计算开销。现象数据后处理计算GAE、排序、筛选耗时很长。诊断如果轨迹数量多、长度长对全部优势值进行排序O(n log n)可能成为瓶颈。解决在线筛选在环境交互过程中实时计算优势值需要一个已训练的Critic并决定是否将当前(s,a)对存入一个“高优先级缓冲区”。这类似于PER优先经验回放的思想。近似Top-K使用基于分位数的阈值法代替精确排序计算更快。5.3 智能体“早熟”或探索不足可能原因筛选机制过于偏向“ exploitation”利用压制了“ exploration”探索。现象智能体很快找到一个局部最优策略比如总是点击第一个链接然后性能就停滞了无法发现更优的路径。诊断检查被筛选数据的优势值分布。如果几乎总是正数说明智能体很少尝试不确定但可能有潜力的动作。解决在筛选准则中引入探索激励修改筛选标准使其不仅考虑高优势值也考虑高不确定性例如策略熵高或新颖性例如访问罕见的状态。混合采样保留一部分如10%的随机采样数据无论优势值高低与筛选出的高价值数据混合使用。这保证了最低限度的探索。调整熵系数适当增加PPO损失中的熵正则项系数entropy_coef鼓励策略保持随机性。5.4 调试与监控清单为了高效地调试STAPO训练建议建立以下监控面板监控指标健康范围异常可能原因平均每轮回报总体趋势应上升允许波动持续下降或剧烈震荡见问题1优势值均值/标准差均值在0附近标准差稳定均值绝对值过大或标准差激增Critic问题筛选比例/数据量根据设置稳定或动态变化平滑数据量骤减至个位数筛选过严策略损失 / 价值损失策略损失为负值在PPO中价值损失逐渐减小策略损失为正或价值损失爆炸学习率、裁剪等问题策略熵初期较高后期缓慢下降熵过早降至极低早熟探索不足裁剪比率ratio大部分落在[1-ε, 1ε]内大量ratio被裁剪更新步长太大或旧策略已过时最后记住RL训练特别是涉及LLM的充满了随机性。任何改动包括STAPO的超参数都需要多次运行通常3-5次不同随机种子来确认其统计显著性。从一个简单的基础PPO开始稳定后再逐步加入STAPO的各个组件先加筛选稳定后再加轨迹感知是控制变量、定位问题的好方法。STAPO不是银弹但它提供了一套系统性的工具让你能更有方向地去优化那个既昂贵又充满不确定性的LLM Agent训练过程。