
预测模块告诉你"别人会怎么走",规划模块要回答一个更难的问题——"我该怎么走"。
这个问题看起来简单,实际拆开来看:自车当前在什么场景?该走哪条参考线?路径怎么绕障碍?速度怎么调?每一个子问题都能单独写一篇论文。Apollo把整个规划拆成了三层:场景(Scenario)→ 阶段(Stage)→ 任务(Task),然后配合参考线和Frenet坐标系做路径-速度分离优化。
我翻完规划模块的源码,最大的感受是:这套架构在工程上极其扎实,但它的设计哲学和端到端方案完全相反——Apollo宁可写12个场景的状态机,也不愿意把决策交给一个黑盒。
这种选择有它的代价。
双层状态机:Scenario→Stage→Task

Apollo规划模块的入口在PlanningComponent::RunOnce,它调用OnLanePlanning::Plan,然后进入PublicRoadPlanner。PublicRoadPlanner不自己做规划——它把活交给ScenarioManager,由场景管理器决定当前走哪个场景。
场景层(Scenario),Apollo定义了12种以上的场景。源码里场景的优先级从高到低排列:
EMERGENCY_STOP——紧急停车 EMERGENCY_PULL_OVER——紧急靠边 VALET_PARKING——代客泊车 PULL_OVER——靠边停车 STOP_SIGN_UNPROTECTED——无保护停止标志 TRAFFIC_LIGHT_PROTECTED——有保护红绿灯 TRAFFIC_LIGHT_UNPROTECTED_LEFT_TURN——无保护左转 TRAFFIC_LIGHT_UNPROTECTED_RIGHT_TURN——无保护右转 YIELD_SIGN——让行标志 BARE_INTERSECTION_UNPROTECTED——无标志路口 PARK_AND_GO——路边启动 LANE_FOLLOW——车道跟随(默认)
ScenarioManager::Update的逻辑很直接:先检查当前场景是否完成(IsDone()),完成了就切回LANE_FOLLOW;否则按优先级从高到低遍历,调用每个场景的IsTransferable()检查是否满足切换条件。第一个满足条件的高优先级场景抢占当前场景。
每个场景的IsTransferable()实现不同。以TRAFFIC_LIGHT_PROTECTED为例,它检查参考线上是否存在红绿灯overlap——reference_line.map_path().signal_overlaps()非空就满足条件。BARE_INTERSECTION_UNPROTECTED检查的是路口没有信号灯也没有停止标志。LANE_FOLLOW是兜底场景,任何场景完成都回到它。
阶段层(Stage),每个场景内部再分阶段。这是Apollo 3.5引入的关键设计。以PULL_OVER场景为例:
Approach阶段:沿参考线接近目标停车位置 RetryApproachParking阶段:第一次停车失败,重新接近 RetryParking阶段:用Open Space Planner停车
阶段之间有明确的转移条件。Approach阶段停车失败(CheckADCPullOver返回PARK_FAIL),就切到RetryApproachParking。RetryParking阶段用Open Space Planner生成Z字形轨迹——需要前后换挡的那种泊车路径。
LANE_FOLLOW场景最简单,只有一个阶段——默认巡航阶段。
任务层(Task),每个阶段包含一组有序的Task。Apollo 6.0定义了22个Task,常用的包括:
PathBoundsDecider——计算路径边界 PathOptimization——在边界内优化路径 SpeedBoundsDecider——计算速度边界 SpeedOptimization——在边界内优化速度
Task的执行顺序在scenario_conf.pb.txt配置文件里定义。不同Stage会选不同的Task组合——停车场景不需要速度优化,泊车场景用Open Space Planner而不是常规路径优化。
这套三层架构的好处是每个场景独立维护、独立测试。坏处是场景之间有80种可能的转换关系(SafePlanner论文统计),任何一个转换条件写错都可能出问题。
参考线:规划的地基

场景选好了,下一步是生成参考线。参考线不是轨迹,它是道路中心线的平滑版本,规划器在这条线附近做横向偏移和纵向规划。
ReferenceLineProvider负责生成参考线。它的输入是Routing模块的路径和高精地图,输出是一条或多条平滑的参考线。关键步骤:
第一步,从地图提取车道段。 根据Routing给出的lane sequence,从高精地图里逐段提取车道中心线。每段车道有自己的几何描述(直线、螺旋线、弧线)。
第二步,拼接和裁剪。 相邻车道段的中心线拼接成一条连续曲线,按自车位置裁剪前方一定距离(默认约8秒行驶距离)。
第三步,平滑。 原始车道中心线在拼接点可能有曲率不连续,直接用来规划会导致控制抖动。Apollo用样条曲线(Spline)做平滑,保证曲率连续。平滑后的参考线才能作为Frenet坐标系的基准。
这里有个容易忽略的细节:Apollo不只生成一条参考线。当自车需要变道时,ReferenceLineProvider会同时生成当前车道和目标车道的参考线,存入Frame的reference_line_info_列表里。变道决策不是规划器拍脑袋做的——每条参考线独立跑一遍路径-速度优化,然后比较各条参考线上的轨迹代价,选代价最小的那条。这就是EM Planner论文里说的"多参考线策略"。
ReferenceLineInfo是规划模块最核心的数据结构之一。它把一条参考线及其上的所有规划信息打包在一起:参考线本身、路径边界、路径、速度边界、速度规划、障碍物决策。规划过程中几乎所有Task都在修改ReferenceLineInfo里的字段。
Frenet坐标系:把3D问题拆成两个2D

