AI在电商价格策略中的应用:动态定价模型与实时调价的后端引擎

一、动态定价的业务背景

电商平台的商品定价已经从"运营手工改价"演进到"算法自动调价"。驱动这一变化的核心原因是定价因素的复杂度爆炸:一个爆款商品的价格受竞品价格(实时变化)、库存水位(动态变化)、时段流量(高峰/低谷)、用户画像(新客/老客/价格敏感度)、促销活动(平台券/店铺券/满减)等多重因素影响。人工调价的频率通常是每天 1~2 次,远跟不上竞品的分钟级调价节奏。

以某服装品类的实际数据为例:竞品 A 在晚上 8~10 点(流量高峰)降价 5%,如果平台在 3 小时后才跟进调价,已经错过了 80% 的高峰流量。而提前预判并自动调价的商品,在高峰时段的转化率提升了 18%。

动态定价的后端系统需要回答三个核心问题:什么时候调(触发时机)、调到多少(目标价格)、如何安全调(防错与回滚)。

二、定价模型的设计

2.1 定价因素的量化建模

定价模型需要考虑的因素可以分为内部和外部两大类:

2.2 规则定价 vs 模型定价

规则定价和模型定价各有适用场景,ROI 对比决定了选择:

维度 规则定价 模型定价
响应速度 毫秒级(简单规则匹配) 毫秒~秒级(特征计算+模型推理)
调价精度 阶梯式(降5%/10%/15%) 连续值(降3.7%)
竞品响应 被动跟随 预判竞品调价方向
ROI 基准线 比规则定价高 8%~15%
可解释性 强("因为竞品降价所以跟降") 弱(特征重要性可解释但不够直观)
适用商品 标品(3C、图书) 非标品(服装、家居)

实践中我们采用双层策略:规则层覆盖明确场景(低于成本价不卖、大促统一折扣),模型层处理灰度场景(竞品微调时是否跟进、库存积压时降多少):

class HybridPricingEngine:
    """规则 + 模型 混合定价引擎"""
    
    def __init__(self):
        self.rule_engine = RulePricingEngine()
        self.model = PricingModel()
    
    def calculate_price(self, product: dict, context: dict) -> dict:
        # 第一层:规则引擎(硬约束)
        rule_result = self.rule_engine.evaluate(product, context)
        
        # 硬约束:任何情况下都不能突破
        constraints = {
            "min_price": product["cost_price"] * 0.95,  # 最低:成本的95%
            "max_price": product["guide_price"] * 1.2,  # 最高:指导价的120%
            "is_promotion": context.get("promotion_active", False),
            "promotion_discount": context.get("max_promotion_discount", 0)
        }
        
        if rule_result["action"] == "FORCE":
            # 强制规则(如大促统一定价)
            return {
                "price": max(constraints["min_price"], 
                            min(constraints["max_price"], rule_result["price"])),
                "source": "RULE_FORCE",
                "confidence": 1.0
            }
        
        # 第二层:模型定价(灰度优化)
        model_result = self.model.predict(product, context)
        
        # 价格约束裁剪
        final_price = max(constraints["min_price"],
                         min(constraints["max_price"], model_result["price"]))
        
        # 调价幅度控制:单次调价不超过15%
        current_price = product["current_price"]
        max_change = current_price * 0.15
        if abs(final_price - current_price) > max_change:
            final_price = current_price + (
                max_change if final_price > current_price else -max_change
            )
        
        return {
            "price": round(final_price, 2),
            "source": "MODEL",
            "confidence": model_result["confidence"],
            "feature_importance": model_result.get("feature_importance", {}),
            "rule_constraints_applied": [
                k for k, v in constraints.items() 
                if final_price != model_result["price"]
            ]
        }

2.3 定价模型的训练与在线更新

import lightgbm as lgb
import pandas as pd

