新闻详情

新闻详情

首页 / 资讯中心 / 详情

事后经验回放(HER)原理与实战:破解强化学习稀疏奖励难题

发布时间:2026/10/2 9:38:01来源:尧图网络
事后经验回放(HER)原理与实战:破解强化学习稀疏奖励难题
1. 项目概述什么是“hindsight”我第一次看到“hindsight”这个词脑子里蹦出来的是那句老话——“事后诸葛亮”。中文语境里这通常带点贬义但在人工智能领域它恰恰是一个重量级算法的名字全称是Hindsight Experience Replay国内一般译作“事后经验回放”。这个算法解决的是强化学习里一个公认的痛点稀疏奖励Sparse Reward。简单说就是当智能体完成一个任务非常困难、试错很久都拿不到正向反馈时该怎么让它学下去。先举一个生活化的例子。你让一个小孩学投篮如果规则是“必须投进才给糖”那前两个小时他基本是零反馈大概率直接放弃。但如果换个思路只要他碰到篮板就给半颗糖碰到篮筐就给一颗糖哪怕投歪了他也能从“歪了”这件事本身学到东西——出手力量、角度和落点之间的映射关系。事后经验回放的核心逻辑与此异曲同工即便这次任务失败了从“这次失败的过程”中也一定能提取出对某个“新目标”而言的成功经验。把每一个“歪打”都当成一次“正着”来学习探索效率就这样被翻了好几倍。这个项目标题背后的技术体系最早由OpenAI在2017年前后提出并公开。它主要面向的目标场景包括机械臂抓取与推箱子、机器人导航、游戏AI、以及任何“目标导向型”Goal-Conditioned的决策任务。对于一个刚入门强化学习、又苦于自己的智能体始终学不起来的同学来说这个算法属于那种“早该知道、必须吃透”的内容。它不需要对现有算法动大手术而是巧妙地改动了数据采样的环节就能让许多原本训不动的任务起死回生。这篇博文我会从原理讲到代码、再讲到实战里那些文档里根本不提的坑。适合的人群包括正在做机器人控制、想快速验证某个RL想法但卡在奖励设计上的研究者以及那些已经跑通了CartPole、想在更真实任务上找感觉的进阶学习者。读完你不仅能理解它为什么有效还能直接照着我给出的代码骨架搭一套自己的训练流程出来。2. 核心思路拆解如何把失败变成成功2.1 稀疏奖励到底难在哪在聊事后经验回放之前得先把“稀疏奖励”这个词掰开揉碎。假设你让智能体控制一只机械臂去抓桌面上的一个杯子。常规做法是定义一个奖励函数比如机械臂末端和杯子的距离小于某个阈值奖励为1否则为0。这是最直白的“成王败寇”。问题在于如果机械臂有6个自由度从随机初始化开始要碰巧让末端执行器恰好落到杯子附近10厘米范围内概率低得可怜。可能跑上百万步智能体拿到的奖励始终是0。这时候很多人的第一反应是“做奖励塑形”即给距离远的时候一个小的负奖励距离近的时候一个渐大的正奖励。乍一听合理但这里藏着一个隐蔽的坏处人为设计的密度奖励很容易造成“捷径学习”。比如你用“越近越好”来引导智能体很可能学到一种奇怪的姿态末端是靠近杯子了但夹爪是张开的或者手臂姿势完全不可用。这就是经典的“奖励黑客”问题。事后经验回放想解决的并不是“要不要奖励信息”而是如何在没有额外人工设计的前提下从稀疏的反馈里自动榨出更多学习信号。这就要提到它对“目标”的重新定义。2.2 把“客观失败”重写为“主观成功”事后经验回放适用的任务有一类共同特点任务可以用一个“目标Goal”来描述。比如前面说的抓杯子“杯子的位置”就是目标。更严格地说这类任务叫Goal-Conditioned强化学习智能体不仅要知道自己当前的状态还要知道此刻的任务目标是什么才能判断自己离成功还差多远。传统算法把轨迹存进经验回放缓冲区时存的是“状态、动作、奖励、下一状态”四元组。事后经验回放在这里动了一个非常聪明的手脚它额外存储了轨迹的“目标”字段并且允许你事后对这个目标做替换。具体操作是这样的。假设智能体这轮的目标是移动到坐标(1, 1)的位置但一通操作之后它实际跑到了坐标(2, 0)。按照原始目标这个回合失败奖励是0。事后经验回放会额外构造一条新经验把目标改成(2, 0)然后重新计算奖励。这时候智能体实际到达的位置和“新目标”完全一致于是奖励变成了1。换句话说它把一次“失败”的执行过程改写成了一段“成功”的轨迹。计算奖励时用的名义目标变成了轨迹的终点状态而动作、状态转移过程完全不变。从数学上看这等于凭空多了一批“正样本”。原本1%的成功率在事后经验回放的加持下每次探索都必然产生至少一条“达到某个目标”的成功经验智能体的学习信号不再是一片荒漠。这个思想其实和人类的学习模式高度一致你打算去某个朋友家结果走错路意外发现一家很好吃的餐馆那么“沿着这条路可以找到好吃的餐馆”这个知识在这次“失败的导航”里同样被你学到了。2.3 为什么这不算“作弊”很多人刚接触这个思想时都会有一种隐隐的不安把目标改了再算奖励这不是自欺欺人吗这里需要澄清一个关键机制经验回放本身是离线的它改变的是“学什么”不是“怎么做”。在强化学习里价值函数是用来评估“在某个状态下、针对某个目标、采取某个动作有多好”的。真实环境只给了智能体一条轨迹以及这条轨迹在原始目标下的反馈。但同一个轨迹如果在目标的维度上做泛化完全可以为其他目标提供训练样本。因为机械臂触碰到(2, 0)这个点的物理过程对于“如何去(2, 0)”这个问题而言就是一份货真价实的成功示范。更重要的是事后经验回放并没有给智能体任何“额外的环境信息”它只是把已经发生的状态转移重新贴上了标签。这种行为更像是一种“数据增强”在图像分类里你把猫的照片水平翻转一下它依然是一只猫样本量翻倍。事后经验回放做的事类似把轨迹终点标记为目标相当于将“接触(2, 0)”这个样本在目标维度上做了平移。最终训练出来的策略依然得在真实环境里面对真实的原始目标如果它真的学会了“移动到终点状态”这个通用能力那么在原始目标下也能做出正确决策。这不是投机取巧而是大幅提高了单条轨迹的数据利用率。3. 关键机制解析目标采样策略与网络设计3.1 四种目标采样策略为什么“future”最常用明确了“可以事后改目标”之后紧接着的问题是改目标时从哪找这个“新目标”最合理总不能随便从天上掉一个吧。事后经验回放论文里给出了几种备选方案每一种都代表不同的数据分布偏好。策略名新目标来源主要特点适用场景final轨迹的最后一个状态最简单直接实现成本最低任务是非稀疏的、探索相对充分episode轨迹中随机一个状态增加目标多样性覆盖中途状态当轨迹较长、中途状态也有意义时future当前时刻之后的某个状态保证“先到先得”的时序因果性大多数机器人控制任务的默认选择random整个状态空间随机采样提供全局探索但噪声较大状态空间较小、目标分布均匀时我自己的经验是future策略在绝大多数场景下都是最稳的。它的做法是在这条轨迹中从当前时间步之后的某个时间步的状态里挑一个当作新目标。为什么强调“之后”因为这保证了这个目标确实是被访问过的而且是在当前动作之后的某个时刻访问到的。换句话说这个目标在时间上是“可达的”时序因果上站得住脚。换用“episode”策略随机挑有时候会挑到当前时刻之前的状态那就意味着智能体先到达了目标、又走开了这种目标对当前动作的评价会比较混乱。当然future策略也有一个需要注意的地方新目标不能离当前状态太远否则构造出来的经验是“要立刻瞬移到一个很远的位置”这会让Q值估计出现很大的偏差。所以实际实现时通常会在“当前时刻之后的轨迹片段”里采样一个比较近的时间步例如从k到kt的范围里均匀取一个状态t可以设置为未来若干步。3.2 数据集构造把“目标”拼进状态里要真正动手实现事后经验回放首先得对状态表示动一个小手术。普通的强化学习经验是一个五元组(状态s, 动作a, 奖励r, 下一状态s, 是否结束done)。而在Goal-Conditioned设定下状态必须要同时包含“当前局面”和“任务目标”两个部分。比如机械臂任务中状态s是关节角度、末端速度等信息目标g就是杯中物体的三维坐标。实现时最朴素也最有效的方法是直接拼接把s和g连接成一个更大的向量作为神经网络输入。有一种说法是“加目标维度会让网络更难训练因为输入维度变大、且目标经常变动”。这里我的看法是拼接法虽然粗糙但目前依然是最稳妥的baseline。很多花哨的注意力机制或双编码器结构在数据量不够大的情况下反而不如简单拼接来得鲁棒。你需要做的只是保证动作选择策略和价值网络都严格以“当前状态目标”作为输入。在这个矩阵下当你要用事后经验回放生成额外的训练样本时需要做的就是改变g字段然后用新的g重新计算奖励。这里有个细节在目标被替换后done标志也应该重新计算。如果新目标本身就是轨迹终点那么这个“伪造”的trajectory结束标志是1如果新目标是中途某个状态那么它对应的done标志是0。处理不好会把时间差分学习的自举过程搞乱。3.3 网络与超参DDPG基座上的关键参数事后经验回放是一个即插即用的技巧通常不限定具体的底层强化学习算法。OpenAI的原始实现跑在DDPG上后来大家也把它移植到了SAC、TD3等算法里。但无论基座是什么网络层面的组件基本都是一样的一个Actor网络用于基于状态和目标输出动作一个Critic网络用于评估“在当前状态和目标下这个动作有多好”。Critic的输入是两个向量的拼接输出是一个标量Q值。基于我复现和调参的经验建议从下面这组参数起步参数名建议取值备注网络结构两层MLP256或512个隐藏单元太小的网络拟合不了高维目标目标网络软更新系数tau0.005过大的tau会让自举过程不稳策略噪声0.1到0.3之间让探索持续进行的关键经验池容量50万到100万条不要太小事后经验回放本身就会产生额外数据每次采样的future时间范围未来20到50步内范围太小目标与当前状态过于接近信息量不足每训练一步使用多少条额外伪造样本1比1或1比2伪造样本太多会冲淡真实的成功经验这里面最容易被低估的是“策略噪声”。很多人训出来后效果差不是算法错了而是Actor输出的动作太早走向确定性丧失了探索能力。强化学习不是深度学习的简单变形它必须保证在训练过程中持续向环境发起“有希望的尝试”。事后经验回放虽然提升了样本利用率但它并不能替代探索本身。关于这一点后面在避坑章节里再细说。4. 实操过程详解从零搭建一个抓取任务训练器4.1 环境准备定义一个“目标是位置”的任务想最快看到事后经验回放的效果建议不要直接用MuJoCo那个构建成本较高的Fetch环境而是先用一个简化的2D自定义环境验证逻辑。举个具体例子一辆小车在一个平面里从原点出发任务是到达一个随机位置的目标点。状态是当前坐标和速度动作是水平与垂直方向上的推力目标g是二维坐标。奖励定义很简单如果小车当前位置与目标点的欧氏距离小于0.1奖励为1否则为0。看起来简单但如果你直接拿随机策略去跑这个任务依然可以被视为稀疏奖励。因为地图若很大随机推力恰好走到目标点附近的概率同样不高。这就是一个典型的验证“后见之明”思想的最小环境。完成环境定义后每次重置时随机采样一个目标g保存在该回合的轨迹数据里。后续每次交互都记录五元组以及当前的目标g。这是事后经验回放的数据基础缺了目标字段后面的“事后改写”就无从谈起。4.2 核心代码事后经验回放的实现骨架下面给出一段核心伪代码展示事后经验回放最关键的一个步骤如何把新目标写入经验池。import numpy as np from collections import deque class HERBuffer: def __init__(self, capacity1000000, future_step50): self.buffer deque(maxlencapacity) self.future_step future_step # future策略的时间窗口 def store_episode(self, episode): # episode是包含多条transitions的列表 # 每条transition格式: [obs, goal, action, reward, next_obs, done] episode_len len(episode) # 1. 先原样存储整条真实轨迹 for transition in episode: self.buffer.append(transition) # 2. 对每个时间步用future策略构造额外经验 for t in range(episode_len): # 从t1到min(t1future_step, episode_len)之间采样一个新目标 future_bound min(t 1 self.future_step, episode_len) future_idx np.random.randint(t 1, future_bound) new_goal episode[future_idx][next_obs][:2] # 假设目标是位置取前两维 transition episode[t] obs, act transition[obs], transition[action] next_obs transition[next_obs] # 用新目标重新计算奖励 dist np.linalg.norm(next_obs[:2] - new_goal) new_reward 1.0 if dist 0.1 else 0.0 # done标志也根据新目标修正 new_done 1.0 if new_reward 1.0 else 0.0 # 新状态需要把新目标拼接进去 new_obs np.concatenate([obs, new_goal]) new_next_obs np.concatenate([next_obs, new_goal]) self.buffer.append((new_obs, act, new_reward, new_next_obs, new_done))这段代码里面可能很多人会问为什么奖励不是直接用下一步状态计算而是用next_obs的前两维因为在这个环境里目标就是二维坐标你在拼接状态和目标时前两维永远是“当前位置”。因此new_goal取的是“未来某个时刻的位置”。这是一种通用写法新目标必须取自轨迹中真实到达过的状态而不是随机编造的数字。记牢这一点基本就掌握了事后经验回放的语义核心。4.3 训练主循环DDPG网络与事后经验回放的配合接着把完整训练循环串起来。def train_her(env, buffer, actor, critic, epochs200, cycles8, episodes_per_cycle16): for epoch in range(epochs): for cycle in range(cycles): # 采样阶段与环境交互并收集经验 episodes [] for _ in range(episodes_per_cycle): obs env.reset() goal env.sample_goal() episode_buffer [] while True: # 将当前状态与目标拼接后输入Actor state_with_goal np.concatenate([obs, goal]) action actor.get_action(state_with_goal) next_obs, _, done, _ env.step(action) # 用原始的稀疏奖励 reward env.compute_reward(next_obs, goal) episode_buffer.append({ obs: obs, goal: goal, action: action, reward: reward, next_obs: next_obs, done: done }) obs next_obs if done: break episodes.append(episode_buffer) # 把每条episode原样存入buffer同时做HER扩展 for ep in episodes: buffer.store_episode(ep) # 学习阶段从buffer中采样mini-batch更新网络 for _ in range(40): batch buffer.sample(256) # 计算目标Q值更新Critic与Actor细节略 actor.update(batch) critic.update(batch)布局上这种“采样一批回合再训若干步”的结构不是为了省事而是为了保证buffer里面有足够多的整条轨迹供后续构造HER样本。如果你随机一步一存、一步一训后面想找到一整段“连续状态序列”来采样future目标就会变得非常麻烦。如果你拿到的基线代码是标准的逐步回放结构那你需要为HER单独维护一个“回合级别的循环缓冲”等一条回合结束之后再统一写入经验池。4.4 实验对照到底快多少为了验证事后经验回放的价值我做了一个对照实验。同样的2D导航任务同样的小车初始位置一组只用DDPG一组DDPGHER。横轴是环境交互步数纵轴是“成功率”即小车到目标点距离小于阈值的概率。结果非常直观纯DDPG在跑了大概30万步之后成功率依然在10%上下波动因为这个环境里拿到一次正奖励实在太难了。加了HER之后在同样的30万步内成功率已经能够冲到60%以上。甚至可以说HER在10万步左右就已经显露出明显的上升趋势。注意这里智能体并没有“作弊”——它最终面对的目标依然是随机生成的新目标而不是训练时见到的那一批。它只是从海量的“歪打正着”里提取出了“如何到达某个状态”的通用规律再迁移到新目标上。这恰好是后见之明这个概念最迷人的地方。5. 常见问题与避坑指南5.1 损失不降、成功率不动先查这五件事自己实现事后经验回放时最常见的挫折是明明感觉逻辑全对网络就是不收敛。根据我跑过的经验绝大多数问题集中在数据构造和超配设置的细节上。你可以按下面的清单顺序逐个排查。第一确认“目标”真的进入了网络输入。拼接后的向量是多一维还是少一维、新目标是否参与了后续的Q值计算这些地方经常出问题。第二检查奖励计算函数是否总是把新目标给替换干净了。假如你用了旧目标计算奖励那等于自欺欺人HER的“重写”环节失效训练的其实还是稀疏奖励能学才怪。第三检查DOÑA标志。前面提过新目标取自轨迹终点时done1取自中途状态时done0。如果你全设成1时序差分学习会过度相信短视的回报策略会变得非常急躁。第四观察策略噪声。如果探索噪声过小智能体后期基本只走一条线HER生成的所谓“成功经验”也全部长得差不多多样性不足新目标覆盖不到状态空间。第五不要忽视网络容量。目标维度一旦增加输入空间的复杂度也随之上升你原来那个“能跑通CartPole”的小网络很可能根本装不下这些信息。5.2 HER不是万能药什么时候该放弃这不是一剂让所有任务起死回生的灵药。首先HER要求任务目标必须“可表达、可采样、可判断”。你要是做围棋AI目标就是赢棋中途状态无法作为目标那你没法用HER。其次HER对“探索过程中产生的状态是否多样”有隐含假设。如果智能体的探索全是原地打转、轨迹蜷缩在一小块区域内那么这些轨迹换来的新目标也挤在同一个区域正样本分布极不均匀训练不出通用的能力。这种情况下我建议你先加大策略噪声、甚至引入随机目标初始分布来拓宽探索范围再来谈HER。最后HER的最大优势是解决“稀疏奖励”和“目标导向任务”如果你的任务奖励本来就是稠密的那HER带来的额外收益非常有限反而因为增加了数据构造复杂度拖慢训练。5.3 实用性技巧经验池怎么存更科学一个很容易被人忽略的工程细节是经验池的数据结构。标准的经验池用的是一个FIFO队列存一条丢一条。但HER要求“整条轨迹一起存”因此你需要一个“回合级”的缓冲先把这个回合的所有transition存在一个临时列表里等回合结束再统一写入大经验池。如果你用的小车环境每条轨迹长度固定可以预先分配一个numpy数组这样写入和采样效率会高很多不必要用Python列表做反复的append。另外经验池的容量一定要大。因为你每存一条真实轨迹就要额外追加大约整条轨迹长度的HER样本。同样一个环境数据量相当于原来的两倍到三倍。如果你沿用原来50万的池子旧数据被挤出去的速度会非常快很多早期有探索价值的信息会过早丢失。我一般习惯把容量翻倍甚至更大。6. “后见之明”概念的边界与延伸写到这里我不由想到hindsight这同一个概念在不同领域长得完全是两张脸。在AI领域它是HER这个算法的灵魂在产品设计领域苹果的Shortcuts应用里也曾推出过一个叫“Hindsight”的功能它做的事情是利用“事后提醒”的机制让你在结束一件活动后收到一条回顾消息——大意是把“当时没来得及记录下来的事”在你“事后想起”的时间点推送给你。这里面的设计哲学和HER如出一辙人并不需要在一开始就瞄准明确的目标把已经发生过的事情重新组织成“有效信息”同样有价值。而在独立游戏领域有一款口碑不错的叙事游戏《Hindsight》讲的是母亲离世后女儿通过翻拣旧物来重新审视回忆。游戏机制里每件物品都会触发一段过去玩家要做的不是“改变历史”而是换一个视角去理解它。这种“用现在重写过去意义”的设计和HER的“用事后目标重写轨迹标签”底层逻辑惊人地一致。理解了这一点你再看HER就会更容易抓住它的神韵。它不试图让智能体每一次都做对而是承认“错误过程”本身也有巨大的学习潜力。与其挖空心思设计复杂的奖励函数逼着智能体走向你预设的路径不如设计好“事后解读”的机制让它自己从失败里提炼规律。这一点对我影响很深我后来做很多决策任务时都会先问自己一句这个任务有没有可能让智能体借助“事后目标”促进探索如果能那就值得把HER那一套流程架起来。7. 我的实操总结与几点最终建议跑过几十个实验后我的体会是HER是一个概念极简、实现却极其讲究细节的算法。它真正改写命运的地方不在网络结构而在数据管线的设计。你把目标拼接、HER采样、奖励重算、done标志修正这几个环节做对了之后几乎不用太操心演员评论家本身的结构效果自然就出来了。反过来说哪怕把TD3或SAC做得再花哨只要数据层的HER构造有偏差训练过程依然会像老牛拉车一样寸步难行。最后分享一个我在实践里觉得特别实用的小技巧在HER训练初期可以故意在状态空间里撒一批“手动设定的成功样本”比如从某一时刻开始让小车以较小的噪声走向一个固定目标。这样能让经验池在最初阶段就有一批高质量分布数据HER的伪造样本也能搭着这趟车快速扩散开来训练能快不少。这种“数据播种”的操作在很多论文里不会写但实测下来真的很有用。如果你正准备在自己的机器人项目里引入HER我的建议是先不要急着上高保真仿真把2D小车或者类似的环境跑透吃透数据格式和采样逻辑再迁移到MuJoCo或真实机械臂上。因为多自由度环境的报错会让你根本分不清是网络问题还是HER构造问题而在简单环境里你只需要几分钟就能定位所有细节错误。这个算法值得你花时间吃透它带给你的不仅是某一个项目的性能提升更是一种看待“失败经验”的全新视角。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

