
先跟你聊个项目烦恼排班表排到崩溃、配送路线越跑越远、仓库装箱总觉得空了一半。这些事搁以前我第一反应是写一堆规则加一堆 if else结果规则越来越多真到要用的那天还是得靠老师傅拍脑袋。直到我在实际项目里把组合优化工具正经用起来才发现这些问题的解法其实是同一个套路。今天要聊的这套 Google OR-Tools就是那个让我少走两年弯路的神级开源套件。它不是一个只会解某种数学题的单一求解器而是一整套组合优化工具箱员工排班、车辆路径规划、装箱、背包、排课、资源分配基本你能想到的离散优化问题它都能建模求解。而且对 Python 开发者极其友好从安装到跑通第一个模型五分钟足够。如果你是做业务系统的工程师、供应链物流方向的同学或者刚接触运筹优化的学生这篇内容应该能帮你把工具的全貌和落地方案一次性看明白。1. OR-Tools 是什么一个组合优化工具箱而不是单一求解器1.1 四种核心求解器各管一摊我最早误以为 OR-Tools 就一个求解函数进去之后才发现它是整套体系。核心组件分四大块每一块处理的数学问题类型完全不同。第一块是约束规划求解器也就是 CP-SAT。它最适合处理一堆约束 有限离散变量的问题比如排班、排课、任务调度。它的核心思路不是传统线性规划那种单纯形法而是基于约束传播与冲突驱动的子句学习在搜索过程中不断剪枝。说人话就是它会把张三不能连续上夜班每个班次至少两个人这类规则塞进约束系统里再用启发式搜索高效地翻找可行区域。后续版本的 CP-SAT 还内置了线性松弛能力使得它处理带目标函数的优化问题也很强不只是找可行解。第二块是路由求解器专精车辆路径问题。这个模块底层其实集成了带本地搜索的元启发式方法比如大规模邻域搜索。它跟你自己在图上写 Dijkstra 完全不是一回事。路由求解器不是单纯找一条最短路而是统筹多台车辆、多个点、多时间窗、多容量限制的整体路线方案。第三块是线性优化求解器处理整数规划、混合整数规划、线性规划。OR-Tools 里通过 pywraplp 接口调用的底层求解器是开源的 CLP、CBC、SCIP 等。这一块适合变量规模大、约束关系平移但约束类型比较规整的问题比如生产排程里常见的混合整数模型。第四块是图算法套件提供最短路径、最大流、最小费用流这些经典图论算法。小场景里你自己写 BFS 没问题但上了规模直接调用它内部高度优化的 C 实现效率天差地别。1.2 为什么说它对开发者友好说一下体感OR-Tools 对开发者最大的友好是抽象层次。它把运筹建模里最繁琐的构造标准型矩阵稀疏化求解器参数调优全部包掉了。你只需要定义有哪些决策变量这些变量满足什么约束你想最大化或最小化什么然后交给求解器。这个思维方式和写普通业务代码不同但一旦转换过来建模速度非常快。而且 Python 接口能做到与 C 底层几乎一致的性能不需要为了性能提前切换到 C这对快速验证方案简直不要太舒服。另外一个很关键的点OR-Tools 是纯开源商用友好的 license。你在商业项目里集成它不需要担心授权成本这就让它成为很多创业团队和中小型项目的第一选择。相比动辄几十万授权的商业求解器这是一上来就能直接开工的方案。2. 安装与第一个模型十分钟跑通 0/1 背包2.1 安装只有一条命令环境要求很简单Python 3.8 以上的环境就够了。安装直接用包管理工具pip install ortools这里有个小坑如果你是在公司内网环境装建议先确认 Python 版本和网络源。我遇到过装完导入时报ortools模块不存在十有八九是环境中同时存在多个 Python 解释器把包装到了另一个环境下。用虚拟环境能规避掉这种低级问题。2.2 用 CP-SAT 解背包问题代码就这么简单先给你一个最小的可运行例子用 CP-SAT 解经典 0/1 背包问题from ortools.sat.python import cp_model weights [2, 3, 4, 5, 9] values [3, 4, 5, 8, 10] capacity 10 n len(weights) model cp_model.CpModel() # 决策变量每个物品选还是不选 x [model.NewBoolVar(fx_{i}) for i in range(n)] # 重量约束总重量不超过容量 model.Add(sum(weights[i] * x[i] for i in range(n)) capacity) # 目标函数最大化总价值 model.Maximize(sum(values[i] * x[i] for i in range(n))) solver cp_model.CpSolver() status solver.Solve(model) if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): selected [i for i in range(n) if solver.Value(x[i]) 1] print(总价值:, solver.ObjectiveValue()) print(选择的物品:, selected) else: print(无解)这段代码很短但已经完整走完了 OR-Tools 建模的标准流程创建模型对象、声明变量、添加约束、定义目标、调用求解器、读取结果。跑完你就能看到在容量 10 的情况下最优总价值是 18选择的物品是索引 0、2、3 这三件。2.3 理解建模三个要素变量、约束、目标很多新手上来就卡在我的业务怎么转换成变量、约束、目标这步。其实有个套路可循先问自己哪些东西是需要系统替我决定的。选的物品、排班的人、路径上的点这些都是决策变量。再说约束现实里硬性不能违反的规则就是约束比如总量不能超载每人每天最多一个班次必须按时交付。最后是目标通常一个量化指标比如最省钱、耗时最短、资源利用最均衡。这个顺序全部想清楚再动手写代码建模成功率会提高非常多。另外注意 CP-SAT 里的变量是整数域。背包问题天然用 0/1 变量这个例子刚好体现布尔变量NewBoolVar的用法。实际情况中更多是NewIntVar(low, high, name)来定义整数区间的变量。3. 三个高频实战场景这样建模基本不会跑偏3.1 员工排班最难的不是约束是把人的规则翻译成数字模型排班问题我前后做过好几个版本给某连锁门店做排班系统时最大的痛苦不是写求解器而是跟店长梳理规则。你会发现现实规则特别多且碎不能连上两个夜班、周末尽量少排新人、每个班次必须有一个老员工、休假天数要动态变化。翻译成数学约束之后代码反而没有那么多。下面是我经常用来演示的排班模型稍微精简但保留了核心约束from ortools.sat.python import cp_model num_workers 4 num_days 7 num_shifts 3 # 班次0、1、2 model cp_model.CpModel() shifts {} # w员工, d天, s班次 for w in range(num_workers): for d in range(num_days): for s in range(num_shifts): shifts[(w, d, s)] model.NewBoolVar(fw{w}_d{d}_s{s}) # 每人每天最多一个班次 for w in range(num_workers): for d in range(num_days): model.Add(sum(shifts[(w, d, s)] for s in range(num_shifts)) 1) # 每天每个班次至少安排一个人 for d in range(num_days): for s in range(num_shifts): model.Add(sum(shifts[(w, d, s)] for w in range(num_workers)) 1) # 每人每周班次数尽量控制在 2~4 之间 for w in range(num_workers): model.Add(sum(shifts[(w, d, s)] for d in range(num_days) for s in range(num_shifts)) 2) model.Add(sum(shifts[(w, d, s)] for d in range(num_days) for s in range(num_shifts)) 4) solver cp_model.CpSolver() status solver.Solve(model) if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): for d in range(num_days): line f第{d1}天: for s in range(num_shifts): for w in range(num_workers): if solver.Value(shifts[(w, d, s)]) 1: line f班次{s1}员工{w1} print(line) else: print(无解)这个模型没有目标函数只求可行解就已经能满足不少小规模排班需求。如果你想进一步做到公平把每个员工总班次数尽量均衡定义成 minimize 目标即可。还有更细的约束组合OR-Tools 提供了AddAllowedAssignments、AddAllDifferent、AddExactlyOne这些封装能很优雅地表达复杂规则。实战心得是排班问题的建模顺序一定是从硬约束开始先让模型有解再加软约束和目标函数。一上来把所有规则全部塞进去求解器可能直接给你返回一个不可行状态到时候你根本分不清是哪条规则写错了。3.2 车辆路径绕城配送的路线一行回调函数就能搞定路径优化是另一个常见的硬骨头。某物流调度项目里我有一张真实的城市配送距离表二十多个配送点三台车每辆车容量还不同。手动排路线基本就是凭借老师傅经验但分完路线之后经常出现一辆车超载、另一辆空跑的情况。用 Routing 求解器之后方案质量提升非常明显。最小的 TSP单辆车的旅行商问题代码如下from ortools.constraint_solver import routing_enums_pb2, pywrapcp distance_matrix [ [0, 2, 9, 10], [1, 0, 6, 4], [15, 7, 0, 8], [6, 3, 12, 0], ] manager pywrapcp.RoutingIndexManager(len(distance_matrix), 1, 0) routing pywrapcp.RoutingModel(manager) def distance_callback(from_index, to_index): from_node manager.IndexToNode(from_index) to_node manager.IndexToNode(to_index) return distance_matrix[from_node][to_node] transit_callback_index routing.RegisterTransitCallback(distance_callback) routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) search_parameters pywrapcp.DefaultRoutingSearchParameters() search_parameters.first_solution_strategy ( routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC ) solution routing.SolveWithParameters(search_parameters) if solution: index routing.Start(0) route [] while not routing.IsEnd(index): route.append(manager.IndexToNode(index)) index solution.Value(routing.NextVar(index)) route.append(manager.IndexToNode(index)) print(TSP 路线:, route)核心是那个distance_callbackOR-Tools 会反复调用它来询问任意两点之间的代价这个代价不一定是距离也可以是时间、油耗、费用。Routing 模块把这类指标统称为代价回调注册到模型里就自动参与优化计算。多车辆、容量限制、时间窗基本都是在这个结构上叠加维度实现的比如用routing.AddDimension来创建容量或时间维度。这类问题我最想提醒的一点距离矩阵一定要保证正确性。我之前用地图 API 批量计算时因为部分点之间没有道路通行直接给了一个极大值当距离结果求解器把路线硬绕到极远的地方我还以为模型写错了排查半天才发现是数据源问题。先把数据清洗干净再谈模型优化。3.3 装箱与资源分配空间和资源其实可以抽象成维度装箱问题听起来难但实际上在 OR-Tools 里建模并不复杂。它可以抽象成有一批物品每个有长宽高或重量有若干个箱子每个箱子有容量上限目标是用最少箱子装完所有物品或者把一个箱子填得更满。这类问题通常会深度使用 CP-SAT 的区间变量和可选区间机制来处理物品与箱子之间的等价关系。不过这里我不想贴太长代码给你一个建模思路把每个物品放进哪个箱子作为整数变量范围为 0 到箱子总数减一再对每个箱子的总重量、总体积分别建立约束最后把箱子是否使用这个布尔变量设为目标函数去最小化。在资源分配场景中OR-Tools 还有一个非常实用的抽象叫维度。路由求解器里的容量维度、时间维度本质上是给每辆车维护一个沿路线累积的量比如装了多少货、走了多久。这个抽象几乎能覆盖所有带累积约束的问题比如外卖配送的保温箱容量、生产线上的上下料时间。理解了维度你再看它的文档会豁然开朗。4. 建模时最容易翻车的细节我替你试过4.1 浮点数会坑你CP-SAT 只吃整数这是新手入坑会遇到的第一大坑。CP-SAT 的所有变量和约束都是整数域它本身不处理浮点数。如果你在业务里遇到价格、重量、时间这些带小数点的数据直接传给模型会报错或者结果被隐式截断。标准做法是缩放成整数。比如金额保留两位小数就乘以 100 转成分重量精确到克就乘以 1000。缩放比例需要全局一致否则约束关系会失调。这个转换过程看似简单但我在项目里吃过亏曾经把时间粒度从分调整成秒时忘了改所有约束里的对应系数结果模型算出一堆不可行的路线。所以建议在一开始就把数据粒度定义写清楚建模代码里统一使用同一个常量系数。4.2 软约束和奖励要怎么揉进目标函数业务系统里很多规则不是绝对的比如尽量让老员工优先休假尽量少用一个箱子。这样的软约束不能直接放进硬约束列表里否则模型可能直接被搞到无解。常规做法是引入惩罚变量或者奖励变量把违反程度乘上一个惩罚系数加进目标函数。目标函数是一个被最小化或最大化的量所以尽量少用一个箱子就可以写成minimize(箱子使用数量 * 成本系数)。这个过程的难点在于系数怎么定。系数太小求解器会无视这个惩罚系数太大又会过度牺牲其他目标。实际调参时我会先固定一个目标观察另一个指标的变化幅度然后二分调整系数基本几次就会收敛到可接受的范围。4.3 求解时间失控怎么限制和判断组合优化在规模大的时候非常容易求解时间失控。OR-Tools 默认会一直算下去直到证明最优这在生产环境是不能接受的。几乎所有实战项目里都要显式加上时间限制。CP-SAT 的用法是solver.parameters.max_time_in_seconds 10Routing 求解器则是search_parameters.time_limit.seconds 10加了时间限制之后求解器可能在超时前返回一个可行但不保证最优的结果。这时候就要学会看两个指标ObjectiveValue是当前找到的最好目标值BestObjectiveBound是理论上能够达到的边界。两者越接近说明距离最优解越近。如果时间到了但 gap 还很大就说明模型还没收敛要么加时间要么简化模型。5. 常见问题与排查思路速查5.1 模型一直解出 0 或空解如果是排班模型这种用布尔变量的场景解出来全是空大概率是约束条件互相冲突。我遇到过有人把每人每天至少一个班次和每人每天最多一个班次同时设成严格相等还把班次数和人数传给搞错了结果是必然无解。排查办法也简单把所有约束先注释掉只保留一组硬约束跑一次如果还产生结果再把约束逐个加回来很快能定位到冲突的那一条。5.2 解有了但不是最优而且差得远出现这个现象的常见原因是目标函数没写对或者没写全。比如排班问题里你只写了可行性约束那求解器给出任何一个可行解就停了你根本不知道它是不是优化过。另外还有一个隐蔽坑距离或成本矩阵的单位不一致。某个项目里我有一段距离是公里另一段距离是米模型计算结果里路线乱绕最终对比实际里程才发现是单位没统一。先检查数据的一致性再检查目标函数。5.3 规模一大就跑不动怎么办如果问题规模确实很大第一件事是审视模型的对称性。比如两个员工能力一致、位置互换解相同这种情况下解空间存在大量对称解搜索效率极低。解决思路是加对称破坏约束比如强制员工 A 的班次数不大于员工 B。第二个手段是给求解器开多线程CP-SAT 支持设置搜索并行度solver.parameters.num_search_workers 8第三招是对问题做降维分解。比如把大片区拆成若干小片区分别求解再合并结果或者先用启发式算法产生一个不错的初始解再交给 OR-Tools 做改进。OR-Tools 里可以通过设置初始解候选来加速收敛。5.4 排查顺序和几个实用技巧把最常见问题的排查顺序整理成一张速查表方便你直接对照现象优先排查推荐的修复动作无解约束冲突逐条注释约束定位冲突检查变量域是否为空结果全为 0变量未被约束绑定检查决策变量定义范围确认布尔变量是否有遗漏约束结果差得远目标函数或数据单位错误验证目标表达式统一数据缩放系数求解超时模型规模或参数设置不合理加时间限制、开并行、降维或拆分问题结果不稳定初值/随机种子影响固定随机种子或增加求解时间还有一个实用技巧CP-SAT 求解后的状态码要会看。OPTIMAL代表已经证明最优FEASIBLE代表找到了可行解但未证明最优INFEASIBLE代表无解。很多人在状态码处理上只看OPTIMAL导致明明有合法排班结果却因为状态码不对直接忽略非常可惜。6. 它的边界在哪里什么时候该换更专业的求解器6.1 哪些问题它搞不定OR-Tools 在离散优化领域很强悍但不代表所有优化问题都能包打天下。连续变量为主的问题比如投资组合优化、连续参数调优它的表现就不够好。这类问题更适合专业的线性规划或非线性规划求解器甚至可以直接用数值优化库。另外如果问题包含复杂的非线性约束比如乘积形式的约束CP-SAT 的整数模型很难直接表达需要通过分段线性化等手段重新建模这工作量往往不小。还有个容易被忽视的限制OR-Tools 不擅长处理不确定性优化。比如需求不确定的库存优化、概率约束问题它不是为这类随机优化设计的。当你发现核心业务逻辑需要大量处理和概率相关的变量就该考虑其他方案了。6.2 和启发式算法的配合我不建议把所有问题都丢给一个求解器。实际工程里效果最好的方式往往是 OR-Tools 和启发式算法配合。比如先用贪心或遗传算法得到一个初始解再把初始解作为起点喂给 OR-Tools 去进行局部大邻域搜索让它在这个基础上优化。反过来也行先让 OR-Tools 快速求一个可行解再用专门的邻域搜索来微调。OR-Tools 路由模块本身就内置了多种搜索策略你完全可以用自定义启发式方法来扰动初始解空间这会让最终方案质量明显提升。6.3 换个更专业的求解器说句实话如果你的问题明确是大型混合整数规划比如几十万级变量、几百万条约束OR-Tools 的开源求解器确实比不过商业求解器比如高性能商用 MIP 求解器带来的性能差距是数量级的。在这个场景下我建议把 OR-Tools 当作原型验证工具用它快速验证模型方向是否可行然后切换到商业求解器做生产级计算。不过绝大部分业务场景其实没有到那个量级我做过不少项目变量规模在几千到几万个之间OR-Tools 的 CP-SAT 表现相当够用而且免费这个优势让它成为中小企业落地的最佳起点。先跑通再优化比一开始就上重武器要现实得多。最后分享一点我在多个真实项目里的体感这类工具一定要用一个真实场景完整跑一遍你才会理解建模比求解更重要。变量定义是否清晰、约束里是否悄悄混进了软规则、目标函数是否真正贴合业务指标这些决定了结果的可用性。工具再强也替你做不了业务建模的思考。建议你找一个手头最常见的排班或路径问题从最小模型开始一步步加约束很快你就会迷上这种把规则变成数学再让机器替你找答案的感觉。遇到具体的坑欢迎在评论区和大家交流。