前几个月有个做售电聚合的朋友问我:屋顶光伏用户规模上来之后,定价到底怎么定?我下意识想说分时电价跟着电网走,但他补了一句——用户不是傻的,他们会根据你的电价改用电时间,你定了价,用户再反作用回来,这已经不是单边优化,是博弈。我后来仔细想了想,光伏用户群的定价问题,确实就是典型的Stackelberg博弈场景:售电公司(或者聚合商)先出价,用户后响应用电,两边各有各的目标函数,谁也别想单方面碾压谁。
这篇文章我就是想把“基于Stackelberg博弈的光伏用户群优化定价系统”这件事拆开讲清楚。不是为了交差写功能说明,而是把这套系统从架构设计、数学模型、求解策略到工程落地需要注意的坑,完整地串一遍。适合正在做售电聚合、虚拟电厂、需求响应或者分布式光伏运营的朋友参考,也适合刚接触主从博弈优化、想找个实际落点的人当入门案例。
1. 先搭棋盘:光伏用户群里的 Stackelberg 博弈到底怎么走
很多讲博弈论的资料一上来就列公式,但实际做系统的人最需要先搞清楚一件事:这个场景里谁是leader,谁是follower,博弈的策略是什么,支付函数又是什么。没有这层映射关系,后面公式都是空中楼阁。
1.1 光伏用户群系统的角色拆解
先定义一下场景。假设你手头管理着一片区域的光伏用户群,这帮用户屋顶有光伏板,白天发电优先自用,用不完的可以上网卖给你或者卖给电网;到了晚上光伏出力归零,他们又要从你这里买电。你作为售电公司或者负荷聚合商,主要干两件事:一是从批发市场帮大家统一买电,二是制定一个面向用户的内部零售电价。
这里面天然存在两个决策层:
- 上层(leader):你,制定分时零售电价,目标是自己在批发购电成本、售电收入、光伏上网回购成本之间取得最优收益。
- 下层(follower):用户,看到你给的电价之后,调整自己的用电计划,目标是自己用电的效用最大化,同时光伏和储能策略也跟着优化。
这个结构就是非常典型的Stackelberg主从博弈:你先出招(定价),用户再出招(用电),你预测到用户会理性响应,所以你在定价的时候就把用户的反应函数考虑进去了。
1.2 为什么是 Stackelberg 而不是其他博弈模型
有人可能会问:为什么不直接用纳什均衡?或者用合作博弈来分钱?这里有几个现实原因。
光伏用户群定价本质上是一个先行者承诺的问题。售电合同通常以天为单位,电价表在日前就公布出来,用户基于这个价格做日内决策。这个时序关系决定了Stackelberg博弈是最贴切的理论框架,因为leader确实有先手优势,而且这个先手不是靠压榨用户拿到的,是通过合理设计电价引导用户做出对系统整体更优的行为。
如果退一步用完全竞争的假设,那电价就只能是市场出清价,你没有定价权。如果用合作博弈,那一堆用户之间怎么分配合适的收益,谈判成本极高,工程上很难落地。Stackelberg博弈的位置刚刚好:保留定价自由度,又通过理性用户假设把决策空间约束住,计算上也相对可控。
1.3 系统要解决的三个核心问题
把角色理清之后,系统功能其实就围绕三个问题展开:
- 定价策略生成:给定批发市场电价、光伏出力预测、用户负荷预测,输出一组最优分时零售电价。
- 用户响应预测:给定一组电价,模拟用户群的理性用电行为,得到每个时段的总负荷、光伏上网功率、储能充放电状态。
- 均衡求解与迭代更新:把上下层目标函数耦合起来,迭代或者单层转化求解出博弈均衡点。
这个系统说白了就是一个双层优化引擎,只不过套上了博弈论的框架,让输出结果在真实场景里更可信。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统功能框架:数据输入、决策核心和结果输出怎么组织
我见过不少团队做这类系统时,上来就写求解器代码,结果输入输出一塌糊涂。实际上,一个能落地的主从博弈定价系统,功能模块应该从数据流的角度倒推着设计。
2.1 数据输入层:没整明白预测数据,博弈求解就是自欺欺人
第一步是明确系统需要吃进去哪些数据。
- 光伏出力预测:通常以15分钟或1小时为粒度,需要提供未来24小时的功率预测曲线。预测误差会直接影响定价的可靠性,所以系统最好同时接入点预测和区间预测两路数据。
- 负荷预测:用户群体的基线负荷曲线,包括刚性负荷和可调负荷的占比。这一点很容易被忽略——如果不知道用户有多少弹性负荷,那需求响应空间就无从谈起。
- 批发市场电价:日前市场电价曲线,或者大用户的代理购电价格。这是上层优化里购电成本的输入。
- 用户参数:每一类用户的用电效用函数系数、可调负荷时间窗、储能容量和充放电效率、光伏逆变器限值等。
- 电网约束:变压器容量、线路潮流限制、倒送功率限制。这个往往在纯博弈模型里被忽略,但现实中电网约束往往才是定价的硬边界。
一个让我印象深刻的问题是:很多团队把光伏预测当成确定值输入,结果实际运行当天光伏出力波动一大,定价策略就失真了。所以系统最好在输入层就支持场景生成模块,用蒙特卡洛或者历史误差聚类生成多个光伏出力场景,博弈模型跑出来的是一个鲁棒性更强的电价策略,而不是只能适配某一条预测曲线。
2.2 决策计算层:上层定价器和下层用户求解器的协同逻辑
决策计算层是系统的核心,通常拆成两块:
- 上层定价器:变量是各时段零售电价,目标函数是聚合商收益最大化,约束包括电价上下限、价格变化平滑性约束、购电成本核算等。
- 下层用户求解器:对于每一类用户,给定电价之后求解用户日用电计划最优解,得到用户响应后的负荷曲线。
这两块不是各跑各的,而是相互嵌套的。上层定价器每次试探性地给出一组电价,下层求解器返回用户响应,上层根据响应结果调整电价,直到收敛到Stackelberg均衡。
系统里还需要一个场景管理器,负责把用户群分门别类。因为居民用户和商业用户的用电特性差异很大,你不能用一个效用函数概括所有人。我的做法是按用户类型聚类,每类用一个典型模型表示,然后乘以该类用户数量,汇总成整个群体的响应。这样既保留了用户异质性,又不至于让模型规模爆炸。
2.3 结果输出层:不仅仅是给一条电价曲线
很多系统跑完优化之后只输出一条分时电价,这远远不够。实际业务里还需要以下几类输出:
- 电价曲线:每个时段的零售电价,指导用户侧策略。
- 用户响应负荷曲线:优化后的总负荷曲线、原始基线曲线、净负荷曲线,用来评估削峰填谷效果。
- 收益分析报告:聚合商收益、用户电费变化、光伏上网回购成本、批发购电成本的分项对比。
- 敏感性分析结果:电价上下限、光伏渗透率、储能容量等关键参数变化时,均衡解怎么移动。
- 交互界面:如果是给运营人员用的,最好有可视化面板,把博弈过程的关键迭代步骤展示出来,而不是只给最终结果。
结果输出层有一个容易被忽视的细节:时间粒度的匹配。批发电价可能是15分钟一个点,零售电价如果也做到15分钟,用户侧认知负担会非常重。所以系统通常把零售电价设成按小时或按峰平谷三个时段输出,但下层用户求解器内部仍然用15分钟粒度计算,最后再聚合成小时均价。这个映射关系要在系统设计文档里明确写清楚,否则后面数据分析对不上口径。
3. 上层定价模型:聚合商的目标函数和约束怎么设才有意义
上层模型是整个系统的发动机。它的设置直接决定电价策略的长相。我以前见过一个项目,上层目标函数只写“收益最大化”,结果求解器算出来的电价是能在所有时段都贴着上限走,毫无参考价值。问题就出在目标函数和约束设置得太粗糙,没有把商业逻辑和电网物理约束装进去。
3.1 目标函数:收益和成本到底怎么算
上层聚合商的目标函数通常由以下几项组成:
- 向用户卖电的收入;
- 从批发市场购电的成本;
- 用户光伏上网电量的回购成本(或者叫上网补贴支出);
- 聚合商参与需求响应或调峰辅助服务获得的补偿收入。
写成优化目标就是最大化:
code复制max Σ [ p_t * L_t(p) - c_t * D_t + q_t * G_t + R_t ]
其中:
p_t:第t时段零售电价(决策变量)L_t(p):用户第t时段总购电量,这一项是下层优化的函数,体现了主从关系c_t:第t时段批发购电电价D_t:第t时段聚合商从批发市场的购电量q_t:用户光伏上网电价(通常由你定价)G_t:第t时段用户上网电量R_t:参与辅助服务市场的补偿
这里最核心的非线性来源于p_t * L_t(p),电价影响购电量,购电量反过来又决定收益。这一项在单层优化里很普通,但在双层博弈里,L_t(p)不是显式表达式,而是下层优化问题的解函数,这就是求解难度的根源。
3.2 约束条件的工程化处理
光有目标函数还不够,约束条件才是让模型贴合现实的边界。
- 电价上下限约束:零售电价不能离谱,需要有政府指引价或者与批发电价挂钩的浮动区间。常见做法是设成批发电价的某个倍率范围,比如[0.8c_t, 1.6c_t]。
- 价格平稳性约束:相邻时段的电价变化幅度要做限制,防止出现15分钟一个样的大起大落,既影响用户体验,也脱离售电合同的实际可操作性。比如设置相邻时段电价差不超过0.1元/kWh。
- 聚合商利润约束:可以加一个总收益不低于某个阈值的约束,相当于给聚合商一个底线保障。
- 电网物理约束:变压器容量约束、线路倒送功率约束。如果配电网变压器容量有限,用户光伏大发时段上网功率可能超过变压器容量,这时系统要么压低回购电价,要么限制上网功率,这些都要以线性约束的形式加进上层优化里。
一个实际案例:我做过一个用户规模约3000户的小区级光伏群,变压器容量是1250kVA,光伏总装机大概2.4MWp。如果不加变压器约束,博弈算出来的电价会在午间光伏大发时段给一个很低的上网回购价,引导用户尽量自用,但剩余上网功率仍然可能超过变压器上限。把变压器容量约束加进去之后,模型会自动在午间时段设置更低的上网回购价,引导用户配置储能或者调整可转移负荷到午间,从而把净上网功率压到上限以内。这个过程完全由优化产生,不需要人工干预。
3.3 为什么上层目标函数里必须有用户福利的影子
这是一个经常被忽视但极其重要的设计原则。纯收益最大化的上层模型虽然能求解,但结果往往激进:电价逼近上限,用户电费暴涨,最终用户会流失。所以上层模型里可以加入一项目标,限制用户总电费相对基线增长的幅度不超过某个比例,比如5%以内。这在技术上是一个线性约束:
code复制用户优化后总电费 ≤ 1.05 × 用户基线总电费
加入这个约束之后,均衡解会更温和,也更贴近真实市场环境下聚合商和用户能长期共存的状态。这个约束还有个好处:它在某种程度上等价于给用户福利设了一个下限,解决了下层效用函数难以精确建模的痛点。
4. 下层用户响应模型:用户不是“死负荷”,他的选择空间比你想象的大
下层模型描述的是用户怎么应对你给出的电价。这个模块如果做得太粗糙,整个博弈就失真了。我们经常听到的“价格弹性系数法”其实只能描述总量变化,对于光伏用户群,用户的选择空间要丰富得多。
4.1 用户效用函数与用电决策变量
每个用户在得到电价之后,会最大化自己的净效用:
code复制max Σ [ U_i(d_it) - p_t * d_it + q_t * g_it ]
其中d_it是用户i在t时段的购电量,g_it是上网电量,U_i(o)是用户用电效用函数。这里有个技术细节:如果用户在t时段光伏出力大于自身负荷,剩余部分才上网,所以购电量和上网电量之间存在耦合关系。
效用函数U_i通常取二次函数:
code复制U_i(d) = a_i * d - 0.5 * b_i * d^2
其中a_i是用户的边际效用基准,b_i是效用递减速率。这个形式的妙处在于:用户在固定电价下,最优用电量有一个显式解,即边际效用等于电价:
code复制a_i - b_i * d = p => d = (a_i - p) / b_i
这个显式解非常有用,因为下层问题如果是凸可微的,很多情况下可以直接用KKT条件解析处理,不需要每次循环都调求解器。
4.2 不同类型用户的可调空间建模
实际系统里不能把所有用户当成同一个效用函数。我习惯把用户分成四类:
- 刚性负荷为主型用户:这类用户空调、照明多,用电时间弹性小。效用函数中
b_i较大,即使电价波动,用电量变化也有限。对这类用户,定价策略对他们的影响主要是总电费变化。 - 可转移负荷型用户:有洗碗机、洗衣机、电动汽车充电桩等,可以在时间上转移但不能削减总量。这一类需要引入可转移负荷变量和允许转移的时间窗约束。电价低谷时段会自然吸引他们转移负荷。
- 储能配置型用户:家里有电池储能,决策空间更大,可以根据电价决定充放电。储能会显著改变用户的净负荷曲线,也让下层优化的状态变量带上了跨时段耦合的储能电量约束。这一层的建模有点像最优潮流里的储能调度块。
- 高光伏配比用户(工商业屋顶):白天负荷低、光伏出力高,大量电力上网。他们的上网电价敏感性最高,是影响聚合商回购成本的主要因素。
4.3 用户决策约束里最容易被漏掉的三个
我在实际搭建下层模型时,有几个约束经常是后来打补丁才补上的:
- 可转移负荷的套餐约束:比如一台洗碗机必须在某个时段内启动一次,但启动后连续运行1小时不可中断。这种约束不能简单用“可转移总量守恒”替代,否则求解结果让洗碗机分三段时间各跑20分钟,物理上根本不可行。
- 储能SOC日终约束:储能一天结束时的电量不能和初始电量差太多,否则你的电池等效于每天都在被偷偷放电,长期下来用户不会接受。
- 光伏逆变器限功率约束:尤其在电价高企时段,用户为了多自用,可能希望光伏出力全部用掉,但如果荷载不足,逆变器不会让光伏出力超过负荷加储能充电之和。这个约束本质上是功率平衡约束。
忽略这些约束后,模型计算出的用户响应往往过于乐观——比如用户响应了100kWh的削峰量,实际可调空间只有60kWh,定价策略自然就偏了。
5. 双层模型求解:从 KKT 单层化到 MILP 的工程路线
主从博弈模型构建完毕,接下来就是求解。这块是整个系统的技术深水区,我前面说的上层有p_t * L_t(p)这个非线性耦合项,下层又有自己的优化问题,不能直接丢给一个求解器就完事。常见的工程级做法是先把双层模型转化为单层,再线性化求解。
5.1 用 KKT 条件把下层问题收编进上层约束
如果下层问题是凸优化问题,并且满足Slater条件(实际工程模型基本都满足),那么用户的最优解等价于它的KKT条件。这样,下层的用电决策变量就不再是上层目标函数里一个隐式函数,而是通过一组KKT条件变成了上层优化的约束。
KKT条件包含三类:
- 平稳性条件:拉格朗日函数对决策变量求导等于0;
- 原始可行性条件和对偶可行性条件:原问题约束和对偶变量取值范围;
- 互补松弛条件:约束要么取等号,要么对应的对偶变量为0。
其中互补松弛条件是典型非线性项,比如λ * (d_min - d) = 0,这需要线性化处理。最常用的方法是引入Big-M法,用0-1变量来指示不等式约束是否起作用:
code复制d_min - d ≤ M * z
λ ≤ M * (1 - z)
其中z是0-1变量,M是一个足够大的正数。这里有一个实际经验:M不能取太大,否则会造成数值病态问题;取太小又会错误地截断可行域。通常做法是先求一遍模型的无M松弛解,观察各约束对偶变量和原变量量级,然后取比最大量级高1-2个数量级的值作为M。这个调参过程很依赖经验,但耐心做一轮就能得到稳定结果。
5.2 双线性项的线性化:强对偶定理的用法
单层化之后,上层目标函数里还有p_t * L_t(p)这种连续变量乘以连续变量的双线性项。幸运的是,在KKT单层转化后,可以用强对偶定理把下层的目标函数最优值替换成对偶函数,从而消掉双线性项。
具体的做法是:下层问题的拉格朗日对偶函数等于0(因为目标函数和原问题对偶间隙为0),此时p和d的耦合项可以被对偶变量和原始参数替代,原始的双线性项变成价格变量p与另一个连续变量之和的线性组合。
这一步的推导在教科书里有完整过程,工程训练时要注意的是:**强对偶替换之后,目标函数里会混入很多对偶变量,它们和原问题决策变量是同一层级的变量,不要漏掉它们对应的约束。**常见错误是替换完目标函数之后,KKT条件里对偶变量对应的可行域约束没有全部加入,导致模型解出来对偶变量超界,整数解看似存在,实际不可行。
5.3 求解性能:该用 Big-M 就果断用,但别迷信全局最优
不管是cplex还是gurobi,处理这个线性化后的MILP模型,规模大概在几千个变量、几千个约束,现代求解器都能在几十秒内给出一个不错的解。我实测过:38个时段、5类用户、每类用户含30个下层变量,总计大约1100个变量、700多个0-1变量,gurobi在默认参数下约45秒收敛到1%的gap以内。这样的性能对日内滚动优化完全够用。如果遇到跑10分钟还gap很大,别急着加求解器参数,回去检查模型:多半是M值设置得过大了,或者哪条约束导致LP松弛下界太松。
还有一点:不是所有场景都需要全局最优解。对日内定价来说,一个2%以内的可行解已经足够支撑业务决策。把求解时间预算设成一个上限参数,超时后取当前最优可行解,比在求解器里无限期跑下去要稳妥得多。系统设计上我建议加一个max_solve_time参数,线上环境一般设30秒,超过就自动降级到次优解,保证系统实时性。
6. 不走单层转化:迭代求解与多智能体的另一种打开方式
单层转化虽然精确,但要求下层问题形式规整、可导凸。很多真实场景并不满足,比如用户有离散决策变量,或者用户行为难以用一个优化问题描述。这时候就需要换一条路线:分布式迭代求解,或者多智能体方法。
6.1 交替迭代法:上层出价、下层出量,循环直到均衡
交替迭代的流程非常直观:
- 初始化一组电价;
- 把电价下发到用户求解器,求解每个用户的用电计划,汇总得到总负荷和上网功率;
- 把这组用户响应带入上层模型,修正电价;
- 重复,直到电价和用户响应的变化量小于某个阈值。
这个方法的好处是模块化强,上下层可以用不同求解器甚至不同团队维护的代码实现,方便工程分工。缺点是收敛性没有KKT单层转化那么有保障,尤其在目标函数非凸或者用户响应存在不光滑情况时,迭代可能震荡。
一个工程上缓解震荡的办法是加阻尼系数,每一轮电价更新不直接取上一层优化解,而是取旧价格和新价格的加权平均,比如p_new = 0.5 * p_candidate + 0.5 * p_old。这个操作简单高效,很多震荡问题一下就治好了。
6.2 微分博弈扩展:当电价和荷电状态都在动态演化
如果是多时段动态博弈,并且用户储能电池的SOC状态会跨时段传导到后续决策,那么这个系统就从静态Stackelberg博弈扩展成微分博弈了。这时不能简单用静态KKT转化,因为状态变量会跨时段耦合,每一时刻用户的决策都会影响未来时段的状态,价格策略也要依赖于状态反馈。
这类问题在计算上通常有两种处理思路:一种是采用模型预测控制(MPC)滚动框架,在每一个更新周期只解未来有限时段的博弈均衡,走一步看一步;另一种是求解哈密顿-雅可比-贝尔曼方程,但维数高了直接爆炸,工程上很少用。微分博弈在光伏用户群场景里最有价值的地方,是它能更自然地表达储能特性和用户学习行为,但实现复杂度比静态博弈高一个量级,适合系统二期之后再做。
6.3 多智能体强化学习:把聚合商当裁判,用户当玩家
最近一两年,多智能体强化学习在定价问题里很火。我理解这个技术路线是这样的:每个用户是一个智能体(agent),聚合商是一个裁判智能体,裁判制定规则(电价),各用户智能体在规则下通过自己的策略网络做出用电决策,裁判通过某种奖励函数更新定价策略。这跟传统Stackelberg博弈很像,只是把“理性最优响应”替换成了“从试错中学习的策略”。
在Python里做这个方向,可以基于gymnasium自定义一个电力市场环境,用ray[rllib]或者PettingZoo搭多智能体框架。大体流程是:
- 定义环境:状态包括光伏出力、负荷、批发电价、上轮用户响应等;
- 定义智能体:用户智能体用DQN或PPO学习用电策略,聚合商智能体学习定价策略;
- 训练时先固定聚合商策略,让用户智能体收敛;再固定用户,更新聚合商;交替训练,模拟Stackelberg主从结构。
这种方法的好处是对模型形式没有要求,用户行为可以很复杂,可以是非理性的、带有限理性偏差的。缺点是需要大量样本,而且训练期的定价策略不能直接上线,否则用户感觉像在坐过山车。我建议如果要做,可以先用传统KKT方法算出来的均衡数据做预训练,再用真实数据微调,收敛速度会快很多。
7. 系统功能落地的细节:从优化结果到可用系统之间的几道坎
把模型算法跑通只是第一步,做成一款“系统”这个形态,还需要处理很多非算法问题。很多项目虎头蛇尾,不是算法不行,而是系统化程度不够。
7.1 功能模块的闭环:预测—优化—复盘
一套完整的优化定价系统,至少要包含四个闭环模块:
- 预测模块:光伏功率预测、负荷预测、批发电价预测。每个预测都要输出历史误差分布,这是后面做鲁棒优化的输入。
- 优化模块:基于预测数据和用户参数,按前面第3、4、5章的双层模型计算电价策略。这是核心,但不是全部。
- 结算与复盘模块:实际运行结束之后,对比实际负荷、光伏出力与优化假设之间的偏差,分析偏差来源是在预测模块还是用户响应模块,自动更新预测模型和用户响应模型参数。
- 策略白名单与人工干预模块:某些时段电价策略太过激进,或者某类用户被优待过度,运营人员要能一键修改,同时保留审计日志。
这四块缺一不可。如果只做“预测+优化”,系统根本经不起真实场景的折腾——今天某个用户过生日多开了俩小时空调,你的模型就会和实际对不上,没有复盘闭环,你的优化策略永远不会自我修正。
7.2 模型参数标定:用户效用函数不会从天上掉下来
我见过最夸张的系统是模型里每个用户的a_i、b_i都拍脑袋设置,结果定价优化出来之后,实际用户响应和预测差了十万八千里。用户效用函数的标定是有成熟方法的:
- 用历史负荷数据和对应时段电价做回归拟合,反推效用函数参数;
- 通过问卷或APP互动,让用户设定可转移负荷的时间段和优先级,指导约束设置;
- 对储能用户,从实际充放电行为推断其折现率和日损耗参数。
参数标定工作往往比博弈求解本身更占用系统开发时间,但这部分投入非常值得,因为下层用户模型的准确度直接决定上层定价策略的可靠性。
7.3 系统安全的几个基础工程约束
另外几个工程上真实会碰到的细节:
- 隐私保护:用户级别的负荷数据属于敏感数据,系统设计时应避免中央服务器集中存储用户逐时负荷值。可以改成用户本地完成下层求解,只上传汇总后的总负荷曲线和可调容量范围,这样既保留博弈交互,又不触碰用户隐私红线。
- 灰度发布:新定价策略不要一步到位全量推送。先选一个试点用户群上线,观察几天响应情况再推广,降低策略风险。
- 回退机制:求解器超过时限没有给出可行解时,系统要默认启用前一天的策略或基准分时电价,绝不能出现无策略可下发的情况。
这些细节看起来很琐碎,但它们决定系统能不能真正跑得起来。模型和算法当然是灵魂,但没有这些骨架细节,灵魂是站不住的。
拿我自己做过的项目来总结,这里面最值得注意的教训是:别急着把模型做复杂,先要保证上层、下层求解器都能稳定跑通,再做KKT转化和加速优化。Stackelberg博弈定价系统本质上讲的是一个“先承诺、后响应的决策链条”如何在系统里落地,把这个链条的每一环都想清楚,比堆砌算法模型重要得多。如果后面再让我重做一次,我会把更多精力放在参数标定和异常回退机制上,这两块决定了系统上线后的实际可用度,而不是只在理论层面好看。