有了参考线,下一步是把笛卡尔坐标转换到Frenet坐标系。
Frenet坐标系的核心思想是把车辆的复杂3D运动(x, y, θ, v, a)解耦成两个独立的1D问题:沿参考线方向的纵向运动s,和垂直于参考线方向的横向偏移l。
转换函数在modules/common/math/cartesian_frenet_conversion.cc里,CartesianFrenetConverter::cartesian_to_frenet。输入是参考线上匹配点的参数(rs, rx, ry, rtheta, rkappa, rdkappa)和车辆在笛卡尔坐标系的状态(x, y, v, a, theta, kappa),输出是Frenet坐标(s_condition, d_condition),各含三个元素:位置、一阶导、二阶导。
核心公式:
横向偏移l = copysign(sqrt(dx²+dy²), cross_rd_nd),方向由参考线方向向量和车辆位置向量的叉积决定——在参考线左侧为正,右侧为负。
横向一阶导l' = (1-κᵣl)·tan(Δθ),其中Δθ是车辆航向和参考线方向的夹角,κᵣ是参考线曲率。这个公式的含义是:车辆偏离参考线越远、曲率越大,横向变化率就越快。
纵向速度ṡ = v·cos(Δθ)/(1-κᵣl),分母(1-κᵣl)是关键修正项——曲率越大、横向偏移越大,纵向速度和实际速度的差异就越大。
纵向加速度s̈ = [a·cos(Δθ) - ṡ²·(l'·Δθ' - κᵣ'l - κᵣl')]/(1-κᵣl),这个公式最复杂,但逻辑清晰:纵向加速度不仅取决于车辆实际加速度,还受横向运动和参考线曲率变化的影响。
反向转换frenet_to_cartesian在轨迹输出时使用——规划器在Frenet空间优化出路径和速度后,要转回笛卡尔坐标交给控制模块执行。
这套转换的数学基础不复杂,但代码实现里有几个容易踩的坑:
匹配点精度。 Frenet转换的前提是找到参考线上离车辆最近的点(匹配点)。Apollo的做法是先粗搜再精搜——先在参考线的离散点中找最近的,再用牛顿迭代精化。如果匹配点找偏了,后续所有的s、l计算全歪。
曲率符号。 κᵣ的符号在不同坐标系下可能不同,Apollo统一用右手定则——左转为正。如果你的参考线数据来源不一致,这里容易出bug。
1-κᵣl趋近零。 当横向偏移l接近1/κᵣ时,分母趋零,纵向速度ṡ发散。物理含义是车辆接近参考线曲率中心时,Frenet坐标退化。这个问题在急弯场景下会暴露。
EM迭代:路径和速度交替优化

