Apollo规划模块实战:从数据清洗到降级策略的工程优化 1. 规划模块在Apollo架构中的真实位置很多人第一次接触Apollo的规划模块容易把它当成一个孤立的“路径生成器”——输入起点终点输出一条轨迹完事。但实际跑过Apollo代码或者调过实车的人都知道规划模块是整个自动驾驶系统里最“夹心层”的存在上游要消化感知、定位、地图、预测四个模块的输出下游要给控制模块喂一条带速度信息的、时间对齐的、可执行的轨迹。任何一层的数据抖动最后都会在规划这里集中爆发。我在一个模拟项目里做过统计规划模块收到的输入数据里大约有30%的帧存在不同程度的“脏数据”——定位跳变、预测障碍物轨迹置信度骤降、地图匹配失败等等。规划模块不能拒绝这些输入它必须在有限的时间内通常100ms以内给出一个“当前最优解”。这就决定了Apollo规划模块的设计哲学不是追求全局最优而是追求在约束条件下的实时可行。Apollo的规划模块在架构上分成三个核心层次这个分层逻辑值得先讲清楚因为后面所有的细节都挂在这三个层次上决策层Decision回答“要不要变道、要不要跟车、要不要让行”这类离散问题。它输出的是一组决策标签比如CHANGE_LANE_LEFT、FOLLOW、STOP。优化层Optimization在决策层给定的约束下求解一条平滑的、满足车辆运动学约束的轨迹。Apollo主要用基于二次规划的离散点优化以及基于样条曲线的平滑。规划层Planning把优化后的轨迹做时间维度的速度规划最终输出带时间戳的轨迹点序列。这三层不是串行流水线而是有反馈回路的。决策层如果发现优化层求解失败会降级到更保守的决策优化层如果发现速度规划不可行会反过来调整路径的曲率约束。这种“层间协商”机制是Apollo规划模块比较独特的地方也是很多自研规划系统容易忽略的。提示如果你在调试规划模块时发现轨迹频繁跳变先别急着改优化器的权重参数大概率是决策层的状态机在相邻帧之间发生了震荡。优先检查决策状态的切换条件是否加了滞回区间。2. 从感知到规划数据流的清洗与对齐规划模块拿到的原始数据质量参差不齐。感知模块输出的障碍物列表可能包含大量误检的“幽灵障碍物”预测模块输出的轨迹可能因为模型推理延迟而比实际时间戳晚了50ms定位模块在隧道或高架下可能突然丢失RTK信号导致位置跳变几米。这些数据如果不做清洗和对齐直接喂给规划器出来的轨迹要么急刹要么画龙。2.1 障碍物列表的过滤策略Apollo在规划模块的入口处有一个ObstacleFilter它的过滤逻辑不是简单的“置信度低于阈值就丢弃”。我实际读过这部分代码它做了几件事第一基于历史轨迹的一致性校验。如果一个障碍物在连续5帧里只出现了2帧而且位置跳变超过2米它会被标记为“低可信”但不会立即删除而是放入一个“观察池”。只有连续3帧稳定出现后才会进入规划器的考虑范围。这个设计很巧妙——它避免了因为单帧误检导致规划器做出过度反应比如突然急刹同时也保留了真实障碍物突然出现的响应能力。第二基于地图先验的过滤。如果障碍物的位置落在已知的静态障碍物区域比如地图里标注的隔离带、桥墩而且它的速度接近零那么它会被直接忽略。这个逻辑依赖高精地图的准确性如果地图本身有偏差反而会引入新的问题。第三基于运动学可行性的过滤。如果一个障碍物的预测轨迹显示它能在1秒内从0加速到100km/h或者转弯半径小于车辆最小转弯半径那么这个预测轨迹会被判定为“物理不可行”规划器会退回到使用障碍物的当前位置和速度做保守估计。# 伪代码示意障碍物过滤的核心逻辑 def filter_obstacles(obstacles, history_buffer, map_info): filtered [] for obs in obstacles: # 历史一致性检查 if not is_consistent(obs, history_buffer): observation_pool.add(obs) continue # 地图先验过滤 if is_in_static_region(obs, map_info) and obs.speed 0.1: continue # 运动学可行性检查 if not is_kinematically_feasible(obs.predicted_trajectory): obs.predicted_trajectory conservative_estimate(obs) filtered.append(obs) return filtered2.2 时间戳对齐的坑时间戳对齐是规划模块里最容易被低估的环节。Apollo的规划周期是100ms但感知和预测模块的输出频率可能不是严格的10Hz。我遇到过的情况是预测模块因为模型推理耗时波动输出的轨迹时间戳有时候是当前帧的有时候是上一帧的。如果规划器直接用这些轨迹做碰撞检测就会出现“明明障碍物已经过去了规划器还在刹车”的诡异现象。Apollo的解决方案是在规划模块内部维护一个时间对齐缓冲区。所有输入数据先进入缓冲区规划器每次从缓冲区里取“最接近当前规划时刻”的数据。如果某个模块的数据延迟超过200ms规划器会触发降级策略——比如把障碍物的预测轨迹替换为匀速直线运动模型或者直接忽略该障碍物的预测信息只用当前位置做保守避让。这个降级策略的触发阈值是可以配置的。我在模拟项目里把阈值从200ms调到150ms后规划器的响应更灵敏了但误刹车的频率也上升了。后来折中到180ms配合一个“连续3帧延迟才降级”的条件效果比较平衡。注意时间对齐缓冲区的深度不要设得太大。缓冲区越深规划器用的数据越“旧”在高速场景下这可能是致命的。一般建议缓冲区深度不超过3帧。3. 决策层的状态机设计与震荡抑制决策层是规划模块里最“像人”的部分。它要回答的问题不是数学优化能直接解决的而是需要一套规则和状态机来处理的离散决策。Apollo的决策层用了一个分层状态机顶层是场景状态比如“跟车”、“变道”、“路口通行”底层是具体的行为状态比如“加速跟车”、“减速跟车”、“等待变道”。3.1 状态机的层级结构顶层场景状态之间的切换条件比较严格。比如从“跟车”切换到“变道”需要同时满足相邻车道有足够的安全间隙、本车道前车速度低于期望速度的80%、变道意愿持续超过2秒。这三个条件缺一不可。我见过一些自研系统只用了“相邻车道有间隙”一个条件结果就是车辆在拥堵路段频繁尝试变道实际根本变不过去反而影响了本车道的通行效率。底层行为状态的切换就灵活得多。以“跟车”场景为例底层有“加速跟车”、“匀速跟车”、“减速跟车”三个状态。切换条件主要看与前车的时距Time Gap和相对速度。时距大于2.5秒切加速1.5到2.5秒之间切匀速小于1.5秒切减速。这些阈值不是拍脑袋定的而是根据大量实车数据标定的——时距小于1.5秒时人类驾驶员的刹车反应时间中位数大约是0.8秒加上系统延迟必须提前减速才能保证安全。3.2 震荡抑制的三种手段状态机震荡是规划模块调试中最常见的问题。表现就是车辆在两种行为之间反复横跳比如一会儿加速一会儿减速乘坐体验极差。Apollo用了三种手段来抑制震荡第一种是滞回区间。比如从“匀速跟车”切到“减速跟车”的时距阈值是1.5秒但从“减速跟车”切回“匀速跟车”的阈值是1.8秒。这0.3秒的差值就是滞回区间它防止了在阈值附近来回切换。第二种是最小持续时间。任何状态一旦进入至少保持0.5秒。这个约束在代码里是通过一个state_entry_time变量实现的每次状态切换时检查当前时间减去进入时间是否大于最小持续时间。第三种是决策平滑。即使状态发生了切换输出的决策指令也不是阶跃变化的。比如从“加速跟车”切到“减速跟车”加速度指令不是从1.0直接跳到-1.0而是经过一个0.3秒的线性过渡。这个过渡在优化层实现但决策层会把这个过渡时间作为约束传给优化层。// 伪代码示意带滞回和最小持续时间的状态切换 bool should_transition(State current, State target, double time_gap, double time_in_state) { if (time_in_state MIN_STATE_DURATION) return false; if (current CRUISE target DECELERATE) { return time_gap 1.5; // 进入减速的阈值 } if (current DECELERATE target CRUISE) { return time_gap 1.8; // 退出减速的阈值滞回 } // ... 其他状态对 }我在实际调试中发现滞回区间的宽度需要根据场景动态调整。在高速场景下时距的波动比较大滞回区间可以设宽一点0.5秒在低速拥堵场景下时距变化快滞回区间要窄一点0.2秒否则车辆对前车起步的反应会太迟钝。4. 优化层的轨迹生成从离散点到平滑曲线优化层是规划模块里数学味最浓的部分。它的任务是在决策层给定的约束下生成一条满足车辆运动学约束、舒适性约束、安全性约束的轨迹。Apollo主要用了两种优化方法基于二次规划的离散点优化和基于样条曲线的平滑。4.1 二次规划问题的构建Apollo的离散点优化把轨迹表示为一组离散的路径点每个点有x、y坐标和航向角。优化变量就是这些点的位置和航向角。目标函数通常包含三项路径平滑度相邻点之间的曲率变化最小、与参考线的偏离度尽量贴着车道中心线走、与障碍物的距离保持安全距离。约束条件包括车辆运动学约束曲率不超过最大曲率、边界约束路径点必须在可行驶区域内、连续性约束相邻点之间的航向角变化不超过阈值。这个二次规划问题的规模取决于离散点的数量。Apollo默认用20个点优化变量大约60个每个点3个变量。求解器用的是OSQP在车载计算平台上求解一次大约需要5-10ms。如果点数增加到50个求解时间会上升到30ms以上在100ms的规划周期里就有点紧张了。我试过把点数从20增加到30路径的平滑度确实有提升但求解时间的增加导致规划周期偶尔超时。后来折中到25个点配合一个“如果求解超时就退回上一帧轨迹”的降级策略整体表现比较稳定。4.2 样条曲线平滑的适用场景二次规划出来的离散点轨迹虽然满足了约束但曲率可能不连续——相邻点之间的曲率有跳变。这会导致控制模块在执行时出现方向盘抖动。Apollo在二次规划之后会再用样条曲线做一次平滑。样条曲线平滑的核心思想是用分段多项式通常是五次多项式拟合离散点保证曲率连续。五次多项式可以保证位置、速度、加速度、曲率都连续适合做轨迹平滑。但五次多项式的计算量比二次规划大而且对离散点的噪声比较敏感。如果二次规划出来的点本身抖动很大样条平滑反而会放大抖动。所以Apollo的策略是二次规划负责“粗调”样条平滑负责“精调”。二次规划的输出必须满足一个“平滑度阈值”如果离散点的曲率变化超过阈值说明二次规划的结果质量不高这时候会跳过样条平滑直接输出二次规划的结果同时触发一个“规划质量降级”的警告。提示如果你在调试时发现轨迹的曲率不连续先检查二次规划的平滑度权重是否设得太低。平滑度权重太低二次规划会优先贴着参考线走导致曲率跳变权重太高轨迹会偏离参考线太远影响通行效率。一般建议平滑度权重在0.1到0.5之间。4.3 速度规划的耦合问题路径规划完成后速度规划是另一个独立的优化问题。Apollo的速度规划把路径上的每个点赋予一个速度值优化目标是舒适性加速度和加加速度最小、效率尽量接近期望速度、安全性满足与前车的时距约束。路径规划和速度规划虽然是分开做的但它们之间存在耦合。比如路径的曲率会影响最大允许速度——曲率越大允许的速度越低。Apollo的处理方式是在速度规划之前先根据路径曲率计算一个“曲率限速”把这个限速作为速度规划的硬约束。如果路径规划出来的曲率太大导致曲率限速低于一个阈值比如10km/h速度规划会反馈给路径规划要求重新规划一条曲率更小的路径。这个反馈机制在实际调试中很容易出问题。我遇到过的情况是路径规划和速度规划互相“踢皮球”——路径规划说曲率已经最小了速度规划说限速太低没法满足时距约束。最后发现是参考线本身有问题——参考线在弯道处的曲率计算有误导致路径规划一直在试图“修正”一个不存在的曲率问题。5. 规划模块的调试工具与实战技巧规划模块的调试不像感知模块那样有直观的图像输出它的输出是一堆数字和轨迹点。如果没有好的调试工具排查问题就像盲人摸象。Apollo提供了一套基于可视化的调试工具但实际用起来还是有一些技巧。5.1 轨迹可视化的正确打开方式Apollo的规划模块可以输出多种格式的调试信息包括规划轨迹带时间戳的轨迹点序列、参考线规划器使用的参考路径、障碍物列表规划器实际考虑的障碍物、决策状态当前场景状态和行为状态。把这些信息叠加显示在同一个视图里是排查问题的第一步。我习惯用不同颜色区分参考线用灰色规划轨迹用绿色障碍物用红色决策状态用文字标注在轨迹起点。这样一眼就能看出规划轨迹是否偏离了参考线、是否与障碍物保持了安全距离、决策状态是否合理。但要注意可视化工具显示的轨迹是“规划器认为的轨迹”不是“控制模块实际执行的轨迹”。两者之间可能有偏差因为控制模块有自己的跟踪误差。如果发现可视化轨迹没问题但车辆实际表现异常问题可能出在控制模块而不是规划模块。5.2 日志分析的三个关键指标Apollo的规划模块会输出大量日志但真正需要关注的指标不多。我总结下来有三个指标最能反映规划模块的健康状况第一个是规划周期耗时。正常情况下应该在50ms以内如果超过80ms说明优化问题的规模太大或者求解器收敛太慢。这时候需要检查离散点数量、约束数量是否合理。第二个是优化求解的迭代次数。OSQP求解器如果迭代次数超过100次还没收敛说明问题可能是不可行的约束之间互相矛盾。这时候需要检查约束条件是否有冲突比如路径边界约束和障碍物避让约束是否矛盾。第三个是决策状态切换频率。如果某个场景下决策状态每秒切换超过3次说明状态机的滞回区间设得太窄或者输入数据的噪声太大。这时候需要先检查输入数据的质量再调整滞回参数。# 从规划日志中提取关键指标的示例命令 grep planning_time planning.log | awk {print $2} | sort -n | tail -10 grep solver_iterations planning.log | awk {sum$2; count} END {print sum/count} grep state_transition planning.log | awk {print $3} | uniq -c | sort -rn | head -55.3 实车调试的注意事项在模拟环境里调好的规划参数搬到实车上往往需要重新标定。原因有几个模拟环境的感知数据是“干净”的实车的感知数据有噪声模拟环境的车辆模型是理想的实车的执行器有延迟和死区模拟环境的交通流是脚本化的实车的交通流是随机的。我在实车调试时踩过的一个坑是模拟环境里把跟车时距设成了1.2秒实车上发现这个时距在拥堵路段太激进前车稍微一减速本车就不得不急刹。后来把时距调到1.8秒同时把减速状态的触发阈值从1.5秒调到2.0秒乘坐体验明显改善。另一个坑是变道决策的时机。模拟环境里变道决策的触发条件是“相邻车道间隙大于1.5倍车长”实车上发现这个条件太宽松——1.5倍车长的间隙在高速上看起来很大但考虑到相对速度实际变道时后车可能已经逼近了。后来把条件改成“间隙大于2倍车长且后车相对速度小于5km/h”变道成功率大幅提升。注意实车调试规划模块时一定要先在一个封闭场地里做低速测试确认基本的跟车、停车、避让功能正常再上公开道路。公开道路上的交通流不会给你“重来一次”的机会。6. 规划模块与其他模块的边界与协作规划模块不是孤岛它和感知、预测、控制、地图都有明确的边界。这些边界如果定义不清就会出现“三个和尚没水喝”的局面——每个模块都以为别人会处理某个问题结果谁都没处理。6.1 与预测模块的责任划分预测模块输出的是障碍物的未来轨迹规划模块基于这些轨迹做避让决策。但预测模块的轨迹是有不确定性的规划模块不能假设预测轨迹100%准确。Apollo的做法是预测模块除了输出轨迹还输出一个“置信度”和“不确定性范围”。规划模块在避让时会根据置信度调整安全距离——置信度低安全距离就大一些。这个机制在实际运行中有一个问题如果预测模块对所有障碍物都输出低置信度规划模块就会变得过于保守频繁刹车。我遇到过的情况是预测模块的模型在雨天场景下性能下降对所有障碍物的置信度都降到了0.5以下结果规划模块把安全距离拉大到了正常值的2倍车辆在雨天变得“畏手畏脚”。后来我们在规划模块里加了一个“置信度平滑”逻辑如果某个障碍物的置信度在连续10帧里都低于阈值但它的位置和速度一直很稳定那么规划模块会逐渐提高对这个障碍物的置信度。这个逻辑基于一个假设稳定的低置信度比波动的低置信度更可信。6.2 与控制模块的接口约定规划模块输出的是轨迹控制模块负责跟踪这条轨迹。两者之间的接口有两个关键约定时间对齐和轨迹格式。时间对齐的意思是规划模块输出的轨迹点带有时间戳控制模块必须按照这些时间戳来执行。如果控制模块的执行周期和规划模块的规划周期不一致比如控制是50Hz规划是10Hz控制模块需要在两个规划点之间做插值。Apollo的控制模块用的是线性插值但线性插值会导致加速度不连续。更好的做法是用样条插值但计算量更大。轨迹格式的约定包括轨迹点的坐标系通常是车辆坐标系、轨迹点的属性位置、速度、加速度、曲率、轨迹的起止时间。这些格式如果不统一控制模块解析轨迹时就会出错。我见过一个案例规划模块输出的轨迹点用的是绝对坐标系控制模块以为是车辆坐标系结果车辆直接往反方向开。6.3 与地图模块的依赖关系规划模块严重依赖高精地图。地图提供了车道线、路口、限速、交通标志等信息这些是规划决策的基础。但地图不是永远准确的——道路施工、临时改道、地图数据过期都会导致地图信息与实际不符。Apollo的处理方式是规划模块在使用地图信息时会做一个“一致性检查”。比如地图显示前方是直道但感知模块检测到的车道线是弯的这时候规划模块会优先信任感知模块的车道线同时触发一个“地图不一致”的警告。这个警告会上报到云端用于地图更新。但在实际运行中感知模块的车道线检测也会出错。如果感知和地图不一致规划模块需要判断哪个更可信。Apollo的策略是在结构化道路上优先信任地图在非结构化道路上优先信任感知。这个策略基于一个经验结构化道路的地图更新频率高准确性有保障非结构化道路的地图数据往往比较粗糙感知更可靠。7. 规划模块的性能优化与降级策略规划模块运行在车载计算平台上算力有限。当系统负载高的时候规划模块必须能够降级运行保证核心功能不丢失。Apollo的降级策略是分级的从“完全正常”到“最小安全”中间有多个级别。7.1 计算负载的监控与响应规划模块内部有一个“负载监控器”它实时统计每个子模块的耗时。如果总耗时超过规划周期的80%比如80ms负载监控器会触发降级。降级的第一步是减少离散点的数量——从25个点降到15个点求解时间可以减少大约40%。如果耗时还是超标第二步是关闭样条平滑直接输出二次规划的结果。第三步是简化障碍物避让约束只考虑最近的3个障碍物。这些降级策略的触发阈值是可以配置的。我在模拟项目里把第一步的触发阈值从80%调到70%规划模块在负载高的时候更早降级但轨迹质量下降也更明显。后来折中到75%配合一个“连续3个周期超标才降级”的条件避免了因为偶发的耗时波动而频繁降级。7.2 传感器故障时的规划降级如果定位模块丢失信号规划模块会切换到“航位推算”模式——用车辆的速度和航向角推算当前位置。这个模式的精度会随时间下降所以规划模块会同时降低最大允许速度并增大安全距离。如果定位丢失超过5秒规划模块会触发“最小风险策略”——靠边停车。如果感知模块故障规划模块会失去障碍物信息。这时候规划模块会切换到“盲行”模式——只依赖地图信息做规划速度限制在20km/h以下并且频繁鸣笛如果车辆有鸣笛功能。这个模式的风险很高一般只在低速场景下使用。如果地图模块故障规划模块会失去参考线。这时候规划模块会切换到“自由空间规划”模式——用感知模块检测到的可行驶区域做规划不再依赖车道线。这个模式的轨迹质量会下降但至少能保证车辆不会撞上障碍物。提示降级策略的触发条件一定要做“去抖动”处理。比如定位丢失的判定不能因为单帧丢失就触发降级而应该连续丢失3帧才触发。否则在隧道等信号不稳定的场景下规划模块会频繁在正常和降级之间切换。7.3 规划失败的恢复机制规划模块偶尔会求解失败——二次规划问题不可行或者求解器不收敛。这时候规划模块不能直接输出“无轨迹”因为控制模块需要一条轨迹来执行。Apollo的恢复机制是退回上一帧的轨迹同时把车辆的速度限制在当前速度的80%并触发一个“规划失败”的警告。退回上一帧轨迹的策略在低速场景下没问题但在高速场景下可能危险——上一帧的轨迹可能已经不适合当前的路况了。所以Apollo在高速场景下会做一个“轨迹外推”用上一帧轨迹的最后几个点结合当前的速度和航向角外推一条短轨迹大约1秒的时长。这条外推轨迹的精度不高但至少能给控制模块一个“缓冲时间”让规划模块有机会在下一帧重新求解。我在实际调试中发现规划失败的频率和参考线的质量高度相关。如果参考线在弯道处的曲率计算有误二次规划问题很容易变得不可行。后来我们在参考线生成阶段加了一个“曲率平滑”步骤规划失败的频率下降了大约70%。8. 从规划模块延伸出的几个思考规划模块的调试和优化是一个长期的过程没有“一劳永逸”的参数配置。不同的道路场景、不同的交通流、不同的车辆特性都需要不同的规划策略。我在这个过程中积累了一些体会可能对正在做类似工作的同行有些参考价值。第一规划模块的很多问题根源不在规划模块本身。定位跳变、感知误检、预测不准、地图过期这些问题都会在规划模块的表现上暴露出来。如果发现规划轨迹异常先别急着改规划参数花点时间检查上游模块的数据质量往往能事半功倍。第二规划模块的参数没有“最优值”只有“最适值”。同一个参数在高速场景下表现好在拥堵场景下可能就很差。与其追求一套“万能参数”不如设计一套“场景自适应”的参数调整逻辑。Apollo的场景状态机其实就承担了这个角色——不同的场景状态对应不同的参数组。第三规划模块的降级策略比正常策略更重要。正常策略决定车辆“开得好不好”降级策略决定车辆“能不能安全停下来”。在资源有限的情况下优先保证降级策略的可靠性再优化正常策略的舒适性。第四规划模块的调试需要“数据驱动”。靠直觉调参的时代已经过去了。Apollo的规划模块支持记录大量的调试数据包括每一帧的输入、输出、中间变量。把这些数据收集起来做统计分析能发现很多靠人工调试发现不了的问题。比如某个参数在特定场景下的分布异常或者某个决策状态的切换频率在特定路段突然升高。第五规划模块的演进方向是“学习规则”的混合。纯规则的方法在应对复杂交通流时越来越吃力纯学习的方法又缺乏可解释性和安全保障。Apollo最近的版本里决策层开始引入一些基于学习的预测模型但优化层仍然保持基于规则的二次规划。这种混合架构可能是未来一段时间的主流。最后分享一个我在调试规划模块时常用的小技巧把规划轨迹和控制轨迹叠加显示。规划轨迹是规划模块“想走的路”控制轨迹是车辆“实际走的路”。两者之间的偏差反映了控制模块的跟踪误差。如果偏差在弯道处特别大说明控制模块的前馈补偿不够如果偏差在直线段也很大说明规划轨迹的曲率不连续控制模块在“追”一条抖动的轨迹。这个技巧帮我定位过好几次问题比单纯看规划轨迹有效得多。