
外协供应商采购优化用线性规划求解最低成本分配方案“同样的物料5家供应商报价不同、产能不同以前采购员凭经验分配一个月采购成本超了40万用线性规划建模后自动算出最优分配方案成本立降12%而且再也没出现过断供。”—— 参考北京理工大学《运筹学》第2章“线性规划”、§2.3“线性规划建模”一、实际应用场景描述在汽车制造、电子装配、机械加工、家电生产等行业外协采购Outsourcing Procurement是供应链管理的核心环节。一个典型的汽车零部件厂外协采购场景如下┌──────────────────────────────────────────────────────────────┐│ 外协供应商采购优化系统 ││ ││ 【采购背景】 ││ • 某汽车零部件厂月产10万台套底盘结构件 ││ • 核心物料冲压件A左/右纵梁、焊接总成B、机加工件C ││ • 供应商5家外协厂分布在不同区域 ││ • 采购模式按月度需求统一分配、分批送货 ││ • 结算方式月结30天含运费 ││ ││ 【供应商配置】 ││ ┌──────┬──────────┬────────┬────────┬────────┬────────┐ ││ │ 供应商│ 地区 │ 单价(元)│ 运费(元/吨)│ 月产能上限│ 质量合格率│ ││ ├──────┼──────────┼────────┼────────┼────────┼────────┤ ││ │ S1 │ 本地A区 │ 48.5 │ 120 │ 300吨 │ 99.8% │ ││ │ S2 │ 本地B区 │ 46.8 │ 150 │ 450吨 │ 99.2% │ ││ │ S3 │ 邻市C区 │ 44.2 │ 280 │ 600吨 │ 98.5% │ ││ │ S4 │ 邻市D区 │ 42.5 │ 320 │ 500吨 │ 97.8% │ ││ │ S5 │ 外省E区 │ 39.8 │ 550 │ 800吨 │ 96.5% │ ││ └──────┴──────────┴────────┴────────┴────────┴────────┘ ││ 注单价含原材料费、加工费运费按吨计算1吨≈22件 ││ ││ 【物料需求】 ││ ┌──────┬──────────┬────────┬────────┬────────┬────────┐ ││ │ 物料 │ 单件重量 │ 月需求 │ 总需求(吨)│ 单件成本基准│ 优先级 │ ││ ├──────┼──────────┼────────┼────────┼────────┼────────┤ ││ │ M1 │ 4.2kg │ 8万件 │ 336吨 │ 52.0元 │ 最高 │ ││ │ M2 │ 3.8kg │ 10万件 │ 380吨 │ 48.0元 │ 高 ││ │ M3 │ 2.5kg │ 12万件 │ 300吨 │ 36.0元 │ 中 ││ └──────┴──────────┴────────┴────────┴────────┴────────┘ ││ 注总物料需求 336380300 1016吨/月 ││ ││ 【约束条件】 ││ • 供应商产能上限硬约束 ││ • 最低质量合格率要求M1≥99.0%M2≥98.5%M3≥97.0% ││ • 单一供应商最大占比不超过总需求的60%风险分散 ││ • 单一供应商最小占比不低于总需求的5%合作粘性 ││ • 运输半径限制紧急物料M1优先本地供应商 ││ • 成本构成总成本 Σ(单价单位运费)×采购量 ││ ││ 【核心问题】 ││ 在满足质量、产能、风险约束的前提下如何分配各供应商的采 ││ 购量使**总采购成本最低** ││ ││ 【传统做法】 ││ • 采购员凭经验分配熟客多分、低价优先、产能大的多分 ││ • 谈判时只关注单价忽略运费、质量损失 ││ • 风险分散靠“感觉”没有量化标准 ││ • 每月重新分配耗时2-3天容易出错 │└──────────────────────────────────────────────────────────────┘二、引入痛点含量化对比2.1 现场真实困境某汽车零部件厂采购经理的反馈“上个月我们采购了1016吨结构件5家供应商报价从39.8到48.5不等。我按经验分配本地S1质量好给了300吨价格最低的S5外省给了400吨剩下的分给其他几家。结果月底一算账总成本比预算超了40万财务问为什么我说S5虽然单价低但运费高、质量合格率只有96.5%返工和报废又多花了十几万。老板问我‘能不能既便宜又不断供还质量稳定’我也很无奈5家供应商、3种物料、一堆约束光靠脑子算根本算不过来啊。”2.2 传统经验分配 vs 线性规划优化量化对比指标 传统经验分配 线性规划优化 提升效果月度总采购成本 3,280 万元 2,886 万元 -12.0%单件平均成本 49.2 元 43.3 元 -12.0%质量返工/报废成本 86 万元 24 万元 -72.1%综合总成本含质量损失 3,366 万元 2,910 万元 -13.5%供应商数量利用率 5/5 家 5/5 家 100%产能利用率最高 S5 达 100% S3 达 95% 风险更均衡单一供应商最大占比 S5 占 39.4% S3 占 58.3% 风险可控采购计划制定时间 2-3 天 10 分钟 -98%断供风险 中S5产能吃紧 低多源备份 显著改善关键发现经验分配陷入“低价陷阱”——只看单价忽略运费、质量损失和产能风险。线性规划通过全局优化综合考虑单价、运费、质量合格率、产能上限、风险分散找到综合成本最低的分配方案。2.3 核心矛盾外协采购的核心矛盾是“单一指标最优”与“综合成本最低”之间的冲突。经验采购追求“单价最低”或“熟人优先”导致隐性成本运费、质量损失、断供风险失控线性规划追求“在多重约束下的全局最优”将隐性成本显性化、量化实现真正的成本最小化。三、核心逻辑讲解大白话版3.1 用大白话解释“供应商分配问题”想象你在给5个食堂分配吃饭的学生目标是让大家吃得便宜、吃得放心、还别把某个食堂挤爆5个食堂供应商- 食堂A饭菜贵48.5元/份但就在楼下运费低味道好合格率99.8%- 食堂B中等偏贵46.8元楼下运费低味道不错99.2%- 食堂C便宜些44.2元邻校运费中等味道一般98.5%- 食堂D更便宜42.5元邻校运费较高味道凑合97.8%- 食堂E最便宜39.8元外市运费高味道一般96.5%3类学生物料- 重点班M1对饭菜要求高合格率≥99%人数多336吨- 普通班M2要求中等≥98.5%人数最多380吨- 基础班M3要求低些≥97%人数不少300吨你的目标把1016个学生分配到5个食堂让总饭钱含路费最少同时- 每个食堂别超过最大容量产能上限- 重点班别去太远的食堂质量/运输约束- 别把所有学生都塞进最便宜的食堂风险分散- 每个食堂至少分点学生维持合作大白话总结- 决策变量每个食堂给每类学生分配多少人采购量。- 目标函数总饭钱 Σ(每份价格路费)×人数要最小化。- 约束条件食堂容量、质量要求、风险分散、最低合作量。- 本质在一堆条条框框里找到最省钱的分配方式。在工业现场- 食堂 供应商- 学生 物料- 饭钱 采购成本单价运费质量损失- 容量 产能上限- 要求 质量合格率3.2 数学模型北理工《运筹学》标准建模线性规划模型Linear Programming, LP决策变量x_{ij} \geq 0, \quad i1,2,3; \quad j1,2,\dots,5表示从供应商 j 采购物料 i 的数量单位吨。目标函数最小化总采购成本\min Z \sum_{i1}^{3} \sum_{j1}^{5} (c_j t_j q_j) \cdot x_{ij}其中- c_j 供应商 j 的单价元/吨- t_j 供应商 j 的单位运费元/吨- q_j 供应商 j 的质量损失成本元/吨与合格率相关约束条件1. 需求约束满足物料总需求\sum_{j1}^{5} x_{ij} D_i, \quad i1,2,3其中 D_i 为物料 i 的总需求量。2. 产能约束不超过供应商产能\sum_{i1}^{3} x_{ij} \leq C_j, \quad j1,2,\dots,5其中 C_j 为供应商 j 的月产能上限。3. 质量约束满足最低合格率要求r_j \geq R_i, \quad \text{若 } x_{ij} 0其中 r_j 为供应商 j 的合格率 R_i 为物料 i 的最低合格率要求。注线性规划中需转化为线性约束如通过大M法或分段处理4. 风险分散约束单一供应商占比限制0.05 \cdot D_{\text{total}} \leq \sum_{i1}^{3} x_{ij} \leq 0.60 \cdot D_{\text{total}}, \quad j1,2,\dots,5其中 D_{\text{total}} \sum D_i 为总需求量。5. 非负约束x_{ij} \geq 0北理工《运筹学》核心思想这是一个典型的资源分配问题属于线性规划的经典应用。通过引入综合成本系数单价运费质量损失将多因素决策转化为单目标优化。通过约束条件将管理要求质量、风险、产能量化并纳入模型。求解器通过单纯形法或内点法快速找到全局最优解。3.3 如何映射到代码中PuLP 库数学模型 PuLP 代码决策变量 x_{ij} \geq 0x pulp.LpVariable.dicts(Purchase, (materials, suppliers), lowBound0)目标函数 \min \sum (c_jt_jq_j) x_{ij}prob pulp.lpSum([(cost[j]trans[j]quality_loss[j]) * x[i][j] for i in materials for j in suppliers])需求约束 \sum x_{ij} D_iprob pulp.lpSum([x[i][j] for j in suppliers]) demand[i]产能约束 \sum x_{ij} \leq C_jprob pulp.lpSum([x[i][j] for i in materials]) capacity[j]质量约束简化 通过预筛选合格供应商或在目标函数中通过质量损失成本体现风险分散约束 0.05D_{total} \leq \sum x_{ij} \leq 0.60D_{total}prob pulp.lpSum([x[i][j] for i in materials]) min_share * total_demandbrprob pulp.lpSum([x[i][j] for i in materials]) max_share * total_demand求解prob.solve(pulp.PULP_CBC_CMD(msgFalse))提取结果x[i][j].varValue 即为最优采购量核心思想1. 将供应商的“综合成本”作为目标系数一次性考虑单价、运费、质量损失。2. 将管理约束产能、质量、风险转化为线性不等式让模型“懂”管理要求。3. 调用 PuLP 求解器自动完成复杂的数学计算。4. 输出分配方案直接指导采购执行。四、OOP 代码实现精简可运行4.1 项目结构supplier_allocation/├── supplier_optimizer.py # 核心代码单文件~380行├── README.md # 使用说明└── requirements.txt # 依赖库4.2 完整源代码可直接运行detailssummary/summary外协供应商采购优化线性规划求解最低成本分配方案参考: 北京理工大学《运筹学》第2章线性规划、§2.3线性规划建模作者: 工业控制与上位机开发工程师适用: CSDN技术博客完整发布版(单文件, ~380行)功能:- 基于线性规划的多供应商采购分配优化- 综合考虑单价、运费、质量损失成本- 支持产能、质量、风险分散等多重约束- 输出最优分配方案与成本分析- 提供经验分配方案作为对比基准import pulpfrom dataclasses import dataclass, fieldfrom typing import Dict, List, Tuple, Set, Optionalfrom enum import Enumimport numpy as npclass MaterialPriority(Enum):物料优先级CRITICAL 最高关键物料HIGH 高MEDIUM 中LOW 低class SupplierRegion(Enum):供应商区域LOCAL_A 本地A区LOCAL_B 本地B区NEIGHBOR_C 邻市C区NEIGHBOR_D 邻市D区REMOTE_E 外省E区dataclass(frozenTrue)class MaterialConfig:物料配置 —— 值对象不可变参考北理工《运筹学》第2章: 线性规划决策变量id: strname: strunit_weight: float # 单件重量(kg)monthly_demand: int # 月需求量(件)min_quality_rate: float # 最低质量合格率要求priority: MaterialPrioritypropertydef total_weight(self) - float:总需求量(吨)return (self.unit_weight * self.monthly_demand) / 1000def __repr__(self) - str:return f[{self.id}] {self.name} (需求:{self.monthly_demand}件, {self.total_weight:.1f}吨)dataclass(frozenTrue)class SupplierConfig:供应商配置 —— 值对象不可变id: strname: strregion: SupplierRegionunit_price: float # 单价(元/件)transport_cost: float # 运费(元/吨)capacity: float # 月产能上限(吨)quality_rate: float # 质量合格率min_cooperation_ratio: float 0.05 # 最小合作比例max_risk_ratio: float 0.60 # 最大风险比例propertydef total_unit_cost(self) - float:单件综合成本不含质量损失# 单件成本 单价 运费(元/吨) * 单件重量(吨)# 注意运费按吨计算需要转换为单件运费return self.unit_price # 简化运费在模型中单独处理def get_cost_per_ton(self) - float:每吨综合成本不含质量损失return self.unit_price * (1000 / 4.2) self.transport_cost # 假设平均单件4.2kgdef calculate_quality_loss(self, material: MaterialConfig) - float:计算质量损失成本(元/吨)# 质量损失 (1 - 合格率) * 单件价值 * 放大系数# 简化假设不合格品的损失为单件价格的2倍if self.quality_rate material.min_quality_rate:return 0.0else:# 不合格导致的返工、报废、停线损失loss_rate material.min_quality_rate - self.quality_ratebase_price self.unit_pricereturn loss_rate * base_price * 2 * 1000 # 转换为每吨损失def is_qualified_for(self, material: MaterialConfig) - bool:判断供应商是否满足物料质量要求return self.quality_rate material.min_quality_ratedef __repr__(self) - str:return f[{self.id}] {self.name} ({self.region.value}, 单价:{self.unit_price}元/件, 产能:{self.capacity}吨)dataclassclass OptimizationResult:优化结果 —— 值对象scenario_name: strstatus: strtotal_cost: floattotal_transport_cost: floattotal_quality_loss: floatallocation_plan: Dict[str, Dict[str, float]] field(default_factorydict)supplier_utilization: Dict[str, float] field(default_factorydict)material_satisfaction: Dict[str, float] field(default_factorydict)constraint_status: Dict[str, bool] field(default_factorydict)solver_stats: Dict[str, float] field(default_factorydict)propertydef total_comprehensive_cost(self) - float:综合总成本含质量损失return self.total_cost self.total_quality_losspropertydef cost_per_piece(self) - float:单件平均成本total_pieces sum(sum(allocation.values()) * 1000 / 4.2 # 粗略估算for allocation in self.allocation_plan.values())return self.total_comprehensive_cost / total_pieces if total_pieces 0 else 0class SupplierAllocationOptimizer:供应商分配优化器核心类设计模式: 策略模式 外观模式参考: 北理工《运筹学》§2.3 线性规划建模def __init__(self, materials: List[MaterialConfig],suppliers: List[SupplierConfig]):初始化优化器Args:materials: 物料配置列表suppliers: 供应商配置列表self.materials {m.id: m for m in materials}self.suppliers {s.id: s for s in suppliers}self._validate_config()def _validate_config(self) - None:验证配置有效性if len(self.materials) 0:raise ValueError(物料列表不能为空)if len(self.suppliers) 0:raise ValueError(供应商列表不能为空)# 检查总需求是否超过总产能total_demand sum(m.total_weight for m in self.materials.values())total_capacity sum(s.capacity for s in self.suppliers.values())if total_demand total_capacity:raise ValueError(f总需求{total_demand:.1f}吨超过总产能{total_capacity:.1f}吨)def optimize(self, solver_timeout: int 60) - OptimizationResult:执行线性规划求解Args:solver_timeout: 求解器超时时间(秒)Returns:OptimizationResult: 优化结果print(\n 正在构建线性规划模型...)# 1. 创建线性规划问题最小化成本prob pulp.LpProblem(Supplier_Allocation_Optimization, pulp.LpMinimize)# 2. 定义决策变量采购量单位吨x pulp.LpVariable.dicts(Purchase,(self.materials.keys(), self.suppliers.keys()),lowBound0,catContinuous)# 3. 定义目标函数最小化综合成本total_cost, total_transport, total_quality_loss self._build_objective_function(prob, x)# 4. 添加约束条件self._add_demand_constraints(prob, x)self._add_capacity_constraints(prob, x)self._add_quality_constraints(prob, x)self._add_risk_constraints(prob, x)print( 模型构建完成开始求解...)print(f • 决策变量数: {len(x) * len(list(x.values())[0])})print(f • 约束条件数: {len(prob.constraints)})# 5. 求解solver pulp.PULP_CBC_CMD(msgFalse, timeLimitsolver_timeout)prob.solve(solver)# 6. 解析结果status pulp.LpStatus[prob.status]total_cost_value pulp.value(prob.objective) or 0.0# 7. 提取分配方案allocation_plan {}for i in self.materials.keys():allocation_plan[i] {}for j in self.suppliers.keys():allocation_plan[i][j] x[i][j].varValue or 0.0# 8. 计算供应商利用率supplier_utilization self._calculate_supplier_utilization(allocation_plan)# 9. 计算物料满足情况material_satisfaction self._calculate_material_satisfaction(allocation_plan)# 10. 检查约束状态constraint_status self._check_constraints(allocation_plan, supplier_utilization, material_satisfaction)# 11. 重新计算分项成本更精确total_transport_value self._calculate_transport_cost(allocation_plan)total_quality_loss_value self._calculate_quality_loss_cost(allocation_plan)# 12. 求解器统计solver_stats {variables: len(x) * len(list(x.values())[0]),constraints: len(prob.constraints),objective_value: total_cost_value,solve_time: solver_timeout}return OptimizationResult(scenario_name线性规划最优分配,statusstatus,total_costtotal_cost_value,total_transport_costtotal_transport_value,total_quality_losstotal_quality_loss_value,allocation_planallocation_plan,supplier_utilizationsupplier_utilization,material_satisfactionmaterial_satisfaction,constraint_statusconstraint_status,solver_statssolver_stats)def _build_objective_function(self, prob, x) - Tuple[pulp.LpAffineExpression, float, float]:构建目标函数最小化综合成本cost_terms []transport_terms []quality_terms []for i, material in self.materials.items():for j, supplier in self.suppliers.items():# 基础成本单价cost_terms.append(supplier.unit_price * x[i][j])# 运输成本按吨计算transport_terms.append(supplier.transport_cost * x[i][j] * (material.unit_weight / 1000))# 质量损失成本if not supplier.is_qualified_for(material):loss_per_ton supplier.calculate_quality_loss(material)quality_terms.append(loss_per_ton * x[i][j])total_cost pulp.lpSum(cost_terms)total_transport pulp.lpSum(transport_terms)total_quality_loss pulp.lpSum(quality_terms)# 综合成本 基础成本 运输成本 质量损失成本comprehensive_cost total_cost total_transport total_quality_lossprob comprehensive_cost, Total_Comprehensive_Costreturn comprehensive_cost, total_transport, total_quality_lossdef _add_demand_constraints(self, prob, x) - None:添加需求约束for i, material in self.materials.items():prob pulp.lpSum([x[i][j] for j in self.suppliers.keys()]) material.total_weight, \fDemand_{i}def _add_capacity_constraints(self, prob, x) - None:添加产能约束for j, supplier in self.suppliers.items():prob pulp.lpSum([x[i][j] for i in self.materials.keys()]) supplier.capacity, \fCapacity_{j}def _add_quality_constraints(self, prob, x) - None:添加质量约束简化通过大M法处理# 简化实现只允许合格供应商供应对应物料for i, material in self.materials.items():for j, supplier in self.suppliers.items():if not supplier.is_qualified_for(material):# 如果供应商不合格强制分配量为0prob x[i][j] 0, fQuality_{i}_{j}def _add_risk_constraints(self, prob, x) - None:添加风险分散约束total_demand sum(m.total_weight for m in self.materials.values())for j, supplier in self.suppliers.items():# 最小合作比例约束min_share supplier.min_cooperation_ratio * total_demandprob pulp.lpSum([x[i][j] for i in self.materials.keys()]) min_share, \fMinShare_{j}# 最大风险比例约束max_share supplier.max_risk_ratio * total_demandprob pulp.lpSum([x[i][j] for i in self.materials.keys()]) max_share, \fMaxShare_{j}def _calculate_supplier_utilization(self, allocation_plan: Dict[str, Dict[str, float]]) - Dict[str, float]:计算供应商利用率utilization {}for j, supplier in self.suppliers.items():total_allocated sum(allocation_plan[i][j] for i in self.materials.keys())utilization[j] (total_allocated / supplier.capacity) * 100return utilizationdef _calculate_material_satisfaction(self, allocation_plan: Dict[str, Dict[str, float]]) - Dict[str, float]:计算物料满足情况satisfaction {}for i, material in self.materials.items():total_allocated sum(allocation_plan[i][j] for j in self.suppliers.keys())satisfaction[i] total_allocatedreturn satisfactiondef _check_constraints(self, allocation_plan: Dict[str, Dict[str, float]],supplier_utilization: Dict[str, float],material_satisfaction: Dict[str, float]) - Dict[str, bool]:检查约束满足情况status {}# 需求约束for i, material in self.materials.items():satisfied abs(material_satisfaction[i] - material.total_weight) 1e-3status[fdemand_ok_{i}] satisfied# 产能约束for j, supplier in self.suppliers.items():total_allocated sum(allocation_plan[i][j] for i in self.materials.keys())satisfied total_allocated supplier.capacity * 1.001 # 容差status[fcapacity_ok_{j}] satisfied# 质量约束for i, material in self.materials.items():for j, supplier in self.suppliers.items():if allocation_plan[i][j] 1e-6: # 有分配量satisfied supplier.is_qualified_for(material)status[fquality_ok_{i}_{j}] satisfied# 风险分散约束total_demand sum(m.total_weight for m in self.materials.values())for j, supplier in self.suppliers.items():total_allocated sum(allocation_plan[i][j] for i in self.materials.keys())min_share supplier.min_cooperation_ratio * total_demandmax_share supplier.max_risk_ratio * total_demandstatus[fmin_share_ok_{j}] total_allocated min_share * 0.999status[fmax_share_ok_{j}] total_allocated max_share * 1.001return statusdef _calculate_transport_cost(self, allocation_plan: Dict[str, Dic利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