class PricingModel:
    """基于 LightGBM 的动态定价模型"""
    
    def __init__(self):
        self.model: lgb.Booster = None
        self.feature_names = [
            # 商品特征
            "cost_price", "current_price", "guide_price",
            "stock_days",  # 库存可售天数
            "sales_velocity_7d",  # 7天日均销量
            "margin_rate",
            # 竞品特征
            "competitor_min_price", "competitor_avg_price",
            "competitor_price_std", "competitor_count",
            "price_position",  # 当前价格在竞品中的分位数
            # 用户特征
            "avg_user_price_sensitivity",
            "repeat_purchase_rate",
            "conversion_rate_7d",
            # 时间特征
            "hour_of_day", "day_of_week",
            "is_promotion_day", "days_to_next_promotion",
            "season_factor"
        ]
    
    def train(self, df: pd.DataFrame):
        """训练定价模型"""
        X = df[self.feature_names]
        # 目标:最大化 revenue = price * predicted_quantity
        # 使用销售额变化率作为标签
        y = df["revenue_change_rate"]
        
        params = {
            "objective": "regression",
            "metric": "rmse",
            "num_leaves": 63,
            "learning_rate": 0.05,
            "feature_fraction": 0.8,
            "bagging_fraction": 0.8,
            "bagging_freq": 5,
            "min_data_in_leaf": 50,
            "verbosity": -1
        }
        
        self.model = lgb.train(
            params,
            lgb.Dataset(X, label=y),
            num_boost_round=200,
            valid_sets=[lgb.Dataset(X, label=y)],
            callbacks=[lgb.early_stopping(20)]
        )
    
    def predict(self, product: dict, context: dict) -> dict:
        """预测最优价格"""
        features = self._build_features(product, context)
        
        # 搜索最优价格:在候选价格范围内枚举
        current_price = product["current_price"]
        candidates = np.linspace(
            current_price * 0.85,
            current_price * 1.15,
            num=20
        )
        
        best_price = current_price
        best_revenue = 0
        best_confidence = 0
        
        for price in candidates:
            features["price_candidate"] = price
            features["price_change_ratio"] = (price - current_price) / current_price
            
            # 预测该价格下的销售额变化
            pred = self.model.predict(
                pd.DataFrame([features])[self.feature_names]
            )[0]
            
            if pred > best_revenue:
                best_revenue = pred
                best_price = price
                best_confidence = min(abs(pred) / 0.1, 1.0)
        
        return {
            "price": best_price,
            "expected_revenue_change": best_revenue,
            "confidence": best_confidence
        }

三、实时调价的后端引擎

3.1 调价的延迟与并发控制

调价操作对延迟和并发有严格要求。一个热门商品的"价格变更"事件需要在秒级内同步到所有前端(搜索、详情、推荐、购物车),否则会出现不同页面显示不同价格的严重问题:

核心代码实现:

@Service
public class PriceAdjustmentEngine {
    
    private final StringRedisTemplate redis;
    private final KafkaTemplate<String, PriceChangeEvent> kafka;
    
    /**
     * 调价操作:CAS保证并发安全,Kafka保证最终一致
     */
    public AdjustResult adjust(PricingDecision decision) {
        String priceKey = "price:" + decision.getProductId();
        String versionKey = "price:version:" + decision.getProductId();
        
        // 1. 调价前校验
        AdjustValidation validation = validate(decision);
        if (!validation.isPassed()) {
            return AdjustResult.reject(validation.getReason());
        }
        
        // 2. 乐观锁更新(CAS)
        Long currentVersion = Long.parseLong(
            redis.opsForValue().get(versionKey) != null 
                ? redis.opsForValue().get(versionKey) : "0"
        );
        
        // 使用 Lua 脚本保证原子性
        String lua = """
            local price_key = KEYS[1]
            local version_key = KEYS[2]
            local expected_version = tonumber(ARGV[1])
            local new_price = ARGV[2]
            local new_version = tonumber(ARGV[3])
            
            local current_version = tonumber(redis.call('GET', version_key) or '0')
            if current_version ~= expected_version then
                return {0, '版本冲突: 期望' .. expected_version .. ' 实际' .. current_version}
            end
            
            redis.call('SET', price_key, new_price)
            redis.call('SET', version_key, new_version)
            return {1, '更新成功'}
        """;
        
        Long newVersion = currentVersion + 1;
        List<Object> result = redis.execute(
            new DefaultRedisScript<>(lua, List.class),
            List.of(priceKey, versionKey),
            String.valueOf(currentVersion),
            decision.getNewPrice().toString(),
            String.valueOf(newVersion)
        );
        
        long code = ((Number) result.get(0)).longValue();
        if (code == 0) {
            // 版本冲突:可能有并发的调价,重新加载后重试
            return retryWithReload(decision);
        }
        
        // 3. 异步通知下游
        PriceChangeEvent event = PriceChangeEvent.builder()
            .productId(decision.getProductId())
            .oldPrice(decision.getOldPrice())
            .newPrice(decision.getNewPrice())
            .version(newVersion)
            .source(decision.getSource())
            .timestamp(System.currentTimeMillis())
            .build();
        
        kafka.send("price-change", decision.getProductId().toString(), event);
        
        // 4. 记录调价流水
        saveAdjustmentLog(decision, newVersion);
        
        return AdjustResult.success(newVersion);
    }
    
