说明:
本文复盘一次城市智驾路口绑灯功能的量产级重构。文章从问题拆解开始,说明如何通过数据、地图、引擎和规控四层归因,将偶发问题变成可复现用例;再通过停止线锚定、受控 link 判断和数字灯状态建模完成架构重构,最后用四项指标建立版本回归体系。全文不涉及代码和内部名称,重点讨论工程方法和量产验证
1. 引言:为什么「路口绑灯」是城市智驾的安全红线
想象这个场景:一辆开启城市领航辅助(城市 NZP)的车,接近一个十字路口——最左侧是左转专用道,中间两条直行道,右侧一条右转道。路口对面挂着四组红绿灯:一个左转箭头灯、两个直行圆灯、一个右转灯,旁边还各配了一个倒计时数字灯。
车要做的,比「看到红灯就停」复杂得多。它必须先搞清楚一件事:
这四个灯里,到底哪一个,管的是我这条道?
这就是「路口绑灯」:把感知到的红绿灯,正确地绑定到「我所在的车道、我要去的方向、我该停的停止线」上。
这件事一旦出错,代价是致命的:
- 绑错了灯:左转道绑到了直行灯,直行绿灯一亮车就冲出去,而左转灯其实还红着——闯红灯,或更糟的事故。
- 绑慢了一拍:连续路口一个接一个,前一个路口的灯还没处理完,下一个路口的灯已经来了,系统延迟几秒——该停的没停,不该停的急刹。
在城市领航里,路口是事故率最高、体验最敏感的场景,而绑灯就是路口安全的「第一道闸门」。它错一次,后面感知、规划、控制做得再漂亮,都是在错误的假设上运行。
正因为它难、也正因为它重要,当我们最终把关键场景下的绑灯成功率和稳定性做到99.9%+,并且这套方案被采纳为集团内多个车型平台(含 Robotaxi)的标准技术基线时,中间走过的弯路和踩过的坑,值得复盘。
这篇文章我会讲清三件事:
- 一个「顽固的绑灯问题」,是怎么被拆成可定位的子问题的;
- 我们怎么用「根因定位」代替「调参碰运气」,一步步把它推到量产级;
- 为什么「评测体系」才是 99.9% 这个数字的真正底气。
2. 问题拆解:把「顽固问题」变成「可定位的子问题」
很多「顽固 bug」之所以顽固,不是因为难,而是因为它是一个含糊的整体。你没法「调一个参数」去解决一个「说不清是什么」的问题。所以我们的第一步不是改代码,而是拆。
先立一条流水线
路口绑灯,表面是一个问题,实际上是一条两段串行的流水线:
[ 绑灯 · 关联 ] ──► [ 信号认知 · 读取 ] ──► [ 下游:规划 / 控制 ] 哪个灯管我这条道 这个灯现在什么信号 据此决策- 第一段,绑灯(关联):确定「哪个灯管我这条道」。要把「灯」和「车道 / 转向 / 停止线」在空间与拓扑上对齐——连续路口、多相位、掉头 / 豁口、大曲率无灯路口,全是这一段的坑。
- 第二段,信号认知(读取):确定「这个灯现在是什么信号」——红 / 绿 / 黄、圆灯还是箭头灯、倒计时还剩几秒。绿闪误刹、数字灯误判、状态跳变,红绿灯信号遮挡,全是这一段的坑。
两段串行且耦合:先绑对灯,才能读对信号;绑错了,读得再准也是白读。反过来,绑对了、读错了(比如把绿闪当绿灯、把倒计时当方向),一样闯红灯。
很多团队的问题在于,把这两段糊成一个「红绿灯功能」,出了 bug 说不清是哪一段的锅。拆开之后,每个 bug 才能落到具体的一段,才能往下定位。
一个例子:倒计时灯的「方向」从哪来
用第二段(信号认知)里的一个真实问题,说明「拆解」到底有什么用。
路口有一类灯是倒计时灯——纯数字,显示 12、11、10……它本身不携带方向信息,你不知道它是给左转的、还是给直行的。但它通常和一个方向箭头灯配套出现。系统要回答的问题是:这个倒计时灯,代表哪个方向的信号?
最初的实现很简单:给每个灯维护一份「历史形状」记录——检测到方向箭头灯时,记下它的方向;检测到倒计时灯时,就用历史里的方向来「补」。
逻辑看起来没问题,实车一跑就翻车。问题出在状态循环上。
一个灯会经历「方向灯(绿箭头)→ 倒计时灯(绿闪数秒)→ 方向灯(红箭头)→ …」这样的循环。单靠一份「历史形状」记录,区分不了两种情况:
- 现在这个倒计时灯,是「从方向灯变来的倒计时」——应该继承方向;
- 还是「倒计时已经结束、系统误读成了别的东西」——不应该去污染历史。
结果就是实车反馈里最头疼的一类:灯变着变着,方向就「回不去了」——要么把倒计时灯永久误判成某个方向,要么方向在两次循环之间乱跳。
这个 bug 的根因,不在「识别得不准」,而在状态建模不对:用一个字段去表达一个本需要多个状态区分的时序问题。
后面我们的修复,核心一句话:把「历史形状」这一个状态,拆成能区分「当前是不是倒计时灯」「是不是刚从方向灯变过来」的多个状态,让倒计时灯永远只「借」最近一次有效的方向,而永远不「污染」它。(具体状态机,第 5 节展开。)
拆解带来的三个好处
- 问题可归因:每个 bug 都能落到「绑灯段」还是「信号认知段」,不再是含糊的「红绿灯有问题」。
- 根因可定位:像倒计时灯这种,一眼看出是「状态建模问题」,而不是「识别精度问题」。
- 方案可复用:拆成两段后,绑灯的重构方案才能被抽出来、做成跨车型的标准基线——这就是后面 99.9%+ 能复用到多个平台的起点。
下一节,讲我们最核心的方法:四层根因法——数据 → 地图 → 引擎 → 规控,以及为什么它比「调参」快一个数量级。
3. 四层根因法:数据 → 地图 → 引擎 → 规控
拆完之后,真正的硬骨头是「偶发」——不是每次都错,而是「时不时错一下,现场抓不到」。
偶发问题最坑人的地方在于:没有稳定复现,就没有根因。现场报上来一个「过某个路口突然退出」,等你派人去那个路口,车又好好的。这种时候,工程师最容易犯的错,就是凭感觉调参——「大概是这个阈值太紧了,放宽一点试试」,然后祈祷下次不出现。这不是定位,这是赌博。
我们的做法,是把每一个「偶发无效 / 异常降低」的问题,先强制归到四层之一,再往下挖:
┌────────────── 一个「偶发」问题 ──────────────┐ │ │ ① 传感器数据 ② 地图数据 ③ 引擎逻辑 ④ 下游规控 (脏数据/跳变) (拓扑/关联错) (状态机/时序) (响应/退出)- ① 传感器数据层:数据脏、跳变、丢帧、时钟不对齐。这一层的问题,会在下游「伪装」成各种奇怪的症状。
- ② 地图数据层:link 拓扑错、停止线缺失或偏移、红绿灯与车道的关联标错。
- ③ 引擎逻辑层:绑灯状态机、里程计、时序对齐——引擎自己的逻辑和状态。
- ④ 下游规控层:对绑灯结果的反应——延迟、提前退出、不该有的制动。
关键不在于这四层本身,而在于一个反直觉的规律:
症状所在的层,往往不是根因所在的层。
举一个真实例子(这类问题里最经典的伪装):里程计跳变,导致路口内功能「非预期退出」。
症状在「④ 规控」——车在路口中间,功能突然退了。乍一看是规控的退出逻辑太敏感。但真去查,会发现是里程计在某个瞬间跳了一下:车以为自己已经越过了停止线、离开了路口,于是「合法地」把绑灯结果清掉了。
而里程计为什么跳?再往下追一层,是「① 传感器数据」的问题(一次车轮打滑、或一次定位数据的毛刺)。跳变传到了「③ 引擎」的里程计,最后在「④ 规控」露出症状。
如果一开始就盯着「④ 规控」去调退出阈值,这个 bug 会陪你到量产,还会换着花样复现。而一旦定位到「① 数据」,问题就从「玄学」变成了「可修」。
三件工具,把「偶发」变「必现」
这套四层归因能跑起来,靠的是三件工具:
- 抓包复现(第一现场):问题发生的瞬间,把该帧的传感器数据、地图数据、引擎内部状态全部抓下来——这是「现场证据」,决定了后面所有分析的地基。
- 日志分析(定位到层):顺着日志,把异常一步步从「症状层」追到「根因层」。
- 数据回灌(离线复现):把抓下来的现场数据,喂回离线环境重跑。一旦离线能稳定复现,偶发就变成了必现——这时才谈得上「修复后验证」。
这三件工具组合起来的效果是:任何偶发问题,都能被钉死在某一层,并变成可复现、可验证、可回归的用例。这才是后面 99.9%+ 能站得住的原因——它不是一个「感觉上的成功率」,而是一套用真实 case 跑出来的、可回归的数字。
下一节,讲我们怎么在「③ 引擎逻辑」这一层,做了一次架构级的重构——为什么「打补丁」救不了连续路口,必须推倒重来。
4. 重构方案:把「停止线」变成绑灯的锚点
定位清楚之后,摆在面前的是一个选择:继续给旧方案打补丁,还是推倒重构。
我们的结论很明确:补丁救不了。因为旧方案的问题不在参数,而在判定的依据本身选错了。
旧方案为什么注定解不完
旧方案里,「路口判断认知」是一个单独的上游模块给出的:感知到停止线前 10 米,上游给一个「路口内」状态;然后系统靠里程计的行驶距离来判断是不是已经驶离了路口。
这套机制有三个解不完的坑:
- 连续小路口:前一个路口还没走完,系统就很容易把下一个路口的灯绑了进来——两个路口离得近,距离判定的边界是糊的。
- 里程计突变:里程计只要跳一下,「已驶离」的判定就错了,绑灯状态跟着异常失效。
- 最根本的:城市路口的范围本身就是非标的——有的路口 30 米,有的 80 米,还有不规则的。用「固定阈值的行驶距离」去判定进出路口,注定有无穷无尽的 case 解不完。这不是调参能解决的,是模型选错了。
一句话总结旧方案的病根:它用一个「推算出来的距离」去判定一个本该「感知出来的事实」。
新方案:以「停止线认知」为核心
重构的核心,是把绑灯和进出路口的锚点,从「里程计距离」换成了「停止线」。
- 先认知停止线:停止线是物理上真实存在、能被感知到的东西(相机、激光都能看到)。
- 灯绑到停止线上:红绿灯本来就控制「在这条线停」,灯 ↔ 停止线是天然的绑定关系。
- 「是否驶离路口」从「里程计走了多远」变成「是否已经通过我的停止线」——一个可观测的事件,而不是一个距离阈值的猜测。
这一步看起来简单,但它把一个「解不完的推算问题」,换成了一个「可感知、可判定的事实」。连续小路口?我只看我自己的停止线,跟下一个路口的灯没关系。里程计突变?我已经不靠里程计判断驶离了,突变影响不到绑灯状态。
第 5 节挑两个典型场景展开:一个是「受控 link」——先问这个灯到底管不管我这条路线;一个是「数字灯」——倒计时灯的方向信息从哪来。它们分别代表「要不要绑」和「读什么信号」两类问题。
5. 关键场景攻坚:受控 link 与数字灯
停止线认知解决了「绑到哪」,但落地时还有两类问题,分别卡在流水线的两端:
- 受控 link:管「要不要绑」——先确认这个灯到底管不管我这条路线;
- 数字灯:管「读什么信号」——确认要绑之后,怎么把这个灯的信号读对。
5.1 受控 link:先问「这个灯管不管我」
城市里有一类「路口」,其实不需要看灯:进环岛后的豁口、Y 字形的汇入、某些 T 型路口——车走的方向,跟路边的信号灯没有关系。
但感知系统不管这些:它只要看到了停止线和信号灯,就默认这是「真实路口」,直接绑定。结果就是误绑——车把一个「根本不管自己」的灯,当成了自己的灯。
根因很清晰:旧逻辑是**「看到灯就绑」**,缺了一道「这个灯到底管不管我这条 link」的判断。
方案是引入SD 数据(标准精度地图的 link 级拓扑),做一层判断:
- 通过 SD link 关联,确定当前行驶的 link 是不是「受控 link」——即是不是被某个信号灯真正控制的路段。
- 受控 link → 正常绑定;非受控 link → 不绑。
补上这一层后,「看到灯」从绑定的充分条件,降级为必要条件——「看到灯」+「它管我这条 link」,才绑。
5.2 数字灯:倒计时灯的方向从哪来
回到第 2 节留的那个问题:倒计时灯本身不携带方向信息,系统要靠「历史形状」来补;但单靠一份历史记录,会在「方向灯 → 倒计时灯 → 方向灯」的循环里把状态搞乱——方向「回不去」。
根因是状态建模不对:用一个字段,去表达一个本需要多个状态区分的时序问题。
修复的核心,是把「历史形状」这一个状态,拆成两个可区分的维度:
- 当前是不是倒计时灯;
- 是不是刚从方向灯变过来。
由此形成一套简单的状态转换:
- 看到方向灯→ 记下方向;
- 方向灯变成倒计时灯→ 继承「最近一次有效的方向」,但不覆盖历史;
- 倒计时灯变回方向灯→ 更新方向。
一句话:倒计时灯永远只「借」方向,永远不「污染」方向。无论方向灯怎么循环,方向都能正确恢复,不会再「回不去」。
6. 量化:99.9%+ 是怎么「量」出来的
前面讲的都是「怎么修」。但「修好了」和「感觉修好了」,是两回事。
在量产这件事上,有一条我们反复强调的原则:
不能量化的优化,是玄学。
一个 bug 修完,如果你说不出「它在哪个指标上、从多少变成了多少」,那它就不算修完——因为下一版随时可能复现,而你连「它有没有回退」都不知道。
四个指标,把「绑灯」变成可度量的东西
所以我们在量产交付小包里,围绕路口绑灯算法和感知模型的红绿灯信号处理这两条链路,基于真实路测抓回来的 case(不是合成数据),建了一套评测体系,核心是四个指标:
- 绑灯成功率:绑对的灯占所有应绑灯的比例——这是主指标,99.9%+ 说的就是它。
- 信号稳定性:同一个路口、同一个灯,算法输出的信号会不会在帧与帧之间乱跳。
- 数字灯判读:数字灯/倒计时灯读得对不对——就是第 5 节那个「借方向不污染方向」的场景,单独拎出来量化。
- 真实信号与算法输出的一致性:算法输出的信号,跟现场真实信号对得上吗——这是最终兜底的一项。
每一个版本,都在回归
评测体系的价值,不在「跑一次拿个好看的数字」,而在它能支撑版本验收与回归:
- 每个版本发布包,先跑这套真实 case 评测集;
- 任何一个改动合入前,先看这四个指标有没有回退;
- 回退了,改动就进不了版。
这才是「99.9%+ 能站得住」的真正原因——它不是一个在某个晴天测出来的峰值,而是每一个量产版本都在回归、都在守住的一个数字。
这套东西最终在某直辖市的 EX 项目「全国首通」攻关、以及后续的全国泛化测试里被反复验证,也直接支撑了城市 NZP 的成功发布。
7. 落地与复用:从单项目到集团标准基线
一次重构,如果只在某一个车型、某一个城市跑通,那它可能只是一次性的工作。真正决定它价值的,是它能不能被别的项目直接拿去用。
这次重构最终拿到了两个「被复用」的证据:
第一个证据:真实路网的全国首通验证。方案在某直辖市的 EX 项目上做了全国首通攻关,又在后续的全国泛化测试里反复验证。这意味着它不是在某条固定测试路线上「背题」,而是在没见过的城市路网上,也能稳住 99.9%+。
第二个证据:被采纳为集团标准技术基线。因为方案在「鲁棒性」和「可维护性」上足够站得住,它被集团内多个项目——不同车型、不同定位的平台,包括 RoboTaxi——采纳为标准技术基线,跨车型复用,保证一致体验。
这两个证据背后,其实指向同一件事:
架构的价值,最终体现在「别人愿意拿来复用」上。
为什么别的项目愿意复用?不是靠关系或流程硬塞,而是靠两样东西挣来的:
- 鲁棒性:停止线认知锚定的是「可感知的停止线」,不是「某个城市的固定阈值」——所以它在不同城市、不同路口形态下都能泛化,不用每个城市单独调一套参数。
- 可维护性:拆成「绑灯 / 信号认知」两段 + 四层归因 + 可回归的评测,出了问题能定位、能改、能验证——别的团队接得动、敢接。
最终,这次重构拿到了项目组颁发的「核心优秀交付成果」推荐,也直接支撑了城市 NZP 的成功发布与用户口碑。
8. 复盘与思考:这套方法,还能用多久
最后收个尾:三点复盘,加一个对行业趋势的判断。
三条复盘
根因定位永远比调参值钱。这次重构里,最难的不是写代码,是「里程计跳变」那个问题——它看起来是规控的锅,实际是数据的锅。只会调参,这种问题会陪你到量产;会四层归因,它一天就现形。
评测体系是量产的底气。99.9%+ 不是跑一次测出来的峰值,是每个版本都在回归的数字。没有评测体系,任何「优化」都是玄学,任何「修复」都可能在下个版本悄悄回退。
架构要服务复用。一次重构的价值上限,不是「解决了这个问题」,而是「别人愿不愿意拿它当基线」。鲁棒性 + 可维护性,是复用的前提,也是架构和补丁的分界线。
一个趋势判断:经典感知-融合会变,但「数据 + 几何」不会消失
这几年行业有个明显的趋势:无图化、端到端。很多过去靠「经典感知 + 融合 + 高精地图」解决的问题,正在被大模型和在线建图替代。
坦白说,路口绑灯这类「经典感知-融合」的活儿,形态一定会变——绑灯的很多手工规则,会逐渐被端到端模型吸收。
但有一件事不会变:
无论模型多强,车要安全过路口,依然需要知道「我该在哪停、哪个灯管我」。
「停止线在哪里」「这个灯管不管我这条路线」「我现在该不该停」——这些是物理世界的几何约束和语义事实,是模型绕不开的底座。端到端模型可以自己学,但学的过程仍然要真实数据、仍然要几何对齐、仍然要可量化的验证。这些东西,恰好是这篇文章从头到尾在做的事。
所以我的判断是:算法会变、模型会变,但「把空间数据和几何约束,做成可量产、可验证的系统」这件事,会一直在。
关于作者
Mainland,14 年「空间智能」研发,前吉利 / 极氪智能驾驶感知开发工程师,主导路口绑灯核心算法重构(99.9%+,集团标准技术基线)、多车型量产交付;现专注机器人 / 具身智能的「现实底座」——三维重建工程化、高精地图 / 时空数据、标定与量产导入。欢迎交流(脉脉搜索「Mainland」)。