【OpenClaw从入门到精通】第25篇:OpenClaw 2026实战——自定义Provider/Channel/ContextEngine插件从0到1开发(附完整代码) 2026/10/2 12:19:23

【OpenClaw从入门到精通】第25篇:OpenClaw 2026实战——自定义Provider/Channel/ContextEngine插件从0到1开发(附完整代码)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
哪些模型、中间件与Agent框架原生支持MCP?一文带你全面掌握TaoToken统一接入实践! 2026/10/2 12:19:23

哪些模型、中间件与Agent框架原生支持MCP?一文带你全面掌握TaoToken统一接入实践!

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
数据结构英文归纳:用 TaoToken 统一 Key 打通 Cline MCP 的术语检索链路 2026/10/2 12:19:23

数据结构英文归纳:用 TaoToken 统一 Key 打通 Cline MCP 的术语检索链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
如何在 Antigravity 中使用 MCP 服务器:把 endpoint 改到 TaoToken 的完整配置 2026/10/2 12:19:23

如何在 Antigravity 中使用 MCP 服务器:把 endpoint 改到 TaoToken 的完整配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
OpenClaw 与 Cursor 的 AI 编程路线之争:TaoToken 统一 Key 下,助手与平台如何各取所需? 2026/10/2 12:19:04

OpenClaw 与 Cursor 的 AI 编程路线之争:TaoToken 统一 Key 下,助手与平台如何各取所需?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
豆包2.1 Pro模型发布后,Coding与Agent能力如何通过TaoToken统一API接入实战 2026/10/2 12:19:04

豆包2.1 Pro模型发布后,Coding与Agent能力如何通过TaoToken统一API接入实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