    /**
     * 调价前校验
     */
    private AdjustValidation validate(PricingDecision decision) {
        // 频次控制:同一商品5分钟内最多调价3次
        String freqKey = "price:freq:" + decision.getProductId();
        Long freq = redis.opsForList().size(freqKey);
        if (freq != null && freq >= 3) {
            return AdjustValidation.fail("调价频次超限: 5分钟内已调价" + freq + "次");
        }
        
        // 幅度控制:单次调价不超过15%
        BigDecimal oldPrice = decision.getOldPrice();
        BigDecimal newPrice = decision.getNewPrice();
        BigDecimal changeRatio = newPrice.subtract(oldPrice)
            .abs().divide(oldPrice, 4, RoundingMode.HALF_UP);
        
        if (changeRatio.compareTo(new BigDecimal("0.15")) > 0) {
            return AdjustValidation.fail(
                "调价幅度超限: " + changeRatio.multiply(new BigDecimal("100")) + "%"
            );
        }
        
        // 风控校验:不低于成本价
        if (newPrice.compareTo(decision.getCostPrice()) < 0) {
            return AdjustValidation.fail("价格低于成本价: " + decision.getCostPrice());
        }
        
        return AdjustValidation.pass();
    }
}

3.2 价格变动的审计与回滚

每次调价都必须留下完整的审计记录,出问题时可快速回滚:

