1. 端到端轨迹预测在实车上的三个失效瞬间
候选路径生成这套思路,我在几个不同规模的自动驾驶项目里来回折腾过好几轮,最后发现它真正解决的问题,并不是"预测得更准",而是"预测错了之后系统还能不能活"。端到端轨迹预测——也就是把感知结果、地图栅格、自车状态一股脑塞进一个大网络,直接回归出未来若干秒的轨迹点——在论文指标上往往很漂亮,但只要把车放到真实路口跑上几天,就会开始暴露问题。这一节我不想先讲方法,先把三个我在实车上反复遇到的失效瞬间摆出来,因为只有先承认这些瞬间,后面的架构选择才有依据。
1.1 输出只有一条线,但世界本来就有无数种可能
端到端模型最常见的输出形态,是未来 T 秒内 N 个时刻的 (x, y) 坐标序列,或者再加上一个协方差。问题在于,路口那辆等待左转的对向车,它的真实意图可能是"抢在直行车之前过去",也可能是"老老实实等一个空档",这两种意图对应的轨迹差异可能有十几米,但模型只能吐出一条。哪怕训练时用了多模态头输出 K 条,最终上车时也往往要选一条执行,选择的那一刻就把不确定性丢掉了。
我在一个城市路口的场景里统计过:同一辆对向车,在感知输入几乎一致的连续 5 帧里,端到端模型输出的横向位置预测抖动范围能到 1.2 米以上,而这 1.2 米在规划侧会被放大成完全不同的让行判断。更麻烦的是,这种抖动不是随机噪声,它和模型内部的注意力权重漂移有关,你没法用简单的低通滤波压掉,压狠了又会引入滞后。
1.2 单高斯假设把多模态吃掉了
很多端到端方案为了表达不确定性,会让网络额外输出一个方差,或者干脆用混合密度网络输出若干个高斯分量。理论上是多模态,实际训练下来经常退化成"所有分量挤在一起",或者"只有一个分量拿到梯度,其余分量饿死"。这是混合密度网络的老毛病,我在三个不同规模的数据集上都复现过:分量数从 3 加到 6,有效分量数往往还是 2 个左右。
原因不复杂。真实驾驶数据里,同一个场景的多模态标注极度不均衡——大部分时候驾驶员的动作是"跟着前车匀速走",只有极少比例是"变道"或"急刹"。模型很快学会用一条"平均轨迹"把所有情况糊过去,损失函数也能降得很低,因为平均轨迹在 L2 意义下确实是最优解。但对规划来说,这条平均轨迹恰恰是最危险的东西:它可能穿过两辆车之间的缝隙,而那个缝隙在物理上并不存在。
1.3 出错之后没人说得清为什么
这一条是我最看重的。端到端模型在某个场景出了偏差,你要定位原因,就得做特征归因、注意力可视化、梯度回传,一整套流程下来可能花掉一周,最后得到的结论还是"某几个通道权重偏高"。而实车问题排查是按小时计的——今天下午这个路口必须复现并修掉,明天还有一批新场景要跑。
候选路径生成这条路子,最大的隐性收益就在这里:候选是显式的、可枚举的、可以逐条打印出来的。哪条候选被选中、它的代价是多少、每一项代价分量占多少、被淘汰的候选是因为压线还是因为碰撞——这些信息全都在日志里,工程师打开一看就知道问题出在候选集没覆盖到,还是打分器判断失误。这个可调试性上的差距,比精度差距重要得多。
我的经验是:在做预测模块的技术选型时,先把"出问题后平均定位时间"作为一个硬指标列进去。很多看起来很先进的方案,就死在这个指标上。
2. 候选路径生成的本质:把预测问题重写成枚举加排序
理解了上面三个失效瞬间,候选路径生成的定位就清楚了。它不是在预测"别人会怎么走",而是在回答"我自己可以怎么走,以及哪一种走法在当前局面下最合理"。这个视角的转换看起来很微妙,但它把一个问题拆成了两个都可以独立验证、独立改进的子问题。
2.1 从"猜一个答案"到"备一桌菜"
候选路径生成的核心流程可以概括成三步:枚举出一族物理可行、覆盖合理的候选轨迹;对每条候选做代价评估或打分排序;选出最优的一条交给控制。整个过程没有任何一步要求模型"猜对"未来,它只要求候选集足够全、打分足够合理。
这个思路的好处在于容错结构变了。端到端方案是单点故障——预测错,规划就跟着错。候选方案是冗余结构:只要候选集里存在一条安全可行的轨迹,哪怕它不是最优的,系统就能活下来。我在实车上做过对比,在那些"前车突然减速+侧方有车切入"的复合场景里,端到端方案的失效表现为"直接输出了一条会碰撞的轨迹",而候选方案的失效表现为"选了一条保守但安全的减速轨迹,只是乘坐体验差一点"。这两种失效的后果完全不在一个量级上。
当然,代价也是有的。候选方案的性能天花板由候选集的覆盖率决定——如果真值轨迹根本不在候选里,打分器再聪明也没用。所以这套方法的所有功力,一半在候选怎么造,一半在分怎么打。
2.2 候选空间的三种构造路线
候选的造法我实践过三种,各有各的适用场景,实际项目里经常是混着用。
第一种是参数化采样,在 Frenet 坐标系下对横向偏移和纵向速度做网格采样,每条候选用多项式连接当前状态到采样终点。这是最经典也最稳的做法,物理约束好施加,生成速度快,缺点是候选数量随维度指数增长,而且采样点之间的"缝隙"里可能藏着真正的最优解。
第二种是锚点式(anchor-based),预先离线聚类出一批典型轨迹模板,线上根据当前状态做仿射变换生成候选。这种做法在结构化道路上(高速、城市主干道)效果很好,候选数量少、质量高,但遇到施工区、非结构化路口就容易失效,因为模板库里没有对应的形状。
第三种是学习式生成,用一个小网络直接从场景特征里解码出 K 条候选轨迹,相当于把端到端模型的多模态输出降级成"候选提供者"。这条路线近年来越来越主流,因为它能用上数据里的驾驶先验,候选的分布更接近真实驾驶行为。但它也继承了端到端模型的部分毛病——候选可能不满足运动学约束、可能互相高度重复、可能有候选穿墙。所以实际用的时候一定要加后处理。
下面这张表是我自己整理的经验对照,参数值是常见设置,具体项目还得按场景调:
| 构造路线 | 典型候选数 | 单帧耗时(参考) | 覆盖率表现 | 主要风险 |
|---|---|---|---|---|
| 参数化采样 | 30~80 | 1~3 ms | 结构化道路好,路口一般 | 维度爆炸、缝隙漏解 |
| 锚点式 | 12~30 | <1 ms | 高速/主干道优秀 | 长尾场景模板缺失 |
| 学习式生成 | 6~16 | 3~8 ms | 分布外场景不稳 | 不满足运动学约束、候选重复 |
| 采样+学习打分 | 40~100 | 5~12 ms | 综合最好 | 打分器训练成本高 |
2.3 打分器才是整个范式的重心
很多人第一次接触候选路径生成,会把注意力全放在"怎么生成候选"上,觉得候选越多越好。我一开始也是这么想的,直到有一次把候选数从 45 加到 200,实车表现反而变差了。原因很简单:候选变多之后,打分器的任务从"从 45 个里挑 1 个"变成了"从 200 个里挑 1 个",难度上升,而训练数据量没变,泛化能力跟不上,选错的概率反而升高。
所以真正决定这套范式上限的,是打分器。它要同时处理几类相互冲突的信号:安全性(不能碰撞、不能压线)、舒适性(jerk 小、向心加速度小)、效率(别无缘无故慢下来)、合规性(遵守车道和限速)、一致性(别每帧都换一条完全不同的轨迹)。这五个目标在具体场景里经常互相打架,比如"安全"要求你减速,"效率"要求你保持速度通过。
我见过两种主流的打分器实现。一种是手工设计的代价函数加权求和,每一项都有明确的物理含义,权重靠标定。另一种是学习式打分,用一个小网络(MLP 或梯度提升树就够,不必上大模型)输入候选特征,输出一个标量分数或排序。前者可解释、可调、可控,后者上限更高但需要大量带标注的驾驶数据。
实际项目里我用得最多的是混合式:手工代价函数做主项,保证基本安全和合规;学习式打分做补充项,主要用来捕捉那些规则写不出来的驾驶习惯,比如"这个路口左转时稍微往内侧压一点更自然"。这个搭配的好处是,即使学习部分失效,规则部分还能保证车不会撞。
3. 一条可落地的候选路径生成链路
讲完思路,落到具体实现。这一段我按实际代码里的顺序走一遍,把每一步的意图、参数来源和踩过的坑都说清楚。整条链路假定已经拿到了参考线(来自路由或车道中心线)、自车状态和动态障碍物预测框。
3.1 坐标系与参考线:先决定在哪张纸上画
第一步一定要把问题搬到 Frenet 坐标系下。所谓 Frenet 坐标系,就是把笛卡尔坐标 (x, y) 换成一维纵向 s(沿参考线走的弧长)和横向 d(偏离参考线的距离)。这个转换的价值在于把"沿着路走"和"横着挪"这两件事解耦了——纵向负责速度规划,横向负责车道选择,两者可以分别采样再组合。
参考线的选取有个容易忽略的细节:参考线不一定是车道中心线。在路口、匝道汇入区、车道数变化的地方,用固定车道中心线会导致候选大量生成在不可行驶区域。我的做法是用一条"引导线",它可以是车道中心线的平滑拼接,也可以是上一次规划的轨迹外推。引导线的平滑性直接影响 Frenet 转换的数值稳定性——如果引导线曲率突变,同一个笛卡尔点转换出来的 d 值会跳变,候选就乱了。
还有一个坑:Frenet 转换在参考线曲率大的地方不唯一。一个笛卡尔点可能对应多个 (s, d) 组合,尤其是回头弯或者参考线自身交叉的时候。处理办法是限制 s 的搜索范围(比如只搜索当前 s 前后 80 米),并且在转换时保留上一帧的 s 做局部连续性约束。这个细节不处理,车在急弯里会莫名其妙地"跳车道"。
3.2 采样参数怎么定:把物理约束翻译成格子
采样维度的设计是整个链路的骨架。我一般用三个维度:横向终点偏移、纵向终点速度、时间跨度。
横向偏移的格子不能随便定。常见车道宽度 3.5 米,如果按 0.5 米一档采,一个车道内就有 7 个候选,跨三个车道就是 21 个,再加上跨车道过程中的中间位置,候选数直接爆炸。我的经验是以半车道为基本步长:[-1.75, -0.875, 0, 0.875, 1.75]米,五个点,正好覆盖"保持当前车道中心""偏左半个车道""偏右半个车道""换到相邻车道中心"。这五个点在绝大多数结构化场景下够用,而且相邻候选之间的差异足够大,不会让打分器陷入两难。
纵向速度维度我一般用相对当前速度的偏移:{-2, 0, +2}m/s,再额外加一条"减速到停止"的特殊候选。加这条特殊候选是有讲究的——它是整个候选集的安全兜底。任何场景下,只要有一条"平缓减速到停"的候选存在且无碰撞,系统就永远有一个可行解。我在一个拥堵场景里吃过亏:所有候选都是"保持或加速",结果前方突然静止,候选集里没有一条能刹住,规划器只能被迫输出一条超调的急刹轨迹。
时间跨度我一般取{3, 5, 8}秒三档。短时距用于快速响应,长时距用于判断是否值得变道。三档乘起来,5 × 3 × 3 = 45 条基础候选,这是个比较舒服的数量级——覆盖率够,打分器也不至于挑花眼。
3.3 五次多项式求解与曲率校验
确定了起点和终点状态,中间怎么连?横向我用五次多项式,因为它能同时约束位置、速度、加速度六个边界条件,保证起终点都不出现加速度突变。
具体来说,横向位移 d(t) 满足:
d(t) = a0 + a1·t + a2·t² + a3·t³ + a4·t⁴ + a5·t⁵六个系数由六个边界条件解出:起点的 d、d'、d'',终点的 d、d'、d''。终点速度一般设 0(横向不持续漂移),终点加速度也设 0。求解就是个 6×6 线性方程组,跑一次不到微秒级,可以直接解析求逆矩阵,不用调库。
纵向我用四次多项式(jerk 最小的解),边界条件是起点的 s、s'、s'' 和终点的 s'、s'',终点 s 由时间跨度和速度曲线积分得到。
这里必须做的一步是曲率校验。五次多项式在数学上很光滑,但物理上可能要求车辆做出超出能力的转弯。曲率的近似计算是κ ≈ d''/(1 + d'²)^1.5,在小横向速度假设下可以简化为κ ≈ d''。然后算向心加速度a_y = v²·κ,和轮胎附着极限做比较。一般乘用车舒适域取 3 m/s²,极限取 6~8 m/s²(视路面附着系数而定),超过舒适域上限的候选直接淘汰。
举个具体算例:起点横向偏移 0,终点偏移 3.5 米,时间 3 秒,起终点横向速度、加速度都为 0。这个五次多项式的解是标准的 smoothstep 形式,峰值横向加速度出现在 t/T ≈ 0.211 和 0.789 处,值为5.77 × d_end / T²。代入得5.77 × 3.5 / 9 ≈ 2.24 m/s²,在舒适域内,可行。如果把时间压到 2 秒,就变成5.77 × 3.5 / 4 ≈ 5.05 m/s²,已经接近极限了——这就是为什么急变道会让人难受,也是为什么短时距的变道候选必须谨慎保留。
这里有个容易漏的点:曲率校验要看整条候选的最大值,不是终点值。多项式在中间段的曲率往往比端点大,只看端点会漏掉危险候选。
3.4 代价函数的分项设计与权重标定
候选生成完之后,每条候选都要过一遍代价函数。我的分项设计大致是这样:
| 代价项 | 物理含义 | 归一化方式 | 典型权重 |
|---|---|---|---|
| J_jerk | 横向+纵向 jerk 平方积分 | 除以参考值 10 m²/s⁵ | 0.8 |
| J_offset | 终点横向偏移绝对值 | 除以半车道 1.75 m | 0.5 |
| J_speed | 终点速度与期望速度之差 | 除以 5 m/s | 0.6 |
| J_clearance | 与障碍物最小距离的软惩罚 | 距离低于 2 m 开始生效 | 2.0 |
| J_deviation | 与引导线的最大偏离 | 除以 1 m | 1.0 |
| J_acc | 最大向心加速度 | 除以 3 m/s² | 0.7 |
| J_consistency | 与上一帧选中轨迹的偏差 | 除以 1 m | 0.3 |
权重怎么定?这是最花时间的部分。我试过三种办法:纯手工拍、网格搜索、用专家示范做逆最优控制拟合。第三种听起来最高级,但实际最难落地,因为逆最优控制需要假设专家的行为是最优的,而真实驾驶员经常做出"次优但习惯性"的动作。
我现在主要用场景回放 + 分场景网格搜索。做法是把历史数据切成场景片段,按路口、跟车、变道、拥堵分成几类,每类单独搜权重,最后取一个折中的全局权重加上少量场景自适应项。搜索空间不用太大,每个权重在 0.1 到 3 之间取 7 个档位,六七个参数用坐标下降法跑几百轮就收敛了,比全网格快得多。
有个经验:权重千万不要在单一场景上调到完美。我早期在一个高速跟车场景里把 J_clearance 调到 3.0,跟车距离非常舒服,结果一到城市拥堵场景,车就变成了"永远不敢往前挪"的状态。
3.5 算力预算:候选数、下采样与早停
候选方案的计算量看起来比端到端大,但只要设计得当,实际跑在车规级芯片上完全可以做到单帧 5~10 ms。几个关键手段:
第一,碰撞检测只做采样点,不做连续曲线。每条候选按 0.2 秒间隔采样,8 秒就是 40 个点,每个点和障碍物的预测框做一次矩形相交判断,用分离轴定理,单次判断大约几十个浮点操作。45 条候选 × 40 点 = 1800 次判断,现代 CPU 上跑一遍在毫秒级。
第二,分级淘汰。先用最便宜的规则筛(曲率超限、出界、压线),再用中等成本筛(与静态障碍碰撞),最后才用完整代价函数。我实测下来,第一轮能淘汰 40%~60% 的候选,整体耗时能降一半以上。
第三,学习式打分器做批量推理。如果打分器是 MLP,把所有候选的特征堆成一个矩阵一次前向,比逐条调用快一个数量级。输入维度一般就是二十来维,隐藏层两三层、每层 64 到 128 个单元,单次批量推理在 1 ms 以内。
第四,热启动与增量更新。相邻帧的候选集高度相似,可以复用上一帧的代价计算结果做初值,只对状态变化超过阈值的候选重新算。这一招在跟车场景里效果特别好,能把耗时压到 2~3 ms。
4. 端到端和候选路径生成不是二选一
我在几个技术评审会上都遇到过这个问题:你搞候选路径生成,是不是说明你不认可端到端?不是。这两者解决的是不同层面的问题,实际项目里最优解往往是混合架构。这一节讲讲怎么把它们拼起来。
4.1 用学习模型当候选生成器
端到端模型的多模态输出头,天然就是一个候选生成器。它吐出来的 K 条轨迹,正好可以当作学习式候选。相比手工采样,它有明显的优势:这些候选的分布贴合真实驾驶行为,在复杂交互场景里能给出人工设计想不出来的走法。
比如无保护左转,人工采样的候选往往是"横向偏移 + 纵向速度"的网格组合,很难表达"先向前试探性挪一点、观察对向车反应、再决定是否通过"这种分阶段的动作。而学习式候选见过大量类似场景,能直接解出这种带试探性的轨迹。
但直接拿来用会出问题。我遇到过三种典型情况:候选不满足运动学约束(曲率突变,控制器跟不了)、候选互相重复(K 条轨迹挤在一个小邻域,等于没有多模态)、候选穿墙(网络没学会地图约束)。所以学习式候选一定要进一个校验层,做曲率检查、去重、与静态地图的碰撞检查,不合格的直接丢弃或者投影到可行域。
4.2 用规则与优化当兜底层
反过来,纯候选方案在极端场景下也会"候选枯竭"。我印象最深的是一个窄路会车场景:路宽只够一辆车,两侧都是静态障碍,所有采样候选都判定碰撞,候选集空了。
处理这种局面的办法有两个层次。第一层是保证候选集里永远有一条保底候选——就是前面提到的"平缓减速到停",它只依赖纵向运动,横向不动,几乎在所有场景下都可行。第二层是在候选全部失效时降级到优化求解,用一条简化的纵向速度优化(比如 IDM 跟车模型或者简单的模型预测控制)生成一条兜底轨迹,同时上报故障。
这两层保底不是可选项,是必须的。我在评审代码时,第一件事就是检查有没有兜底分支。没有兜底分支的规划模块,不管候选生成得多漂亮,我都会打回去。
4.3 混合架构里最容易忽略的接口约定
把学习式候选、采样候选、兜底候选拼在一起,接口上有几个约定不写清楚会出大事。
第一个是归一化基准。学习式候选输出的坐标可能基于自车坐标系,采样候选基于 Frenet 系,兜底候选直接在全局系。混在一起打分之前必须统一到一个坐标系,一般统一到当前帧的自车坐标系。这个转换看着简单,但坐标系定义的时间戳差一帧就会导致轨迹整体偏移。
第二个是时间戳对齐。学习式候选往往有推理延迟(比如 30 ms),它输出的轨迹起点是"上一帧的自车位置",而采样候选起点是"当前帧位置"。混用之前必须把学习式候选按自车运动做一次运动补偿,把它推演到当前时刻。这一步不做,车在高速行驶时会出现明显的横向偏移(30 ms × 30 m/s ≈ 0.9 米)。
第三个是候选的元信息。每条候选要带上"来源""是否满足约束""原始得分"这些字段,方便出问题时追溯。我要求团队里所有候选必须带 provenance 字段,这在实际排障时省了大量时间。
5. 真机跑通之后才会遇到的坑
离线指标全绿、仿真里跑得飞起,一上实车问题就来了。这一节列几个我踩得比较深的坑,都是那种"文档里不会写、但一定会遇到"的类型。
5.1 覆盖率评估:别只盯着 Top-1
候选路径生成的评估体系里,有个指标比 Top-1 精度更重要:候选覆盖率,也就是真值轨迹能被候选集中某一条覆盖的比例(一般用平均位移误差小于 2 米作为"覆盖"判据)。这个指标直接决定了这套方案的天花板——覆盖率是 60%,意味着 40% 的场景里你无论怎么打分都不可能选对。
我早期犯的错是只看 Top-1,觉得精度到 85% 就不错了,结果查了一个月的 bad case,最后发现大部分问题根本不在打分器,而在于那 15% 的失败里有三分之二是因为候选集压根没覆盖到真值。换句话说,优化方向找错了。
覆盖率怎么看?我的做法是画覆盖率-候选数曲线:横轴是候选数,纵轴是覆盖率。正常情况下曲线应该先陡后平,如果到 100 条还不饱和,说明采样维度的设计有问题,可能是某些方向没采到,或者格子太粗。另一个诊断是画覆盖率的时间分布:预测时域前 2 秒覆盖率高、后面陡降,说明长时域的候选形状不够丰富,需要增加时间跨度的档位或者引入非线性更强的轨迹族。
5.2 打分器会悄悄学坏驾驶习惯
用人类驾驶数据训练打分器,最大的隐患是它学到的是人类怎么开,而不是怎么开安全。人类驾驶员普遍存在跟车偏近、变道不打灯提前量不足、路口抢黄灯这些行为。如果训练数据里这类行为占多数,打分器就会认为"近距离跟车"是好行为,因为它见过太多次了。
我在一个项目里就遇到过这个:学习式打分器上线后,跟车距离比原规则版本短了将近 30%,虽然没出事故,但前车一个急刹就触发了 AEB。查下来是训练数据里大量跟车样本的时距都在 1.2 秒左右,模型把这个当成了"正常"。
解法有几种。一是在训练标签上做安全约束重加权,对时距过短的样本降权;二是把安全相关的项(碰撞、最小间距)从学习式打分里剥离出来,继续用硬规则,学习式只负责舒适性和习惯性这些软指标;三是加一个安全门控,任何得分再高的候选,只要碰撞时间低于阈值就直接否决。我现在基本都用第三种,简单粗暴但有效。
5.3 帧间抖动比单帧精度更影响体验
候选方案有个天然的抖动来源:每帧独立求解,最优候选可能在相邻帧之间发生切换。如果两条候选的形状差异较大,切换的瞬间车辆就会感受到一次方向或加速度的突变。
我在实车数据里量化过这个现象:在直道跟车场景下,即使每帧都选到"最优"候选,相邻帧选中轨迹在 3 秒时域末端的横向位置差异也能达到 0.3~0.8 米。控制器跟这种轨迹,方向盘会持续小幅修来修去,乘客的感受就是"车在画龙"。
处理办法是加帧间一致性代价(前面代价函数表里的 J_consistency),惩罚与上一帧选中轨迹的偏离。但权重不能太大,太大会导致该变道的时候不变道。我一般设在 0.3 左右,并且做成场景自适应:直道跟车时权重高(抑制抖动),变道意图明确时权重降到 0.05(允许切换)。
另一个手段是轨迹热启动加平滑输出。规划输出的轨迹不是直接给控制器,而是先过一层平滑(比如用样条拟合选中轨迹的采样点),再按控制周期重采样。这一层能吃掉大部分高频抖动,代价是引入 50~100 ms 的相位滞后,需要在下游的控制器里做补偿。
5.4 候选枯竭的几种隐蔽形式
前面说过候选枯竭会导致规划器无解,实际遇到的枯竭形式比想象中隐蔽。
形式一:所有候选都"轻微碰撞"。窄路、施工区、密集车流里很常见。这时候不能简单地选"碰撞最轻的那条",而要区分是"真的过不去"还是"障碍物预测框画大了"。我的做法是先做一轮保守预测松弛,把障碍物的预测框按置信度缩一圈(比如缩 15%),看看是否有候选能通过,如果能通过就选它但降速通过;如果还是不行,才走兜底减速。
形式二:候选存在但全都不满足舒适性约束。这在急弯或者需要紧急避让时会发生。这时候要临时放宽舒适性约束,优先保证安全。具体做法是把向心加速度的软约束上限从 3 m/s² 临时提到 5 m/s²,同时在日志里打标记,事后统计这类场景的比例。
形式三:候选存在且可行,但打分器给出的分数全部接近。这种情况最危险,因为打分器的选择接近于随机。我加了一个分数区分度检查:如果最优候选和次优候选的分数差小于阈值(比如总代价的 5%),就判定为"决策模糊",触发保守策略——选两条候选里更保守的那条(速度更低、偏移更小)。
最后分享一个我个人的习惯:每次上车前,先把当天要跑的场景里所有"候选枯竭"的历史日志翻一遍,看看兜底分支被触发了多少次。这个数字比任何精度指标都更能反映系统的真实健壮性。兜底触发率高说明候选设计有问题,触发率是零反而要警惕,可能是兜底分支根本没被正确调用。踩过几次坑之后我越来越相信,候选路径生成这套方法的真正价值不在它上限有多高,而在于它把系统的每一条失败路径都摆在了明面上,让你能一条一条去堵。