Frenet坐标系把3D的(x,y,t)问题降维成了两个2D问题:(s,l)路径优化和(s,t)速度优化。但这两个问题不是独立的——路径影响速度(弯道要减速),速度也影响路径(高速不能急转)。
Apollo的做法是EM迭代(Expectation Maximization,不是那个算法的严格含义,是借用术语)。EM Planner论文里的流程:
E步(投影): 把障碍物投影到Frenet空间。静态障碍物直接投影到SL图,动态障碍物用上一帧的速度规划估计自车未来s坐标,再投影到ST图。
M步(优化): 在SL图上优化路径,在ST图上优化速度。
关键在于迭代。路径优化的E步需要上一帧的速度规划来估计动态障碍物在SL图上的位置——因为动态障碍物的位置随时间变化,你要知道自车什么时刻经过障碍物附近,才能在SL图上标出正确的交互区域。优化完路径后,M步的速度优化需要新路径来确定ST图上的障碍物边界。然后下一轮E步用新的速度规划重新投影动态障碍物。
论文给了一个例子:对向来车场景。第一轮路径优化看到障碍物,选择nudge(微绕),对应的速度需要减速;第二轮用新速度重新投影,发现减速后nudge点前移,路径需要调整;第三轮路径速度收敛。
计算复杂度:假设n个障碍物、M条候选路径、N条候选速度,总复杂度O(n(M+N))。论文报告平均耗时小于100毫秒。这是把3D问题拆成两个2D的代价——直接3D优化的复杂度随搜索分辨率指数增长,拆开则线性增长。
争论一:12个场景状态机 vs 端到端
Apollo的规划架构有一个根本假设:驾驶场景可以被穷举和分类。
12种场景,每种场景有自己的Stage和Task组合,场景之间的转换条件硬编码在IsTransferable()里。这套方案的好处是每个场景可独立测试、可解释、可调试。出问题了你能定位到具体是哪个场景的哪个Stage哪个Task出了错。
坏处是场景覆盖的完备性。12种场景够吗?SafePlanner论文统计,Apollo 7.0定义了14种抽象场景,场景之间有80种可能的转换。每新增一种场景,需要定义它的Stage、Task、转移条件,以及它与所有现有场景的转换关系。复杂度是O(n²)的。
更致命的问题是长尾场景。你在代码里没定义的场景,系统无法处理。EMERGENCY_STOP和EMERGENCY_PULL_OVER是后来加的——说明Apollo在工程实践中遇到了原来没预见的紧急场景。每加一种场景,就是一次代码修改和回归测试。
端到端方案的思路完全不同:不枚举场景,让模型从数据里学习场景隐含的决策逻辑。Waymo的端到端规划器已经在无保护左转这种复杂交互场景上超越了规则方案。但端到端的问题也很明显——你没法给安全审计员解释"为什么模型选择了这条轨迹",也没法给某个具体场景写单元测试。
Apollo选择场景状态机不是不知道端到端的存在,而是在L4的安全约束下做的权衡。可验证性比灵活性更优先。但当场景数量继续膨胀,这套架构的维护成本会越来越高。折中的方向已经在出现——场景内用学习优化替代手工代价函数,场景切换用学习分类替代规则判断。场景架构保留,内部逻辑软化。
争论二:Frenet坐标系的硬边界

Frenet坐标系的前提是有一条可用的参考线。它把复杂的笛卡尔空间运动降维成沿参考线的纵向+横向运动,极大简化了优化问题。
但这个降维有代价。
急弯场景。 当参考线曲率κᵣ很大时,Frenet坐标的横向线(等s线)会互相交叉。一个简单的掉头弯,弯道内侧的等s线挤在一起,外侧稀疏。l方向的"垂直于参考线"在曲率大的时候不再是好的正交基——同一组(s,l)坐标可能对应笛卡尔空间里两个不同的位置。
公式层面,1-κᵣl趋近零时纵向速度发散。Apollo的代码没有对这种情况做特殊处理——实际工程中靠参考线平滑来避免极端曲率,但这意味着参考线平滑不只是舒适性需求,还是坐标系有效性的前提。
路口场景。 Frenet坐标系绑死在一条参考线上。但在路口里,车辆可能走多个方向——直行、左转、右转,每种选择对应不同的参考线。Apollo的多参考线策略部分解决了这个问题(每条候选路径独立生成参考线),但在路口内部的几何复杂性下,参考线本身可能不再是一条简单的曲线——它需要在多条车道的连接处拼接,拼接点的曲率连续性很难保证。
横向大偏移。 变道过程中,自车的横向偏移l可能超过一个车道宽度。此时Frenet坐标系的一阶近似(l远小于参考线曲率半径)开始失效。l' = (1-κᵣl)·tan(Δθ)里的κᵣl修正项变得显著,横向和纵向的耦合增强,路径-速度分离优化的前提被削弱。
这些问题在学术圈已经有替代方案。直接在笛卡尔空间或BEV空间做规划,不依赖参考线降维。CUDA优化让直接3D搜索的计算量也可接受了。但Apollo的选择是工程导向的——Frenet坐标系在90%的结构化道路场景下够用,剩下10%用场景状态机特殊处理。这是务实的,但不是最优的。
我的判断
Apollo的规划模块是我读过的工程化程度最高的自动驾驶规划代码。三层状态机把复杂决策拆成了可管理的单元,Frenet坐标系把3D优化降到了2D,EM迭代把路径和速度耦合变成了可收敛的交替优化。每一个设计决策都有明确的工程理由。
但问题也很清楚。场景状态机的膨胀速度比大多数人的预期快——从6.0到7.0,场景数量从8个涨到14个,每个新场景意味着一套新的Stage/Task/转移条件。Frenet坐标系在非结构化道路上力不从心,而自动驾驶的落地场景恰恰包含大量非结构化道路。
下一代的规划架构大概率不会再是纯场景状态机。场景级别的判断交给学习模型,底层优化保留结构化的约束求解,参考线从刚性降维工具变成弹性引导。这个过渡已经在发生,只是Apollo的工程惯性让它走得不快。
下一篇我会拆EM Planner和Lattice Planner的具体优化算法——路径DP+QP和速度DP+QP到底怎么跑的。
源码路径:modules/planning/
夜雨聆风