CREATE TABLE price_adjustment_log (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_id BIGINT NOT NULL,
    old_price DECIMAL(10, 2) NOT NULL,
    new_price DECIMAL(10, 2) NOT NULL,
    change_ratio DECIMAL(6, 4) NOT NULL COMMENT '调价幅度(正=涨价,负=降价)',
    source VARCHAR(32) NOT NULL COMMENT 'RULE_FORCE/MODEL/MANUAL',
    confidence DECIMAL(4, 3) COMMENT '模型置信度',
    version BIGINT NOT NULL,
    operator VARCHAR(64) NOT NULL COMMENT '操作人/系统',
    reason TEXT COMMENT '调价原因(模型特征重要性等)',
    status VARCHAR(16) NOT NULL DEFAULT 'APPLIED',
    created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    
    KEY idx_product_time (product_id, created_at),
    KEY idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='价格调整流水审计表';
@Service
public class PriceRollbackService {
    
    /**
     * 价格回滚:恢复到指定版本
     */
    @Transactional
    public RollbackResult rollback(long productId, long targetVersion) {
        // 查找目标版本的价格记录
        PriceAdjustmentLog targetLog = logRepository
            .findByProductIdAndVersion(productId, targetVersion);
        
        if (targetLog == null) {
            return RollbackResult.fail("目标版本不存在");
        }
        
        // 查找之后的所有调价记录
        List<PriceAdjustmentLog> laterLogs = logRepository
            .findByProductIdAndVersionGreaterThan(productId, targetVersion);
        
        // 标记后续调价为已回滚
        for (PriceAdjustmentLog log : laterLogs) {
            log.setStatus("ROLLED_BACK");
            logRepository.updateStatus(log.getId(), "ROLLED_BACK");
        }
        
        // 恢复目标价格
        String priceKey = "price:" + productId;
        String versionKey = "price:version:" + productId;
        
        redis.opsForValue().set(priceKey, targetLog.getNewPrice().toString());
        redis.opsForValue().set(versionKey, String.valueOf(targetVersion + laterLogs.size() + 1));
        
        // 发布价格变更事件(通知下游恢复)
        PriceChangeEvent event = PriceChangeEvent.builder()
            .productId(productId)
            .oldPrice(findCurrentPrice(productId))
            .newPrice(targetLog.getNewPrice())
            .version(targetVersion + laterLogs.size() + 1)
            .source("ROLLBACK")
            .timestamp(System.currentTimeMillis())
            .build();
        kafka.send("price-change", String.valueOf(productId), event);
        
        return RollbackResult.success(targetLog.getNewPrice());
    }
}

四、AB实验与效果评估

4.1 调价策略的AB实验

评估定价策略需要非常谨慎的 AB 实验设计。价格直接影响 GMV、利润和用户体验,实验设计不当可能导致严重的财务损失:

@Component
public class PricingExperiment {
    
    /**
     * 分层随机实验:
     * - 对照组:人工调价
     * - 实验组A:规则定价
     * - 实验组B:模型定价
     * 
     * 分流粒度:商品级别(用户级别的价格差异会造成"杀熟"争议)
     */
    public String assignVariant(long productId) {
        // 商品级别hash,保证同一商品始终在同一组
        int hash = Hashing.murmur3_32()
            .hashLong(productId)
            .asInt();
        int bucket = Math.abs(hash) % 100;
        
        if (bucket < 50) return "control";      // 50%: 对照组
        if (bucket < 75) return "variant_a";     // 25%: 规则定价
        return "variant_b";                       // 25%: 模型定价
    }
    
    /**
     * 实验评估指标
     */
    public ExperimentReport evaluate(ExperimentVariant variant, 
                                      LocalDate startDate, LocalDate endDate) {
        // 核心指标
        BigDecimal totalGMV = calculateGMV(variant, startDate, endDate);
        BigDecimal totalProfit = calculateProfit(variant, startDate, endDate);
        long totalOrders = calculateOrderCount(variant, startDate, endDate);
        double conversionRate = calculateConversion(variant, startDate, endDate);
        
        // 与对照组对比
        VariantMetrics control = getMetrics("control", startDate, endDate);
        VariantMetrics treatment = getMetrics(variant, startDate, endDate);
        
        return ExperimentReport.builder()
            .variantName(variant.name())
            .gmvChange(calculatePercentChange(control.totalGMV, treatment.totalGMV))
            .profitChange(calculatePercentChange(control.totalProfit, treatment.totalProfit))
            .conversionChange(calculatePercentChange(
                control.conversionRate, treatment.conversionRate))
            .statisticalSignificance(calculatePValue(control, treatment))
            .build();
    }
}

五、总结

动态定价是数据和算法的竞技场,但技术之外有三个业务原则比模型本身更重要:

原则一:利润优先于 GMV。 降价的 GMV 增长很容易,但定价模型的优化目标应该是利润最大化,而不是 GMV 最大化。一个常见的陷阱是模型为了追求转化率不断降价,最终 GMV 增长了但毛利下降了。我们在损失函数中加入了毛利约束项,确保推荐价格不低于毛利的基准线。

原则二:规则兜底优于模型自由发挥。 模型推荐的价格必须通过规则引擎的硬约束(成本底线、频次限制、幅度限制)才能生效。这不是对模型的"不信任",而是风控的基本原则——任何自动化系统都需要安全边界。

原则三:审计比调价更重要。 每一笔自动调价都必须是可追溯、可解释、可回滚的。当(不是"如果")某次自动调价出现问题时,能在一分钟内定位到原因并回滚到上一个安全版本,这个能力比调价精度更重要。

从实际效果来看,模型定价相比人工定价将调价频次从每天 1.5 次提升到每小时 1 次,对竞品降价的响应时间从 3 小时缩短到 8 分钟,商品的综合利润率提升了 7.2%。但更重要的收获是建立了一套完整的调价决策-执行-审计闭环,使得定价策略从"拍脑袋"进化到"数据驱动+人工复核"。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