1. 先看十年大趋势:从“能开”到“敢坐”
2015年我刚开始接触智能驾驶规划算法时,业内谈得最多的是“这车能不能自己拐弯”。到了2025年,大家讨论的已经是“这车能不能在晚高峰的窄路上自己找车位”。同样叫规划算法,这十年的内涵变化用“天翻地覆”来形容一点不过分。
规划算法在智能驾驶系统里的位置,一句话概括就是:感知告诉车“周围有什么”,规划告诉车“接下来怎么走”。它往下衔接控制,往上承接决策,是整个系统的“大脑思维层”。2015年那会儿,主流方案还在用A*、RRT这类源自机器人领域的经典搜索算法,处理的是结构化道路上的简单场景;到2025年,端到端模型、Occupancy Network、VLA模型直接冲击传统管线,规划算法的技术栈和思维模式都发生了根本性变化。
这篇文章我想以这十年亲历者的视角,把规划算法的演进脉络、核心方案背后的取舍逻辑、工程落地的坑点,以及“端到端时代传统规划何去何从”这个热门话题,一次性说透。无论你是刚入行的学生,还是想转行做规控的工程师,这篇文章都能给你一张相对完整的地图。
提示:文章里涉及的具体算法参数和实现细节,我会结合多年来在实车上跑过的经验来讲,不会只停留在纸面推导。
2. 核心算法逐个数:每个方案背后的取舍逻辑
2.1 搜索派的开山鼻祖:A与Hybrid A
先聊A*。这个算法学机器人或AI的人都不陌生——栅格地图上从起点到终点,维护一个open list和一个closed list,每次取f值最小的节点扩展,直到找到终点。它的本质是BFS的“带方向优化版”,用启发式函数(比如曼哈顿距离或欧氏距离)指引搜索方向,避免盲目扩展。
在2015年左右的智能驾驶项目里,A主要用在低速场景,比如园区通勤车、封闭场地的摆渡车。原因很简单:低速车辆可以近似看成质点,航向角约束不那么致命,栅格分辨率取0.5米甚至1米也够用。但放到开放道路的高速场景,A就露怯了——它规划出来的路径是“折线”,车辆在高速行驶时根本没法直接跟踪,因为曲率不连续、方向突变的点会让方向盘在瞬间打满,乘坐体验和安全性都无法接受。
于是出现了Hybrid A*,这可以看成是“带运动学约束的A*”。它不再把车当成质点,而是把状态扩展成 (x, y, yaw),用转向角、轴距来模拟车辆的转弯半径。斯坦福在2007年DARPA Urban Challenge夺冠时用的就是这套思路,2010年前后发表的论文直接引爆了这个方向。Hybrid A*的核心代价函数设计是门手艺活,我当时调参时印象很深:
[ f(n) = g(n) + h(n) + \alpha \cdot \text{转向惩罚} + \beta \cdot \text{挡位切换惩罚} ]
g(n)是已经走的路径长度,h(n)是用Reeds-Shepp曲线算出来的无障碍最短路径长度,转向惩罚让搜索倾向于“少打方向盘”的路径,挡位切换惩罚则避免路径上频繁前进后退。这个公式看起来简单,但三个权重系数的标定直接决定轨迹质量。我记得很清楚的例子:转向惩罚系数调太大,车辆会走“绕大圈”的路径,在窄路里直接把自己堵死;调太小,路径上全是锯齿形的小幅转向,控制端抖得不行。
A*的代码实现思路其实很固定,核心骨架如下:
def astar(grid, start, goal): open_list = PriorityQueue() open_list.put((0, start)) came_from = {} g_score = {start: 0} while not open_list.empty(): _, current = open_list.get() if current == goal: return reconstruct_path(came_from, current) for neighbor in get_neighbors(current): tentative_g = g_score[current] + cost(current, neighbor) if tentative_g < g_score.get(neighbor, float('inf')): came_from[neighbor] = current g_score[neighbor] = tentative_g f = tentative_g + heuristic(neighbor, goal) open_list.put((f, neighbor)) return None而Hybrid A*扩展节点时,不是往四邻域或八邻域扩,而是按一组离散的转向角(比如-30°、-15°、0°、15°、30°)积分出一段段圆弧,每段弧的长度由车辆速度和步长时间决定。这样扩展出来的节点天然满足车辆运动学约束,这就是“Hybrid”的含义——混合了“栅格离散”和“连续运动学”。
2.2 采样派代表:RRT与它的变体们
RRT(Rapidly-exploring Random Tree)是另一个从机器人领域引入的流派。它的思路很反直觉:在地图上随机撒点,把树向随机点方向生长,直到树连接到目标点。因为不依赖精确的栅格划分,RRT在高维空间(比如机械臂的7自由度关节空间)表现很好,理论完备性方面也有概率保证。
但把RRT原封不动搬到智能驾驶上,问题很多。最典型的是路径“过于随机”——它找得到路,但找的往往不是一条人类愿意走的、平滑的路。想象一下,一个新手司机在停车场里反复倒车、打轮,最后也停进去了,但过程让人晕车——RRT生成的轨迹有时就是这种感觉。另一个问题是,随机采样在狭窄通道场景下效率极低,树要“挤”过门缝一样的空隙,可能要采样几万个点才碰巧成功。
后来出现的RRT在渐进最优性上做了改进:每次找到新节点后,会检查它周围一定半径内的已有节点,看是否存在代价更小的连接方式,把这个节点“重新接线”(rewire)。这个操作让树的拓扑结构不断优化,理论上能收敛到最优解。在智能驾驶领域,RRT最成功的应用场景是泊车——尤其是有障碍物遮挡、通道狭窄的复杂车位,搜索类方法容易陷入组合爆炸,RRT*反而能用概率优势快速找出一条可行通道。
我跟朋友开玩笑说,A和RRT的选择有点像“严谨的公务员”和“天马行空的艺术家”——A老老实实地遍历所有可能路径,保证找到最优;RRT则靠随机采样碰运气,速度快但结果不稳定。实际工程里,很多泊车系统会先用RRT*粗搜一条可行通道,再用参数化曲线把它平滑成可执行的轨迹,两者配合效率更高。
2.3 老牌劲旅:参数化曲线与Lattice Planner
搜索和采样解决的是“有没有路”的问题,但工程上还需要解决“这条路好不好开”的问题。于是有了Lattice Planner——百度Apollo早期版本的核心规划器,也是我职业生涯早期花时间最多的一个模块。
Lattice Planner的思路可以这样理解:车在Frenet坐标系(纵向距离s,横向偏差l)下运动,规划器在s方向和时间方向上采样一系列目标状态,然后用五次多项式或三次多项式把这些目标状态连成候选轨迹,再从中挑选一条代价最低的。Frenet坐标系的妙处在于,道路的曲率被“拉直”了,横向偏差和纵向速度可以分开优化,这让轨迹的语义非常清晰——哪条轨迹更贴中心线、哪条轨迹加速更快,一眼就能看出来。
我记得当时采样参数是这样设置的:
- 纵向采样间隔:每1.0秒一个点,总共采样3~5秒的时间范围,覆盖30~80米的前方距离
- 横向偏移采样:以当前车道中心为基准,向左右各采样-3.5米到+3.5米,间隔0.5米
- 末端速度采样:从当前速度到当前速度+10 m/s,间隔2 m/s
每次规划周期,大概会生成几百条候选轨迹。代价函数包含跟车距离、偏离中心线的程度、加速度冲击度(jerk)、与障碍物的距离等多维项。选最小代价轨迹时,还要做碰撞检查——候选轨迹先经过“碰撞”筛选,剩下的才进入代价评比。
Lattice Planner最大的优点是“生成轨迹本身就是平滑的”,不需要额外的后处理;最大的缺点是“横向采样粒度有限”,在弯道多、曲率变化大的场景下,0.5米的横向采样间隔可能不足以覆盖所有安全空间。比如一个急弯处,安全走廊可能只有——我举个例子——道路边界内20厘米的余量,这时候0.5米的采样步长就可能“漏掉”最优解。
Apollo后来引入了EM Planner(Expectation Maximization Planner),本质上还是在Frenet坐标系下做DP+QP两段式优化。DP(动态规划)粗搜出一条参考线,把问题空间缩小;QP(二次规划)在这个凸空间内求解最优轨迹。EM的思路可以简单概括为“先找到大概对的路,再精确优化”,这种递进式策略大幅提升了复杂场景的求解成功率。
2.4 路径规划算法代码:一个可运行的样例
说了这么多理论,给一个可以直接跑起来感受一下的简化例子。我用一个简化的Frenet坐标系Lattice采样逻辑来展示核心思路,配置好环境就能跑通:
import numpy as np def generate_candidate_trajectory(s0, s_dot0, s_ddot0, target_s, target_s_dot, T): # 五次多项式:s(t) = a0 + a1*t + a2*t^2 + a3*t^3 + a4*t^4 + a5*t^5 a0 = s0 a1 = s_dot0 a2 = s_ddot0 / 2.0 A = np.array([ [T**3, T**4, T**5], [3*T**2, 4*T**3, 5*T**4], [6*T, 12*T**2, 20*T**3] ]) b = np.array([ target_s - (a0 + a1*T + a2*T**2), target_s_dot - (a1 + 2*a2*T), 0 # 末端加速度设为0,让轨迹更平滑 ]) x = np.linalg.solve(A, b) a3, a4, a5 = x return [a0, a1, a2, a3, a4, a5] # 示例参数 s0, s_dot0, s_ddot0 = 0.0, 10.0, 0.0 target_s, target_s_dot, T = 30.0, 12.0, 2.0 coeffs = generate_candidate_trajectory(s0, s_dot0, s_ddot0, target_s, target_s_dot, T) print("五次多项式系数:", coeffs) # 在t=1.0秒时计算纵向位置 t = 1.0 s_t = 0 for i, c in enumerate(coeffs): s_t += c * (t ** i) print(f"t={t}s时纵向位置: {s_t:.2f}m")运行这段代码,你能直观看到五次多项式如何保证“起点状态”和“终点状态”都被约束住,中间过程平滑过渡。真实工程里,还会对横向偏移做同样的多项式拟合,然后把纵向和横向合成到笛卡尔坐标系下,得到最终的XY路径点序列。
3. 算法落地的工程真相:从仿真到真车的gap
3.1 第一坑:坐标系与坐标变换
规划算法在纸面上跑得再好,上车之后第一个暴击往往来自坐标系。感知模块输出的障碍物一般在自车坐标系(原点在后轴中心,x向前,y向左),地图模块给出的是绝对坐标系下的道路信息(比如UTM坐标),而规划算法内部运算又经常用Frenet坐标系。这三者之间的转换一旦出问题,轻则轨迹抖动,重则撞上真实障碍物。
我最深的一次教训是处理“时延”问题。感知输出的障碍物位置是“过去某一时刻”的位置,而规划算法计算时用的是“当前时刻”的位置。在高速场景下(比如100 km/h),100毫秒的延迟意味着车辆往前跑了2.8米。如果不做运动补偿——也就是用障碍物的历史速度和加速度外推它的当前位置——规划出的轨迹可能已经穿过“实际上的障碍物”了。这个细节,很多仿真环境里根本不会暴露,因为仿真里的感知是“理想同步”的,但实车一定会暴露。
坐标变换的代码核心其实很简单,但方向反了就会出现完全相反的结果:
import math def global_to_vehicle(global_point, vehicle_pose): # vehicle_pose: (x, y, yaw) dx = global_point[0] - vehicle_pose[0] dy = global_point[1] - vehicle_pose[1] cos_yaw = math.cos(vehicle_pose[2]) sin_yaw = math.sin(vehicle_pose[2]) local_x = dx * cos_yaw + dy * sin_yaw local_y = -dx * sin_yaw + dy * cos_yaw return (local_x, local_y)很多新手不注意“坐标轴方向”和“航向角参考系”,写完转换之后直接在仿真里通过,一上车就发现轨迹偏移。建议的做法是:写单元测试,输入一组已知坐标,手动推导期望输出,再加一个反向变换做往返一致性校验,这样能拦截掉绝大多数基础错误。
3.2 第二坑:轨迹平滑与曲率连续性
规划算法输出的轨迹,控制模块是要直接去跟踪的。如果轨迹上的曲率不连续,控制模块的反馈会非常痛苦——方向盘会一直抖,车辆会画龙。我曾经接过一个项目,规划算法用的是纯几何样条曲线拼接,看起来挺优美,但曲率在拼接点处出现了突变,上车之后方向盘在拼接点处“咔哒”一下猛打,把测试工程师吓出一身冷汗。
解决曲率连续,业界最常用的做法是“曲率平滑后处理”。具体可以分为两步:
- 第一步:用三次样条或五次样条对原始路径点做插值,保证位置、一阶导数(航向)、二阶导数(曲率)连续
- 第二步:用非线性优化对轨迹进行“推优化”,目标函数约束最大曲率、最大曲率变化率
我习惯用的一个简化版曲率约束优化,核心思想是把相邻三个路径点确定的圆弧曲率作为状态量,限制其变化率:
[ \text{minimize} \quad \sum_{i=1}^{n-1} \left( \kappa_{i+1} - \kappa_i \right)^2 + \lambda \sum_{i=1}^{n} \kappa_i^2 ]
第一项让曲率变化尽量平缓,第二项防止曲率本身过大,(\lambda)是正则化系数,控制两项的权重。这个优化问题规模不大,用OSQP或自定义的高斯牛顿都能在几毫秒内解完。需要注意的是,平滑后的轨迹必须要重新做碰撞检测,因为平滑过程可能让轨迹“侵占”到障碍物一侧。
注意:轨迹平滑和碰撞检测是一个闭环迭代过程。平滑让轨迹更舒适,但可能破坏安全性;碰撞检测发现问题后,要重新在局部生成替代轨迹。实际工程里这两个模块往往要循环执行2~3轮才能得到既安全又平滑的结果。
3.3 第三坑:规划与控制的时间同步
规划模块的输出并不只是“一条路径”,而是一条带时间戳的轨迹——每个路径点都对应一个期望到达时刻。控制模块跟踪这条轨迹时,如果规划周期(比如100ms)和控制周期(比如10ms)之间不同步,控制模块就会一直在跟踪“过期”的轨迹点。
我记得有一次实车测试,车辆在过弯时出现明显的“一卡一卡”现象。排查了半天,问题出在规划模块输出的轨迹点时间戳用的是“规划计算完成时刻”,而控制模块读取时用的是“读取时刻”,两者之间差了将近一个规划周期的时间偏移。结果控制模块总是跟踪一个落后于实际的点,导致每周期都产生约0.3米的跟踪误差,在弯道中被放大成明显的顿挫感。
解决方式也很直接:规划模块每次输出轨迹时,不是从“当前时刻”开始,而是从一个“未来时刻”(通常是下一次控制周期开始的时间)开始。比如规划周期100ms、控制周期10ms,规划模块可以预瞄到“当前时刻+110ms”的状态作为轨迹起点,保证控制模块跟踪时看到的是“未来”的轨迹而不是“过去”的轨迹。这个偏移量的标定需要在实车上反复试,不同的传感器延迟特性、不同的控制模块响应速度都会影响最优值。
4. 泊车与无人机:两个场景的规划算法变体
4.1 泊车为什么是“低速但高难”的典型
泊车路径规划在热词里出现频率很高,这确实是个独特的场景。说它低速,是因为泊车过程中车速通常不超过5 km/h;说它高难,是因为空间极其狭小、需要反复前进后退操作,对规划的“几何可行性和多段操作”能力要求很高。
解决泊车问题,主流方案有两种路线。第一种是“搜索+平滑”:用Hybrid A*搜索一条满足车辆运动学约束的粗糙路径,再用优化方法平滑。2010年前后Stanford团队的论文已经证明这条路可行,但问题是窄小车位里搜出来的路径经常带有大量的“挡位切换”,每条小弧段长度可能只有几十厘米,平滑之后依然可能碰壁。
第二种是“数值优化”路线,把泊车问题建模成非凸优化:
[ \min_{x} \sum_{k=0}^{N} | \mathbf{x}k - \mathbf{x}{ref,k} |^2_Q + | \mathbf{u}_k |^2_R ]
约束条件包括运动学模型、转向角限制(比如±35°)、障碍物距离限制(车辆轮廓到障碍物的最小距离大于安全阈值)、路径曲率限制等。这个优化问题是非凸的,不能直接求全局最优,一般用SQP(序列二次规划)或罚函数法迭代求解。工程实战中,通常会先用Hybrid A*生成一条初始可行解作为优化起点,再用优化方法把路径“磨”得更加平滑、贴边。这叫“热启动”——好的初始解能大幅提升优化收敛速度和结果质量。
泊车场景的另一个特点是“终点姿态约束严格”——最终车辆必须精确停入车位正中,且航向角要与车位方向一致。这个约束在优化问题里必须作为硬约束处理,否则车辆可能停在歪斜的位置,虽然也能入库,但用户体验极差。
4.2 无人机路径规划:同一个家族的不同分支
无人机路径规划算法和智能驾驶规划算法经常并列出现在讨论里,很多人以为两者是同一套技术。“它们同宗但不同路”,我来具体说说。两者的底层技术栈确实有交集——A*、RRT、优化算法在无人机上也很常见,但约束条件差异巨大:
- 维度差异:无人机是三维空间运动,智能驾驶基本是二维平面运动(忽略坡道时)。三维空间的状态空间大得多,搜索算法的“组合爆炸”问题更严重
- 运动学差异:无人机(尤其是多旋翼)可以悬停、垂直起降、任意方向飞行,没有非完整约束——它不需要像汽车那样考虑转弯半径和倒车;但无人机有动力学约束:加速度、速度、姿态角速率限制,轨迹规划必须考虑“能不能飞出来”
- 安全空间差异:智能驾驶的安全距离是厘米级的,无人机在城市峡谷等场景的安全距离通常是米级的
无人机领域最常用的规划算法是“混合A*的采样 + 多项式轨迹优化”——先粗搜一条平滑轨迹,再用Minimum Snap(最小加加速度)优化得到动力学可行的轨迹。Minimum Snap的思想是让四旋翼在航点之间飞行时,四个电机推力的变化率尽量小,这样飞得稳、不抖。它的本质是一个二次规划问题,约束条件是航点的位置、速度、加速度,目标函数是最小化加加速度的平方积分:
[ \int_0^T \left( \frac{d^3 \mathbf{r}}{dt^3} \right)^2 dt ]
这个目标函数在数学上的好处是:它是凸的,求解快,而且得到的轨迹非常平滑。但缺点是需要预先知道航点的位置和时间分配——如果时间分配不合理,轨迹会很“别扭”,比如某段飞行时间过长导致速度过低,另一段又太短导致加速度过大。工程上通常会在轨迹求解后检查速度和加速度是否超限,如果超限就增大对应航段的时间,重新求解。这个过程叫“时间重分配”,是无人机轨迹规划里一个重要但常被忽略的环节。
5. 端到端的冲击:传统规划算法会被淘汰吗
5.1 端到端到底改变了什么
2023年以来,端到端(End-to-End)成为智能驾驶领域最热的话题。端到端模型把“感知-预测-规划”压缩成一个神经网络,输入多摄像头图像(有时加激光雷达点云),直接输出控制指令(方向盘转角、油门、刹车)或轨迹点。这个思路的本质是“用数据驱动替代规则驱动”——传统的规划算法本质上是一堆人为设定的规则和公式,端到端模型则是从海量驾驶数据中“学习”出规划策略。
端到端对大算力的需求非常夸张。一个典型的端到端模型,比如基于Transformer架构的驾驶模型,参数量通常在数亿到数十亿级别,训练需要上千张A100/H100 GPU。训练数据则是数十万小时的真实驾驶视频和对应的控制指令。这种规模让很多中小团队望而却步,也让“数据闭环”成为行业竞争的真正壁垒。
但端到端不是银弹。它最大的问题是“可解释性差”——模型在某种场景下做了一个奇怪的决策,工程师很难说清楚是哪里出了问题。这带来了两个工程层面的困难:一是安全验证变得困难,传统规划算法可以用形式化方法穷举验证,端到端模型则只能依赖大量测试;二是“长尾问题”处理困难——模型在训练数据稀疏的场景(比如极端天气、非常规障碍物)下行为不可预测。
5.2 传统规划工程师的生存之道
我身边不少做传统规划的同行,在2024年那波端到端热潮中感到焦虑,担心自己积累多年的A*、Lattice、QP优化经验一夜之间变成废纸。但其实,“传统规划技术不会消失,只会换一种形态存在”。
原因很简单:端到端模型输出的是轨迹或控制指令,但系统的其他模块仍然需要大量“传统”技术。首先,模型本身需要监督信号和数据标注——标注数据时通常需要参考传统规划器的输出。其次,端到端模型输出的轨迹往往不够平滑,需要后处理模块用样条插值、曲率约束优化进行修正——这些正是传统规划的核心技术。最后,安全兜底机制不可或缺——当端到端模型输出异常时,系统需要一个“安全护栏”(比如RTK(Real-Time Kinematic)边界限制、紧急制动策略),这些也都依赖传统规则算法。
从更宏观的视角看,端到端解决的是“感知信息如何变成驾驶行为”的映射问题,而规划算法解决的是“在约束条件下如何生成最优轨迹”的优化问题。后者本质上是一个数学问题,跟感知用什么模型没有直接关系。哪怕感知端完全变成端到端,规划端的优化框架依然成立。
我给同行们的建议是:不要丢掉传统规划的基本功,但要主动学习端到端的思维模式。一个既能写QP优化,又能理解Transformer注意力机制的工程师,在未来两三年内会非常抢手。
6. 给想入行的人的一点建议
很多读者问我,2025年了,想入行智能驾驶规划算法,应该从哪里入手。
我的建议是先动手把经典算法的代码写一遍,不要停留在“看懂了”的层面。“看懂了”和“能上手调参”之间隔着无数个细节。A的open list用什么数据结构?Hybrid A的Reeds-Shepp曲线怎么生成?Frenet坐标和笛卡尔坐标的转换公式怎么推导?这些不看代码自己实现一遍,永远不会真正理解。
第二个建议是“把仿真当玩具,把实车当真考验”。再好的仿真环境也模拟不了真实的传感器噪声、通信延迟、机械执行误差。如果条件允许,尽量参加实车测试——哪怕只是坐在副驾观察问题,也比只看仿真日志收获大得多。我在实车上看到的一次“轨迹突然向左猛拐”,比读一百篇论文更能理解安全冗余的重要性。
第三点是“保持对数学的敬畏”。规划算法表面上是代码问题,本质上是个优化问题——不论是图搜索里的代价函数设计,还是轨迹优化里的约束建模,背后都是数学。线性代数、凸优化、概率论这三门课,值得反复研读。
第四点在前面提过,现在值得强调一下:不要拒绝端到端。“不要只做传统规划,也不要只相信端到端”,两条腿走路才能在行业震荡期站稳脚跟。这十年的经验告诉我,这个行业最稳定的人,往往是那些“什么都懂一点,又有一项深耕到极致”的人。
最后,回到开头的那个比喻——规划算法这十年,从解决“能不能开”的问题,走到了解决“敢不敢坐”的阶段。技术的迭代不会停止,但底下那些“在约束条件下找最优解”的核心逻辑,会一直延续下去。