物流路径优化案例:算法结果要能被运营接住
从一次配送调度说起
上个月帮一个同城配送团队看他们的调度系统,运营主管跟我讲了一个故事:系统算出了一条总里程最短的路线,司机跑了两天,投诉了三次。第一次是因为路线穿过了一个早市,车进不去;第二次是因为某个收货点下午三点后才有人,但系统排的是上午;第三次是因为那条「最短路线」让某辆车多跑了 60 公里,而其他车闲着。
他总结了一句我觉得很准确的话:算法给你的是数学最优,运营需要的是可执行最优。这两者之间的差距,才是工程真正要做的事。
这篇文章聊的就是这个差距怎么填。
VRP 不是「最短路径」的升级版
很多人第一次接触物流路径优化,会觉得它只是 TSP(旅行商问题)加了辆车。实际差距比想象中大得多。
VRP(Vehicle Routing Problem,车辆路径问题)是运筹学中的经典 NP-hard 问题。根据 Toth 和 Vigo 在《Vehicle Routing Problems: Methods & Applications》中的定义,VRP 的核心是在满足一系列约束条件下,为一组车辆找到从仓库出发、服务所有客户、最终返回仓库的最优路线集合。它的变体超过 200 种,工程中最常见的三类是:
| 变体 | 全称 | 核心约束 | 典型场景 |
|---|---|---|---|
| CVRP | Capacitated VRP | 车辆容量限制 | 仓配一体、满载配送 |
| VRPTW | VRP with Time Windows | 客户时间窗 | 即时配送、快递末端 |
| LRP | Location-Routing Problem | 设施选址 + 路径 | 前置仓网络规划 |
在求解方法上,业界主流分三大流派:
| 方法 | 代表算法 | 适用规模 | 工程成熟度 |
|---|---|---|---|
| 精确算法 | 分支定价、列生成 | 50 节点以下 | 学术成熟,工程少用 |
| 启发式算法 | 节约算法、2-OPT、ALNS | 数百节点 | 工程主流,OR-Tools / Jsprit |
| 元启发式 | 模拟退火、遗传算法、蚁群 | 数千节点 | 大规模场景补充 |
| 混合方法 | DRL + OR 融合 | 视实现而定 | 学术前沿,工业落地早期 |
Google OR-Tools 的 Routing 模块是工程中使用最广的开源求解器之一。它的核心建模方式是:用 RoutingIndexManager 管理节点索引,用回调函数定义弧线成本,用 Dimension 追踪累计约束(里程、时间、容量)。求解器内部使用约束规划(Constraint Programming)框架,通过 first_solution_strategy 生成初始解,再用局部搜索迭代改进。
芬兰 Aalto 大学 2025 年的一项研究验证了这个思路在最后一公里配送中的效果:使用开源约束规划求解器,在 25 个客户以下的小规模场景中,总行驶时间降低最高 25%,平均单程缩短 9.92 分钟。但当客户数超过这个阈值,纯精确方法效率急剧下降,必须引入启发式策略辅助。
案例一:时间窗约束——算得出来,送不到门口
场景
一个社区团购平台,每天从 3 个网格仓向约 200 个自提点送货。运营希望系统自动排出每辆车的配送顺序,并保证在每个自提点规定的收货时间窗内到达。
翻车
第一版系统用 OR-Tools 跑 VRP,目标函数是总里程最小。结果出来后发现两个问题:
- 某些自提点要求 14:00 之后才能收货(团长下午才开门),但系统把配送安排在上午——因为总里程更短。
- 有一辆车被塞了 45 个点,另一辆只有 8 个——因为里程最优解天然倾向于「少出车」。
修复
加时间窗约束 + 车辆负载均衡。核心改动是在 OR-Tools 中增加 Time Dimension,把每个节点的 earliest_arrival 和 latest_departure 作为硬约束;同时对每辆车设置站点数上下限。
# ❌ 坏做法:只考虑距离,不管时间窗
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
# 没有 AddDimension,没有 slack,没有等待时间处理
# ✅ 好做法:增加时间维度,带 slack 和等待时间
routing.AddDimension(
transit_callback_index,
slack=60, # 允许 60 分钟弹性(堵车/卸货)
capacity=480, # 单车最大工作时长 480 分钟
fix_start_cumul_to_zero=True,
name="Time"
)
time_dimension = routing.GetDimensionOrDie("Time")
# 为每个节点设置时间窗
for node_idx, (earliest, latest) in enumerate(time_windows):
index = manager.NodeToIndex(node_idx)
time_dimension.CumulVar(index).SetRange(earliest, latest)时间窗建模的关键在于 slack 参数的选取。设得太小,真实路况中一个红灯就可能让整条路线不可行;设得太大,算法会把等待时间「藏」在 slack 里,输出看起来可行但实际空等很久的路线。我们最终选了 60 分钟——来自运营提供的历史卸货时间 P90 值。
效果
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 准时到达率 | 62% | 94% |
| 单车最大站点数 | 45 | 28 |
| 司机日均等待时间 | 47 分钟 | 12 分钟 |
| 总里程(增幅) | 基准 | +6.3% |
总里程增加了 6.3%,但准时率和负载均衡大幅改善。运营说了一句:「里程多一点没关系,至少司机不再骂我了。」
案例二:容量约束与实际装载——理论值和真实值之间
场景
一个生鲜电商的前置仓,每辆车有固定容积(0.8 立方米)和载重上限(500kg)。订单里有蔬菜、冻品、日用品,体积和重量差异很大。
翻车
系统按订单重量做了容量约束,排出的路线看起来没问题。但到仓库装车时发现:冻品需要保温箱,实际占用体积是「裸体积」的 2.5 倍;某些订单因为要分两个温区存放,虽然重量没超,但容积已经爆舱。
结果现场调度不得不手动拆单、重新排车,算法排好的路线全部作废。
修复
核心是把「逻辑容量」换成「物理容量」——模型中的容量约束不再用订单重量,而是用实际装车后的物理占用量。具体做法:
- 为每个 SKU 维护一个
physical_footprint(物理占用量),包含包装、保温箱、温区隔离的额外空间。 - 在 VRP 模型中同时设置重量维度和容积维度,两个维度独立约束。
- 加入温区兼容性检查:同一辆车的订单必须属于可混装的温区组合。
# ❌ 坏做法:只用订单重量做容量约束
routing.AddDimension(
weight_callback_index,
0,
vehicle_max_weight,
True,
"Weight"
)
# 没有考虑体积,没有温区约束
# ✅ 好做法:双维度(重量 + 容积)+ 温区校验
routing.AddDimension(weight_callback, 0, 500, True, "Weight")
routing.AddDimension(volume_callback, 0, 800, True, "Volume") # 单位:升
# 温区兼容性:用 disjunction 约束不可混装的订单
for order_id in frozen_orders:
node_index = order_to_node[order_id]
routing.AddDisjunction(
[manager.NodeToIndex(node_index)],
penalty=10000 # 高惩罚 = 必须分配,但不允许和常温混装
)温区兼容性在 OR-Tools 中没有原生约束,工程上可以用 VehicleVar 约束把某些节点限制在特定车辆子集上,或者在数据预处理阶段按温区分组,不同温区的订单分别求解再合并。
效果
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 现场手动拆单率 | 38% | 4% |
| 装车利用率 | 61% | 87% |
| 算法路线采纳率 | 52% | 91% |
| 冻品损耗率 | 2.1% | 0.4% |
这个案例的教训是:算法模型中的「容量」必须和仓库现场的「物理现实」一致。 用订单重量做容量约束,看起来合理,实际上丢掉了包装、温区、堆叠这些真实因素。
案例三:运营干预——系统不允许接管,异常就没人管
场景
一个区域快递网点,每天 12 辆车送 1500+ 单。系统负责排线,但运营主管需要在以下情况手动调整路线:
- 某个大客户临时要求改时间
- 某辆车半路抛锚,剩余订单要重新分配
- 新来一个紧急件,需要插到某条路线中
翻车
第一版系统把路线排好之后就锁死了,运营没法改。遇到上面的情况,要么打电话让程序员「后台改数据库」,要么干脆让调度员在 Excel 里重新排一遍,系统完全被绕过。
三个月后统计,系统路线采纳率只有 40%——不是算法不好,是系统不给人留操作空间。
修复
引入了「人工干预层」,在算法输出和实际执行之间加了一层可编辑的路线视图。核心设计:
- 可锁定节点:运营可以把某个订单标记为「已确认」,后续重排时该节点不参与优化。
- 手动拖拽排序:在路线视图中直接拖动节点顺序,系统实时校验是否违反约束。
- 干预日志:每次手动修改都记录操作人、原因、修改前后的指标差异。
干预日志的价值在三个月后显现出来——我们发现运营手动改得最多的场景是「学校周边订单」,原因是放学时段道路封锁,但地图数据没更新。把这个规律反馈给算法后,在地图层加了学校时段的道路权重,系统自动就避开了。
# ❌ 坏做法:算法输出直接锁定,不给运营留空间
solution = routing.SolveWithParameters(search_parameters)
# 直接下发到司机端,运营无法修改
dispatch_to_drivers(solution)
# ✅ 好做法:算法输出为「建议方案」,运营可编辑后下发
candidate_routes = routing.SolveWithParameters(search_parameters)
editable_plan = RoutePlan(candidate_routes)
# 运营可以在编辑器中修改
for adjustment in operator_adjustments:
if validate_constraints(editable_plan, adjustment):
editable_plan.apply(adjustment)
log_intervention(adjustment, operator_id, reason)
else:
return constraint_violation_feedback(adjustment)
dispatch_to_drivers(editable_plan.finalize())效果
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 路线采纳率 | 40% | 88% |
| 异常处理平均耗时 | 25 分钟 | 4 分钟 |
| 运营满意度 | 2.1/5 | 4.3/5 |
| 干预反哺算法覆盖率 | 0% | 67%(3 个月后) |
算法和工程之间的四条经验
做完这几个项目之后,我总结了一些判断标准。
1. 数据质量决定算法上限
| 数据问题 | 影响 | 发现方式 | 修复手段 |
|---|---|---|---|
| 地址坐标偏移 | 路线绕行、距离失真 | 轨迹回传比对 | 地址清洗 + 坐标纠偏 |
| 时间窗未更新 | 空等或超时 | 到达时间分布分析 | 定期校验 + 运营确认 |
| 车辆信息不同步 | 容量超限、车型不匹配 | 现场抽查比对 | 车辆主数据管理 |
| 路况数据延迟 | 高峰期路线失效 | 实时 vs 历史用时对比 | 接入实时路况 API |
2. 目标函数要贴近业务价值
# ❌ 坏做法:只优化总里程
routing.SetArcCostEvaluatorOfAllVehicles(distance_callback_index)
# ✅ 好做法:多目标加权(里程 + 时间 + 负载均衡 + 准时惩罚)
routing.SetArcCostEvaluatorOfAllVehicles(composite_cost_callback_index)
# 全局跨度系数:压缩最长路线,实现负载均衡
distance_dimension.SetGlobalSpanCostCoefficient(100)
# 时间窗违反惩罚:宁可绕路也不超时
time_dimension.SetCumulVarSoftUpperBound(
index, max_time, penalty_per_minute
)目标函数的设计不是算法团队闭门造车能完成的。我见过一个团队花两周调权重系数,运营看了一眼说「你让我一辆车跑 300 公里另一辆跑 30 公里,这不叫最优」。最后权重是运营和算法一起看着历史数据定的。
3. 求解时间要有预算
VRP 求解时间是节点数的超线性函数。在实时调度场景中,不能等求解器收敛到全局最优,要在「解的质量」和「响应时间」之间做取舍。
| 场景 | 节点规模 | 时间预算 | 推荐策略 |
|---|---|---|---|
| 次日达排线 | 200-500 | 5-10 分钟 | 充分搜索,求高质量解 |
| 实时插单 | 20-50 | 3-5 秒 | 局部搜索,快速响应 |
| 应急重排(抛锚) | 50-100 | 10-30 秒 | 固定已配送节点,局部重优化 |
| 周期性网络规划 | 1000+ | 1-4 小时 | 分层求解 + 并行计算 |
4. 得物技术团队的观察值得参考
他们在 2025 年的技术分享中提到:当前工业界路径规划「普遍使用的还是传统优化方法」,DRL 更多用于上游预测(单量预测、需求分布),而非直接替代 OR 求解器。核心原因是 OR 方法在业务约束频繁变化时能「开箱即用」,DRL 每次改约束都需要重新训练,试错成本高。
这个判断和我的经验一致。不是说 DRL 不好,而是在物流调度这个场景下,约束条件的复杂度和变化频率决定了 OR + 工程化是当前最务实的选择。
系统架构全景
一个完整的物流路径优化平台,数据流和控制流大致如下:
每一层都要做好自己的事情:数据层保证及时准确,算法层输出可行且高质量的方案,业务层让运营能接住并微调,反馈层让系统越来越准。
上线前检查清单
数据准备阶段
- 所有节点的经纬度坐标已经过清洗和纠偏,与地图服务匹配
- 时间窗数据来源可靠(客户确认 / 历史统计),不是拍脑袋填的
- 车辆容量模型用的是「物理占用量」,不是简单的订单重量或件数
- 路况数据来源已接入(实时 API 或历史分时段矩阵),高峰期有独立数据
- 司机排班、车辆可用性已与 HR/车队系统同步
算法建模阶段
- 目标函数权重经过运营确认,不是算法团队单方面决定
- 约束条件分为硬约束(不可违反)和软约束(可付费违反),边界清晰
- 求解时间预算已明确,不同场景(排线、插单、应急)有不同策略
- 负载均衡约束已设置,不会出现一辆车满载其他车空闲的情况
- 异常场景(节点不可达、车辆故障、订单取消)有 fallback 逻辑
运营交付阶段
- 运营审核台可查看、编辑、锁定路线,不是只读展示
- 手动干预有日志,记录操作人、原因和指标变化
- 司机端能收到调整后的路线,且有异常上报通道
- 上线首周有人工比对机制:系统路线 vs 司机实际路线逐条比较
持续迭代阶段
- 每周计算核心指标:准时率、路线采纳率、空驶率、异常率
- 运营干预数据定期分析,提取可反哺算法的规律
- 地图数据、时间窗数据定期刷新,不依赖一次性配置
- 算法版本有 A/B 测试机制,新策略上线前用历史数据回放验证
参考资料
- Google OR-Tools 车辆路线问题文档 — 官方 VRP 建模示例与参数说明
- 得物技术:路径规划中的 DRL 与 OR 算法对比与展望 — 工业界路径规划现状与混合方法展望
- Aalto University: Optimizing Route Planning in Last-Mile Logistics (2025) — 开源约束规划求解器在最后一公里配送中的效果验证
- Springer: Optimization and Artificial Intelligence in Logistics Management (2025) — 物流管理中优化与 AI 方法的综合综述
- 汉斯出版社:大数据驱动的电商物流路径实时优化算法研究 (2026) — 电商物流场景下的 VRP 模型与实时优化方法
- ScienceDirect: A Systematic Review of VRPs and Models in Multi-echelon Logistics (2024) — VRP 在多层级物流网络中的系统性综述
- Springer: Industrial Vehicle Routing Problem — A Case Study (2022) — FMCG 企业工业级 VRP 落地案例
- 系统仿真学报:优先配送绿色 VRP 的混合启发式求解算法 (2025) — SA + ALNS 混合方法在绿色 VRP 中的应用