第2279篇:零售AI工程——从需求预测到门店数字化运营的完整实践
2026/4/30大约 6 分钟
第2279篇:零售AI工程——从需求预测到门店数字化运营的完整实践
适读人群:零售技术工程师、Java后端开发者、新零售和连锁零售技术团队 | 阅读时长:约17分钟 | 核心价值:解决零售AI的数据质量和预测准确性问题,构建真正能用的智能补货和运营系统
连锁超市的AI补货系统,是我做过最"摔跤"的项目之一。
第一版上线效果很差:大概有20%的SKU,补货量要么严重过剩(最后打折清货),要么严重不足(断货)。
排查了一个月,找到三个根本原因:
原因1:没有考虑天气数据。下雨前后,雨伞、雨衣、热饮的需求会显著变化。我们的模型只有历史销量,没有天气特征。
原因2:促销活动影响没有隔离。有促销时销量暴增,模型把这个当成了正常需求,导致后续预测偏高。
原因3:门店间的数据差异没有建模。同一SKU在不同门店的销售节奏完全不同,用全局模型无法捕捉门店特性。
这三个教训,后来成了我做零售AI项目的标准检查清单。
零售AI的核心系统
零售AI不是一个系统,而是三个相互关联的系统:
1. 需求预测系统
输入:历史销量 + 天气 + 节假日 + 促销计划 + 竞品信息
输出:未来N天每个SKU的预测销量
2. 智能补货系统
输入:预测销量 + 当前库存 + 采购提前期 + 安全库存策略
输出:补货建议(补什么、补多少、什么时候补)
3. 门店运营分析系统
输入:销售数据 + 客流数据 + 货架陈列数据
输出:热区分析、商品陈列优化建议、损耗预警需求预测系统的工程实现
/**
* 零售需求预测服务
* 集成多维度特征的时序预测
*/
@Service
@Slf4j
public class DemandForecastService {
private final TimeSeriesModelClient forecastClient;
private final WeatherDataService weatherService;
private final PromotionCalendarService promotionCalendar;
private final HolidayCalendarService holidayCalendar;
private final SaleHistoryRepository saleHistory;
/**
* 为指定SKU在指定门店生成未来7天的需求预测
*/
public List<DailyForecast> forecast(
String skuId,
String storeId,
LocalDate startDate,
int forecastDays) {
// 1. 获取历史销量(最近90天)
List<DailySale> historicalSales = saleHistory.getHistory(
skuId, storeId, startDate.minusDays(90), startDate.minusDays(1));
// 2. 去除促销期间的异常值(避免模型被促销数据"污染")
List<DailySale> cleanedSales = removePromotionOutliers(historicalSales, storeId);
// 3. 获取预测期间的天气预报
List<WeatherForecast> weatherForecasts = weatherService.getForecast(
storeId, startDate, forecastDays);
// 4. 获取节假日和促销计划
List<Holiday> holidays = holidayCalendar.get(startDate, forecastDays);
List<PromotionEvent> promotions = promotionCalendar.getPlanned(
skuId, storeId, startDate, forecastDays);
// 5. 构建特征矩阵
List<ForecastFeatures> featureMatrix = buildFeatureMatrix(
cleanedSales, weatherForecasts, holidays, promotions, startDate, forecastDays);
// 6. 调用预测模型
ForecastRequest request = ForecastRequest.builder()
.skuId(skuId)
.storeId(storeId)
.historicalData(cleanedSales)
.futureFeatures(featureMatrix)
.startDate(startDate)
.forecastDays(forecastDays)
.build();
List<ForecastPoint> rawForecasts = forecastClient.predict(request);
// 7. 结合促销活动调整预测
return applyPromotionAdjustments(rawForecasts, promotions);
}
/**
* 识别并剔除促销导致的异常销量
*
* 促销期间的销量不代表真实需求,需要用统计方法识别并剔除
*/
private List<DailySale> removePromotionOutliers(
List<DailySale> sales, String storeId) {
// 计算基准销量(去掉前10%和后10%极值后的均值)
List<Double> sortedQuantities = sales.stream()
.map(s -> (double) s.getQuantity())
.sorted()
.collect(Collectors.toList());
int trimSize = sortedQuantities.size() / 10;
double baseline = sortedQuantities.subList(trimSize, sortedQuantities.size() - trimSize)
.stream().mapToDouble(d -> d).average().orElse(0);
double std = computeStd(sortedQuantities.subList(trimSize,
sortedQuantities.size() - trimSize), baseline);
// 超过均值3倍标准差的,替换为基准值
return sales.stream()
.map(sale -> {
if (sale.getQuantity() > baseline + 3 * std) {
return sale.withQuantity((int) baseline);
}
return sale;
})
.collect(Collectors.toList());
}
}智能补货系统
/**
* 智能补货计算服务
*/
@Service
public class SmartReplenishmentService {
private final DemandForecastService forecastService;
private final InventoryRepository inventoryRepo;
private final SupplierLeadTimeRepository leadTimeRepo;
/**
* 计算补货建议
*/
public ReplenishmentOrder calculateReplenishment(
String skuId,
String storeId,
LocalDate calcDate) {
// 当前库存
int currentStock = inventoryRepo.getCurrentStock(skuId, storeId);
// 采购提前期(从下单到到货的天数)
int leadTimeDays = leadTimeRepo.getLeadTime(skuId);
// 预测需求(预测期 = 提前期 + 安全库存覆盖天数)
int forecastDays = leadTimeDays + SAFETY_STOCK_DAYS;
List<DailyForecast> forecasts = forecastService.forecast(
skuId, storeId, calcDate, forecastDays);
// 提前期内的总需求
int demandDuringLeadTime = forecasts.subList(0, leadTimeDays)
.stream().mapToInt(DailyForecast::getQuantity).sum();
// 安全库存(基于需求波动性计算)
int safetyStock = calculateSafetyStock(forecasts, SERVICE_LEVEL_TARGET);
// 再订货点 = 提前期需求 + 安全库存
int reorderPoint = demandDuringLeadTime + safetyStock;
if (currentStock > reorderPoint) {
// 库存充足,不需要补货
return ReplenishmentOrder.noReplenishmentNeeded(skuId, storeId);
}
// 经济订货量(EOQ)
int totalForecastDemand = forecasts.stream()
.mapToInt(DailyForecast::getQuantity).sum();
int orderQuantity = calculateEOQ(skuId, totalForecastDemand, leadTimeDays);
return ReplenishmentOrder.builder()
.skuId(skuId)
.storeId(storeId)
.orderQuantity(orderQuantity)
.orderDate(calcDate)
.expectedArrivalDate(calcDate.plusDays(leadTimeDays))
.currentStock(currentStock)
.reorderPoint(reorderPoint)
.forecastedDemand(totalForecastDemand)
.confidence(calculateConfidence(forecasts))
.requiresManagerApproval(orderQuantity > HIGH_VALUE_THRESHOLD) // 大额订单需要人工确认
.build();
}
/**
* 计算安全库存
* 基于历史需求波动性和目标服务水平
*/
private int calculateSafetyStock(List<DailyForecast> forecasts, double serviceLevel) {
// 需求标准差
double mean = forecasts.stream().mapToInt(DailyForecast::getQuantity)
.average().orElse(0);
double std = Math.sqrt(forecasts.stream()
.mapToDouble(f -> Math.pow(f.getQuantity() - mean, 2))
.average().orElse(0));
// z-score对应服务水平(95% → 1.645, 99% → 2.326)
double zScore = serviceLevel == 0.99 ? 2.326 : 1.645;
return (int) Math.ceil(zScore * std * Math.sqrt(SAFETY_STOCK_DAYS));
}
private static final int SAFETY_STOCK_DAYS = 3;
private static final double SERVICE_LEVEL_TARGET = 0.95; // 95%服务水平
private static final int HIGH_VALUE_THRESHOLD = 50000; // 5万以上需要人工审核
}门店热区分析
/**
* 门店热区分析服务
* 基于客流和销售数据,分析门店内商品的位置效益
*/
@Service
public class StoreHeatmapService {
private final TrafficCounterRepository trafficRepo;
private final ShelfSalesRepository shelfSalesRepo;
/**
* 生成门店热区报告
* 分析哪些货架区域人流量高但销售低(陈列优化机会)
*/
public StoreHeatmapReport analyze(String storeId, LocalDate reportDate) {
// 获取各区域客流量
Map<String, Integer> zoneTraffic = trafficRepo.getZoneTraffic(storeId, reportDate);
// 获取各区域销售额
Map<String, Double> zoneSales = shelfSalesRepo.getZoneSales(storeId, reportDate);
List<ZoneAnalysis> zoneAnalyses = new ArrayList<>();
for (Map.Entry<String, Integer> entry : zoneTraffic.entrySet()) {
String zoneId = entry.getKey();
int traffic = entry.getValue();
double sales = zoneSales.getOrDefault(zoneId, 0.0);
// 销售转化率 = 销售额 / 客流量
double conversionRate = traffic > 0 ? sales / traffic : 0;
ZoneType type;
String recommendation;
if (traffic > HIGH_TRAFFIC_THRESHOLD && conversionRate < LOW_CONVERSION_THRESHOLD) {
// 高人流低转化:黄金区域利用不充分
type = ZoneType.HIGH_TRAFFIC_LOW_CONVERSION;
recommendation = "高价值区域,建议放置高利润率商品或主推商品";
} else if (traffic < LOW_TRAFFIC_THRESHOLD && conversionRate > HIGH_CONVERSION_THRESHOLD) {
// 低人流高转化:目标客群明确但曝光不足
type = ZoneType.LOW_TRAFFIC_HIGH_CONVERSION;
recommendation = "商品受欢迎但位置偏僻,可考虑迁至主通道";
} else {
type = ZoneType.NORMAL;
recommendation = null;
}
zoneAnalyses.add(ZoneAnalysis.builder()
.zoneId(zoneId)
.trafficCount(traffic)
.salesAmount(sales)
.conversionRate(conversionRate)
.zoneType(type)
.recommendation(recommendation)
.build());
}
// 用LLM生成整体运营建议
String overallRecommendation = generateOverallRecommendation(storeId, zoneAnalyses);
return StoreHeatmapReport.builder()
.storeId(storeId)
.reportDate(reportDate)
.zoneAnalyses(zoneAnalyses)
.overallRecommendation(overallRecommendation)
.generatedAt(Instant.now())
.build();
}
}实践总结
数据质量是零售AI的命脉
促销数据、缺货记录、退货数据——这些都是正确预测的必要前提。很多零售企业的历史数据残缺不全(缺货时记录为0销量,但这个0是"卖完了"还是"没人买"无法区分)。数据清洗往往比建模更费时间。
不要追求最优,要追求稳健
补货模型不需要预测精度达到99%,而是需要在各种异常情况下(节假日、天气、新品上线、促销活动)都不出大问题。稳健性比精确性更重要。
人工审核仍然必要
所有大额补货建议,必须有采购经理的人工确认。AI系统出的建议是"参考",不是"命令"。特别是新品上线、季节性商品这类历史数据不足的场景,AI的把握不大。
