第2239篇:供应链智能优化——用AI预测库存和需求的工程框架
第2239篇:供应链智能优化——用AI预测库存和需求的工程框架
适读人群:供应链技术团队、Java后端工程师、电商和制造业技术负责人 | 阅读时长:约16分钟 | 核心价值:系统讲解需求预测与库存优化的完整工程框架,解决库存积压和缺货并存的核心矛盾
上个季度有个做家电电商的朋友来找我聊,他们运营组在抱怨两件事:一是仓库里一堆空调滞销,占用了大量资金;二是某款新品上市后爆卖,补货跟不上,大量订单取消,损失了几百万的销售额。
我问他们现在怎么预测库存的。他说,主要靠经验——采购经理根据去年同期销量加一个主观系数,然后再看一下促销计划,最后拍脑袋定个数。
这基本上是大部分中等规模电商公司的现状。说起来是"经验",其实是对历史数据的一种粗糙记忆,没有系统性地考虑季节效应、促销活动、竞品动态、外部经济环境这些因素。
结果就是:库存积压和缺货同时存在,两个问题互相独立,互不相关,但都很痛。
供应链AI的核心问题拆解
需求预测和库存优化是两个相关但不同的问题:
需求预测:给定历史销量和外部特征,预测未来T天的需求量。这是个回归/时序预测问题。
库存决策:给定需求预测(有不确定性),决定何时补货、补多少。这是个优化问题,需要考虑缺货成本、持货成本、补货周期等约束。
需求预测:多模型集成框架
单一模型很难覆盖所有商品类型。我们用集成框架,针对不同特性的商品选择不同模型:
@Service
public class DemandForecastingService {
@Autowired
private ProphetForecastClient prophetClient; // 处理季节性强的商品
@Autowired
private XGBoostForecastClient xgboostClient; // 处理特征丰富的商品
@Autowired
private LSTMForecastClient lstmClient; // 处理非线性时序模式
@Autowired
private SkuFeatureRepository skuFeatureRepo;
@Autowired
private SalesHistoryRepository salesHistoryRepo;
/**
* 预测单品未来30天的日销量
*/
public ForecastResult forecast(String skuId, int horizonDays) {
SkuFeature skuFeature = skuFeatureRepo.findById(skuId)
.orElseThrow(() -> new SkuNotFoundException(skuId));
// 获取历史销售数据(至少需要90天)
List<DailySales> history = salesHistoryRepo
.findBySkuIdOrderByDate(skuId, LocalDate.now().minusDays(365));
if (history.size() < 90) {
// 新品或数据不足:使用类似品销售数据做迁移
return forecastNewItem(skuId, skuFeature, horizonDays);
}
// 选择预测策略
ForecastStrategy strategy = selectStrategy(skuFeature, history);
return switch (strategy) {
case SEASONAL -> forecastWithProphet(skuId, history, horizonDays);
case FEATURE_RICH -> forecastWithXGBoost(skuId, history, horizonDays);
case ENSEMBLE -> forecastWithEnsemble(skuId, history, horizonDays);
};
}
private ForecastStrategy selectStrategy(SkuFeature sku, List<DailySales> history) {
// 季节性强(CV > 0.5):用Prophet
double cv = calculateCoefficientOfVariation(history);
boolean hasStrongSeasonality = detectSeasonality(history);
boolean hasRichFeatures = sku.getPromotionHistory().size() > 10;
if (hasStrongSeasonality && cv > 0.5) {
return ForecastStrategy.SEASONAL;
} else if (hasRichFeatures) {
return ForecastStrategy.FEATURE_RICH;
} else {
return ForecastStrategy.ENSEMBLE;
}
}
/**
* 集成预测:加权平均多个模型的结果
*/
private ForecastResult forecastWithEnsemble(String skuId,
List<DailySales> history,
int horizonDays) {
// 并行调用三个模型
CompletableFuture<ForecastResult> prophetFuture =
CompletableFuture.supplyAsync(() -> prophetClient.forecast(skuId, history, horizonDays));
CompletableFuture<ForecastResult> xgboostFuture =
CompletableFuture.supplyAsync(() -> xgboostClient.forecast(skuId, history, horizonDays));
CompletableFuture<ForecastResult> lstmFuture =
CompletableFuture.supplyAsync(() -> lstmClient.forecast(skuId, history, horizonDays));
// 等待所有结果
CompletableFuture.allOf(prophetFuture, xgboostFuture, lstmFuture).join();
try {
ForecastResult prophet = prophetFuture.get();
ForecastResult xgboost = xgboostFuture.get();
ForecastResult lstm = lstmFuture.get();
// 加权集成:权重基于各模型的历史MAE反比
double[] weights = calculateModelWeights(skuId);
return weightedEnsemble(List.of(prophet, xgboost, lstm), weights);
} catch (Exception e) {
log.error("集成预测异常,降级到Prophet单模型", e);
return prophetClient.forecast(skuId, history, horizonDays);
}
}
/**
* 基于历史验证误差计算模型权重
* MAE越小,权重越大
*/
private double[] calculateModelWeights(String skuId) {
List<ModelValidationScore> scores = modelScoreRepo.findBySkuId(skuId);
if (scores.isEmpty()) {
return new double[]{0.33, 0.33, 0.34}; // 均等权重
}
double prophetMAE = getMAE(scores, "prophet");
double xgboostMAE = getMAE(scores, "xgboost");
double lstmMAE = getMAE(scores, "lstm");
// 反比权重归一化
double totalInvMAE = 1/prophetMAE + 1/xgboostMAE + 1/lstmMAE;
return new double[]{
(1/prophetMAE) / totalInvMAE,
(1/xgboostMAE) / totalInvMAE,
(1/lstmMAE) / totalInvMAE
};
}
}特征工程:把业务知识编码进模型
需求预测的上限是特征工程的质量。除了历史销量,需要加入大量业务特征:
@Service
public class ForecastFeatureBuilder {
@Autowired
private PromotionRepository promotionRepo;
@Autowired
private HolidayCalendarService holidayCalendar;
@Autowired
private WeatherForecastClient weatherClient;
/**
* 为预测窗口构建特征矩阵
* 每行是一天,每列是一个特征
*/
public double[][] buildFeatureMatrix(String skuId, LocalDate startDate, int days) {
double[][] features = new double[days][getFeatureCount()];
for (int i = 0; i < days; i++) {
LocalDate date = startDate.plusDays(i);
features[i] = buildDayFeatures(skuId, date);
}
return features;
}
private double[] buildDayFeatures(String skuId, LocalDate date) {
List<Double> featureList = new ArrayList<>();
// 1. 时间特征
featureList.add((double) date.getDayOfWeek().getValue());
featureList.add((double) date.getDayOfMonth());
featureList.add((double) date.getMonthValue());
featureList.add(date.getDayOfWeek().getValue() >= 6 ? 1.0 : 0.0); // 是否周末
// 2. 节假日特征
HolidayInfo holiday = holidayCalendar.getHolidayInfo(date);
featureList.add(holiday.isHoliday() ? 1.0 : 0.0);
featureList.add((double) holiday.getDaysToNextHoliday());
featureList.add((double) holiday.getDaysAfterLastHoliday());
featureList.add(holiday.isPreHolidayPeak() ? 1.0 : 0.0); // 节前冲销
// 3. 促销特征
List<PromotionEvent> promotions = promotionRepo
.findBySkuIdAndDate(skuId, date);
featureList.add(promotions.isEmpty() ? 0.0 : 1.0);
featureList.add(promotions.stream()
.mapToDouble(PromotionEvent::getDiscountRate).max().orElse(0.0));
featureList.add(promotions.stream()
.anyMatch(p -> p.getType() == PromotionType.FLASH_SALE) ? 1.0 : 0.0);
// 4. 价格特征
PriceHistory price = priceRepo.findBySkuIdAndDate(skuId, date);
featureList.add(price != null ? price.getCurrentPrice() : 0.0);
featureList.add(price != null ? price.getPriceChangePct() : 0.0);
// 5. 季节和天气(对服装、食品类商品影响大)
WeatherForecast weather = weatherClient.getForecast(date);
featureList.add(weather.getTemperature());
featureList.add(weather.isRainy() ? 1.0 : 0.0);
return featureList.stream().mapToDouble(Double::doubleValue).toArray();
}
}库存优化引擎:从预测到决策
有了需求预测,还需要把它转化为实际的补货决策:
@Service
public class InventoryOptimizationService {
@Autowired
private DemandForecastingService forecastingService;
@Autowired
private InventoryRepository inventoryRepo;
@Autowired
private SupplierRepository supplierRepo;
/**
* 计算品类的安全库存和补货点
* 基于服务水平目标(如95%的订单不缺货)
*/
public ReplenishmentPlan calculateReplenishmentPlan(String skuId) {
SkuInventory currentInventory = inventoryRepo.findBySkuId(skuId);
Supplier supplier = supplierRepo.findBySkuId(skuId);
// 预测提前期内的需求(含不确定性)
int leadTimeDays = supplier.getLeadTimeDays();
ForecastResult forecast = forecastingService.forecast(skuId, leadTimeDays + 30);
// 计算安全库存
// 公式:Z * σ_demand * sqrt(leadTime) + Z * μ_demand * σ_leadTime
double serviceLevel = 0.95; // 95%服务水平
double zScore = getNormalZScore(serviceLevel); // ≈ 1.645
double demandMean = forecast.getMeanDemand(leadTimeDays);
double demandStd = forecast.getStdDemand(leadTimeDays);
double leadTimeStd = supplier.getLeadTimeStdDays();
double safetyStock = zScore * Math.sqrt(
Math.pow(demandStd, 2) * leadTimeDays +
Math.pow(demandMean, 2) * Math.pow(leadTimeStd, 2)
);
// 补货点 = 提前期内平均需求 + 安全库存
double reorderPoint = demandMean * leadTimeDays + safetyStock;
// 经济订货量(EOQ)
double annualDemand = forecast.getAnnualDemand();
double orderingCost = supplier.getOrderingCost();
double holdingCostRate = 0.25; // 年持有成本率25%
double unitCost = currentInventory.getUnitCost();
double eoq = Math.sqrt(
(2 * annualDemand * orderingCost) / (holdingCostRate * unitCost)
);
// 考虑最小起订量约束
double orderQuantity = Math.max(eoq, supplier.getMinOrderQuantity());
// 判断是否需要触发补货
boolean needReplenish = currentInventory.getAvailableQuantity() <= reorderPoint;
return ReplenishmentPlan.builder()
.skuId(skuId)
.currentStock(currentInventory.getAvailableQuantity())
.safetyStock(safetyStock)
.reorderPoint(reorderPoint)
.recommendedOrderQuantity(needReplenish ? orderQuantity : 0)
.urgency(calculateUrgency(currentInventory, reorderPoint))
.estimatedStockoutDate(estimateStockoutDate(currentInventory, forecast))
.build();
}
/**
* 批量优化:平衡整体资金占用和服务水平
* 在总预算约束下,优化资金在各SKU间的分配
*/
public List<ReplenishmentPlan> optimizePortfolio(List<String> skuIds,
double totalBudget) {
List<ReplenishmentPlan> plans = skuIds.parallelStream()
.map(this::calculateReplenishmentPlan)
.collect(Collectors.toList());
// 计算各SKU的补货总成本
double totalCost = plans.stream()
.mapToDouble(p -> p.getRecommendedOrderQuantity() *
getUnitCost(p.getSkuId()))
.sum();
if (totalCost <= totalBudget) {
return plans; // 预算充足,全部执行
}
// 预算不足:按ROI优先级排序削减
// ROI = (避免缺货的销售损失) / (补货成本)
return plans.stream()
.filter(p -> p.getRecommendedOrderQuantity() > 0)
.sorted(Comparator.comparingDouble(this::calculateROI).reversed())
.collect(Collectors.toList());
// 实际截断逻辑略,需要解背包问题
}
private LocalDate estimateStockoutDate(SkuInventory inventory, ForecastResult forecast) {
double stock = inventory.getAvailableQuantity();
double cumDemand = 0;
for (int day = 0; day < forecast.getHorizonDays(); day++) {
cumDemand += forecast.getDailyDemand(day);
if (cumDemand >= stock) {
return LocalDate.now().plusDays(day);
}
}
return null; // 预测期内不会断货
}
}促销场景的特殊处理
大促期间(双11、618)的需求会有几十倍的峰值,普通预测模型完全失效:
@Service
public class PromotionForecastService {
/**
* 大促需求预测:基于历史大促数据和当次促销配置
* 关键变量:折扣力度、坑位资源、品类排名
*/
public PromotionForecast forecastPromotion(String skuId,
PromotionConfig config) {
// 查找历史相似促销事件
List<HistoricalPromotion> similarPromos = findSimilarPromotions(skuId, config);
if (similarPromos.isEmpty()) {
// 新品或首次参与大促:用类目均值估算
return forecastByCategory(skuId, config);
}
// 计算提升系数(lift):大促销量/日常销量
double avgLift = similarPromos.stream()
.mapToDouble(p -> p.getPromotionSales() / p.getBaselineDailySales())
.average().orElse(1.0);
// 按折扣力度调整:折扣越大,提升系数越高(弹性系数)
double discountElasticity = calculateDiscountElasticity(skuId, similarPromos);
double adjustedLift = avgLift * Math.pow(
config.getDiscountRate() / getHistoricalAvgDiscount(similarPromos),
discountElasticity
);
// 基准日均销量 × 活动天数 × 提升系数
double baselineDailySales = getDailyBaselineSales(skuId);
double estimatedSales = baselineDailySales * config.getDurationDays() * adjustedLift;
return PromotionForecast.builder()
.skuId(skuId)
.estimatedSales(estimatedSales)
.liftFactor(adjustedLift)
.confidenceLevel(calculateConfidence(similarPromos.size()))
.recommendedStock(estimatedSales * 1.2) // 留20%缓冲
.build();
}
}效果评估与持续改进
供应链AI的核心指标是这几个:
- 预测准确率(MAPE):Mean Absolute Percentage Error,目标控制在15%以内
- 库存周转天数:越低越好,但不能低到缺货
- 服务水平(In-Stock Rate):目标95%以上的订单不因缺货取消
- 呆滞库存比例:90天无销售的SKU占总库存的比例
在系统运行了三个月之后,那个家电电商朋友反馈:库存积压金额下降了28%,同时缺货率从12%降到了4%。更重要的是,采购团队的工作量减少了——不再需要每周开几个小时的库存会议拍脑袋,系统自动生成补货清单,人只需要审核异常情况。
这才是AI工具真正的价值:不是替代人的判断,而是把常规决策自动化,让人有精力处理真正需要判断的复杂情况。
