
先说我为什么折腾这个东西。有一次要给一个执行机构做控制器设计对象是典型的强非线性加参数慢漂移机理模型折腾了两周辨识出来的模型换个工况就失效。后面我转向无模型自适应这条路才算找到一套能落地的方法。无模型自适应预测控制MFAPC和无模型自适应迭代学习控制MFAILC核心都是绕开被控对象数学模型直接用输入输出数据在线构造一个局部的动态线性化关系再分别用预测优化和迭代学习把控制质量做上去。这篇文章把我自己搭的一套数值验证仿真程序完整拆开讲从公式是怎么来的、代码骨架长什么样到参数怎么整定、仿真里最容易翻车的几个坑都会覆盖到。想研究数据驱动控制、先进控制算法验证的同学和工程师可以参考这套流程直接复现。1. 模型拿不到手时控制器还能怎么设计1.1 传统控制设计链路有多长按传统流程一个新对象的控制器设计通常要经历开环试验确定工作点、设计激励信号、系统辨识、模型结构选择从 ARX 到状态空间再到各类非线性模型、参数估计、残差验证、模型降阶最后才轮到控制律设计。每一步都有专门的工具链光是激励信号的设计就够写一篇长文。更麻烦的是这套流程建立在对象模型是准的这个前提下。对象一旦出现参数漂移、工况切换、执行机构饱和摩擦等变化辨识模型的置信度就直线下降之前投入的建模工作很可能全部作废。我碰到的那类执行机构问题就是如此二阶线性模型在验证集上拟合很好但输入信号频段一变预测输出马上偏离实际调好的 PID 参数上线后还是被现场工程师改了好几次。这个经历让我意识到模型不确定性本身就是控制回路里最大的风险源之一而消除这个风险的路径不一定非得是把它建模建得更好也可以是直接绕开模型。1.2 数据驱动思路的诱惑无模型自适应控制的思路是不依赖任何显式模型直接把系统在每一个采样时刻局部线性化来看待。具体来说对一个单输入单输出离散时间非线性系统如果输出关于输入的变化是 Lipschitz 连续的那么在任意时刻 k 一定存在一个时变系数 φ(k)使得Δy(k1) φ(k)·Δu(k)这个 φ(k) 被称为伪偏导数Pseudo Partial DerivativePPD。注意这个式子里没有任何关于被控对象的先验知识它只是一个在当前工作点附近的局部输入输出关系。类比一下你不知道整段山路的地图但你知道当前这个坡度下方向盘打多少车头会偏多少下一段路再修正就行PPD 就是那个当前坡度。MFAPC 和 MFAILC 都建立在伪偏导数这个基石上。MFAPC 在单步 MFAC 的基础上引入预测控制的多步外推让控制器能看到未来参考轨迹的走势MFAILC 则把数据驱动的思想搬到迭代轴上专门处理那些重复执行的批次任务让误差随着运行次数下降。两者共同的特点是开发周期短不需要建模参数少适合对象机理复杂但输入输出数据易得的场景。1.3 这套仿真程序要回答的问题我这次搭建的数值验证程序主要回答三个问题第一MFAPC 在非线性对象上能不能做到无稳态偏差的轨迹跟踪和单步 MFAC 比到底强在哪里第二MFAILC 在重复批次任务中式子误差是否随迭代次数收敛收敛速度受哪些参数主导第三这两类算法真正放到数值仿真里跑哪些参数组合会直接导致发散哪些现象是看起来收敛但其实隐患很大。程序用 Python 实现结构上把被控对象、PPD 估计器、控制器拆成三个独立模块这样后面替换对象、替换算法都很方便。2. 别把无模型理解成没有模型紧格式动态线性化与伪偏导数2.1 动态线性化是怎么成立的紧格式动态线性化Compact Form Dynamic LinearizationCFDL在理论上要成立需要被控对象满足两个条件一是系统的输入输出关系可以写成 y(k1) f(y(k), ..., u(k), ...) 这种一般非线性离散时间形式二是输出关于输入的变化满足 Lipschitz 条件也就是输入变化足够小时输出变化不会发生跳变。这两个条件在大多数实际物理对象上是成立的因为执行器、传感器、采样过程本身都是连续且有惯性的。有了这两个条件利用差分中值定理的思路就能保证在每个采样时刻存在一个时变的 φ(k)使得上面的等式成立。这个 φ(k) 的物理含义可以理解为当前工作点下输入变化对输出变化的局部增益。它和传统意义上的模型参数完全不同传统模型参数是固定的、全局的而 PPD 是时变的、局部的每个采样时刻都在被重新估计和更新。2.2 PPD 怎么在线估计PPD 不能直接测量只能通过递推估计。标准做法是极小化一个带正则项的准则函数J [Δy(k) - φ̂(k-1)·Δu(k-1)]² μ·[φ̂(k) - φ̂(k-1)]²第一项让估计结果能解释最新的输入输出变化第二项惩罚 PPD 的剧烈跳变μ 是正则权重一般取 0.1~1。对 φ̂(k) 求梯度并令其为零整理后得到递推式φ̂(k) φ̂(k-1) η·Δu(k-1) / (μ |Δu(k-1)|²) · [Δy(k) - φ̂(k-1)·Δu(k-1)]这里 η 是步长因子理论上取 (0,2] 可以保证估计误差有界。这个递推式本质上是一种归一化梯度下降分母里的 μ 起着两个作用一是防止控制输入长时间不变导致除零二是限制 PPD 估计的更新幅度避免对噪声过度敏感。实际实现里还有两条铁律当 |Δu(k-1)| 小到接近零时PPD 直接保持原值不更新当估计得到的 φ̂(k) 符号发生翻转或者说与前一步符号不一致直接把它重置为初值。这两条看起来不起眼但少了任何一条仿真很可能跑到中途就发散。后面调试部分我会专门说原因。2.3 从单步控制到多步预测的跳板单步 MFAC 的控制律是极小化输出跟踪误差和控制增量的加权平方和J [y*(k1) - ŷ(k1)]² λ·Δu(k)²对 Δu(k) 求导整理得到Δu(k) ρ·φ̂(k) / (λ φ̂(k)²) · [y*(k1) - y(k)]这个式子极其轻量每个采样周期只要做几次乘加就能算出来很多低成本控制器都能跑得动。但它有一个明显短板只预测一步控制律对未来的参考轨迹走势没有前瞻性。对象一旦存在大滞后或者惯性环节单步 MFAC 就会显得迟钝总是先看到误差再反应。想让它提前动作就得把预测时域拉长这正是 MFAPC 要做的事。3. MFAPC 的预测机制把伪偏导数拿来外推未来输出3.1 多步预测模型怎么构造在预测时域 Np 内一个简单且实用的做法是假设未来 Np 步的 PPD 基本恒定即 φ(kj) ≈ φ(k)。这假设听起来粗糙但预测控制本身就是滚动时域优化每走一步都会用最新 PPD 估计重新构造预测误差会被及时纠正。基于这个假设可以从当前时刻 y(k) 出发逐层外推ŷ(k1|k) y(k) φ̂(k)·Δu(k) ŷ(k2|k) y(k1|k) φ̂(k)·Δu(k1) …… ŷ(kNp|k) y(k) φ̂(k)·[Δu(k) Δu(k1) … Δu(kNp-1)]可以看到早期控制增量对后期的预测输出影响更大这天然符合控制系统的实际物理过程。把所有预测输出写成向量形式Y_Np 1·y(k) G·ΔU其中 1 是 Np 维全 1 列向量ΔU 是 Np 维待求控制增量向量G 是主对角线全部为 φ̂(k) 的 Np 阶下三角矩阵。拿 Np3 举例G 的样子就是| φ̂ 0 0 | | φ̂ φ̂ 0 | | φ̂ φ̂ φ̂ |这个结构里Δu(k) 影响所有未来输出Δu(k1) 影响第二步及以后的输出依此类推。3.2 滚动优化一个带 λ 的线性最小二乘问题MFAPC 的代价函数取参考轨迹与预测输出的偏差平方和加上控制增量的惩罚J ||Y*_Np - Y_Np||² λ·||ΔU||²其中 Y*_Np 是参考轨迹在未来 Np 步的采样值序列。对 ΔU 求梯度并令其为零得到解析解ΔU (GᵀG λI)⁻¹·Gᵀ·(Y*_Np - 1·y(k))这个式子看起来要解一个 Np 维线性方程组但 Np 一般只有 3~10计算量完全可以忽略。实现时可以用最小二乘求解器也可以用 Cholesky 分解代替直接求逆。算完整个 ΔU 序列后只取第一个元素作为当前采样周期的控制增量控制器输出为 u(k) u(k-1) ΔU(1)。下一采样周期到达后重新估计 PPD、重新构造矩阵、重新优化这就是滚动时域的执行方式。λ 在这个问题里一箭双雕一是数学上保证 GᵀGλI 可逆避免矩阵奇异二是工程上调节控制动作的激进程度λ 越大控制越平稳但跟踪越慢λ 越小动作越猛但容易激发高频振荡。初始调试我一般从 λ0.5 起步。3.3 为什么多步外推比单步强我一开始也怀疑PPD 本身就是估计值外推多少步都会带误差多步预测凭什么比单步好跑完对比实验就清楚了。在被控对象输入端串联一个一阶惯性环节之后单步 MFAC 的动作模式是看到误差、产生修正、等待反馈而 MFAPC 因为预测时域覆盖了参考轨迹的上升段会提前输出控制量跟踪滞后明显变小。换句话说预测机制的价值不在于把未来算得多准而在于让控制器对参考轨迹的走势有前瞻性提前分配控制力。这一点在带滞后、带惯性的对象上特别明显。此外多步优化在扰动频段较高时会起到类似滤波的作用把单个采样点的异常误差稀释到整个时域里考虑控制信号的高频抖动会被抑制。代价也有Np 拉太长PPD 恒定的假设失真控制会变得激进甚至振荡这点会在后面的调试部分细讲。3.4 关键参数的初始值推荐下面是我在多个非线性对象上试过、能够稳定起步的一组参数可以作为仿真的起点参数含义推荐初值调整方向Np预测时域5惯性大取大噪声大取小λ控制增量惩罚0.5振荡就调大响应慢就调小ρ控制律步长因子1.0一般保持 1.0特殊对象微调ηPPD 估计步长1.0估计抖动就调到 0.5μPPD 估计正则系数0.5噪声大就调大φ(1)PPD 初值1.0见第 6 节踩坑记录εΔu 更新阈值1e-4按控制量实际量纲调整这套参数不是万能的但它能保证程序先跑通。跑通之后再去调性能比一上来就追求最优参数明智得多。4. MFAILC把迭代轴当成新的学习维度4.1 重复任务里的另一条时间线很多生产任务本质上是重复的注塑成型、点焊、包装、批量反应每次运行都执行同样的参考轨迹。对这种批次型任务普通反馈控制的问题在于每次运行都要从头开始犯同样的错误上一批积累的经验完全没有被利用。迭代学习控制ILC的思想正好相反上次犯的错下次不再犯把历史数据变成学习素材。MFAILC 把 MFAC 的伪偏导数思想从时间轴移植到迭代轴。设第 i 次运行时系统输出为 y_i(k)输入为 u_i(k)k 从 1 到 N。如果两次运行之间系统动力学变化足够平滑那么迭代轴上存在伪偏导数 φ_i(k)使得y_i(k1) - y_{i-1}(k1) φ_i(k)·[u_i(k) - u_{i-1}(k)]注意这里的时间索引 k 不动变化的是迭代索引 i。整个式子的含义是固定在第 k 个采样时刻如果本次输入相对上次输入改变了一点本次输出相对上次输出会沿着 φ_i(k) 这个局部斜率变化。这和时间轴上的 CFDL 在形式上几乎一致只是差分的方向从时间变成了迭代。4.2 学习律是梯度下降在迭代轴上的投影MFAILC 的控制目标是让第 i 次运行的系统输出逼近期望轨迹 y_d(k)。考虑代价函数J [y_d(k1) - ŷ_i(k1)]² λ·[u_i(k) - u_{i-1}(k)]²这里决策变量是本次输入相对上次输入的增量 u_i(k)-u_{i-1}(k)而 ŷ_i(k1) 用迭代轴上的动态线性化关系展开。对代价函数求梯度并令其为零得到学习更新律u_i(k) u_{i-1}(k) ρ·φ̂_i(k) / (λ φ̂_i(k)²)·[y_d(k1) - y_{i-1}(k1)]这个式子非常直观本次输入等于上次输入加上一个修正项修正方向由 y_d(k1) 与上一次输出 y_{i-1}(k1) 的误差决定修正幅度由 PPD 和 λ 共同控制。从优化角度看这就是在迭代轴上做梯度下降把误差投影到输入更新方向上去。λ 在这里决定学习强度λ 小学习快但容易过冲λ 大学习稳健但收敛慢ρ 可以视为对步长的外置放大。迭代轴上的 PPD 估计式与时间轴结构相同只是用上一次迭代与上上次迭代的输入输出差来更新φ̂_i(k) φ̂_{i-1}(k) η·Δu_{i-1}(k) / (μ |Δu_{i-1}(k)|²)·[Δy_{i-1}(k1) - φ̂_{i-1}(k)·Δu_{i-1}(k)]其中 Δu_{i-1}(k) u_{i-1}(k) - u_{i-2}(k)Δy_{i-1}(k1) y_{i-1}(k1) - y_{i-2}(k1)。因为用到的都是历史迭代数据整个 PPD 序列可以在本次运行开始前批量算好不占用在线计算时间。4.3 MFAPC 与 MFAILC 到底哪里不同两者名字相近很容易混。我把关键差异列成一张表仿真选型时照着选就行对比维度MFAPCMFAILC学习发生的维度时间轴每个采样周期更新迭代轴每次批次运行更新适用任务连续生产过程、不可重复轨迹重复批次、跟踪同一条期望轨迹控制信号改进方式滚动优化当前时刻输入在上一次批次输入上叠加修正对参考轨迹的依赖只需未来 Np 步参考需要整条参考轨迹已知收敛机制反馈加预测单次运行内收敛多次迭代逐渐逼近批次间收敛典型应用场景化工连续流程、运动控制注塑机、点焊机、批次反应器如果任务对象既需要批内抗扰又需要在批次间积累经验可以把两者结合成二维更新策略时间轴跑 MFAPC 抑制干扰迭代轴跑 MFAILC 修正批次偏差这种组合我在另一版程序里试过效果比单独用任何一个都好。不过这篇先不展开。5. 数值仿真程序搭建一个能直接跑的 Python 骨架5.1 被控对象和任务设计我选择的测试对象是文献中常用的非线性离散时间系统y(k1) 0.8·y(k) / (1 y(k)²) 0.6·u(k)这个对象有两点设计意图第一项非线性项让系统增益随工作点变化能够检验算法在非线性格局下的适应能力第二项输入项保证系统相对阶为 1PPD 在正常工况下基本为正方便和后续调试经验对照。批次任务设定为时间长度 N200期望轨迹为 y_d(k) 0.5·sin(2πk/100)每次迭代从相同的初始状态开始。仿真分为两个任务任务一是比较单步 MFAC 与 MFAPC 在连续跟踪场景下的表现任务二是用 MFAILC 重复运行若干批次观察跟踪误差随迭代次数的变化。这种设计能一次性把两个算法的核心特性都验证出来。5.2 代码结构对象、估计器、控制器三段式程序按被控对象、PPD 估计器、控制器三个模块拆分替换任何一部分都不影响其他模块。核心代码骨架如下import numpy as np def plant(y, u): 非线性测试对象: y(k1) 0.8*y(k)/(1y(k)^2) 0.6*u(k) return 0.8 * y / (1.0 y**2) 0.6 * u def ppd_estimate(phi_old, dy, du, eta1.0, mu0.5, phi_init1.0, eps1e-4): PPD 递推估计带重置机制 if abs(du) eps: return phi_old phi phi_old eta * du / (mu du**2) * (dy - phi_old * du) if abs(phi) eps or np.sign(phi) ! np.sign(phi_init): phi phi_init return phi def mfapc_control(y_now, y_ref_seq, phi, u_prev, Np5, lam0.5): MFAPC 滚动优化返回当前控制输入 G np.tril(np.ones((Np, Np))) * phi Y0 np.full(Np, y_now) dU np.linalg.solve(G.T G lam * np.eye(Np), G.T (y_ref_seq - Y0)) return u_prev dU[0]任务一的 MFAPC 主循环N, Np, lam 200, 5, 0.5 yd 0.5 * np.sin(2 * np.pi * np.arange(N 1) / 100.) u np.zeros(N) y np.zeros(N 1) phi 1.0 for k in range(1, N): ref_seq yd[k 1:k 1 Np] if len(ref_seq) Np: break u[k] mfapc_control(y[k], ref_seq, phi, u[k - 1], Np, lam) y[k 1] plant(y[k], u[k]) phi ppd_estimate(phi, y[k 1] - y[k], u[k] - u[k - 1])注意第 k 步估计 PPD 用的是最新的输出差 y(k1)-y(k) 和输入差 u(k)-u(k-1)也就是把上一拍算好的控制输入引起的实际输出变化喂给估计器这保证了估计器永远用的是真实闭环数据。5.3 MFAILC 的主循环结构MFAILC 的代码结构和 MFAPC 不太一样多了一层迭代循环def mfailc_control(u_prev_iter, phi, error, lam0.5, rho1.0): MFAILC 学习律返回本次输入 return u_prev_iter rho * phi / (lam phi**2) * error max_iter 60 U np.zeros((max_iter, N)) Y np.zeros((max_iter, N 1)) PHI np.ones((max_iter, N)) # 第 0 次迭代先用零输入跑一遍作为历史基线 for k in range(N): Y[0][k 1] plant(Y[0][k], U[0][k]) for i in range(1, max_iter): # 用最近两次迭代的数据估计本次各时刻的 PPD for k in range(N): du_prev_iter U[i - 1][k] - U[i - 2][k] if i 2 else 1e-6 dy_prev_iter Y[i - 1][k 1] - Y[i - 2][k 1] if i 2 else 0.0 PHI[i][k] ppd_estimate(PHI[i - 1][k], dy_prev_iter, du_prev_iter) # 依据学习律计算本次输入 for k in range(N): error yd[k 1] - Y[i - 1][k 1] U[i][k] mfailc_control(U[i - 1][k], PHI[i][k], error) # 运行被控对象 for k in range(N): Y[i][k 1] plant(Y[i][k], U[i][k])这段代码里的关键点在于第 i 次迭代的 PPD 是用 i-1 与 i-2 两次迭代的数据预先估计出来的不依赖当前迭代的实时数据。所以 PPD 估计可以离线批量算好真正运行时只需要执行学习律和对象仿真这一点和在线估计的时间轴 MFAPC 有本质区别。5.4 仿真结果解读我在两组任务里记录了跟踪误差指标评价指标用 RMSE均方根误差和最大绝对误差两个维度。核心结果如下表算法RMSE最大绝对误差单步 MFAC对照组0.1340.28MFAPCNp5λ0.50.0710.16MFAILC第 5 次迭代0.0890.21MFAILC第 30 次迭代0.0240.06MFAPC 相比单步 MFAC由于提前看到了参考轨迹的走势上升段跟随明显更紧RMSE 下降了接近一半。MFAILC 在初始几次迭代时误差下降最快30 次以后进入缓慢收敛区我特意把程序跑到第 60 次发现误差在 0.02 附近波动没有再继续下降这是一个值得注意的信号后面调试部分会专门解释。评价指标的代码实现很简单顺手放在这里def rmse(yd, y): return float(np.sqrt(np.mean((yd - y) ** 2))) def max_abs_err(yd, y): return float(np.max(np.abs(yd - y)))如果读者想复现建议先在任务一上把 MFAPC 跑通确认 PPD 估计不发散再切换到任务二跑 MFAILC。两个任务共用同一个对象函数和 PPD 估计器移植成本很低。6. 参数整定与调试踩坑记录6.1 伪偏导数初值和重置机制为什么如此重要PPD 初值 φ(1) 这个参数看起来不起眼但它在仿真里可以直接决定成败。如果初值给太小等价于控制器认为输入对输出影响很弱于是会输出很大的控制增量来补偿误差容易导致超调甚至振荡如果初值给太大控制器又认为输入稍微一动输出就很敏感控制动作会畏首畏尾跟踪速度变慢。我一般取对象静态增益量级的估计值作为初值对这个测试对象取 0.6~1.5 之间都能正常起步。重置机制更关键。因为估计器的分母里有 |Δu(k-1)|²当输入信号在某段时间内几乎不变时分母趋近于零PPD 估计会变得极不稳定。另一个典型情况是系统非线性的局部斜率本身就会改变符号比如某些对象的 PPD 在工作点切换后会从正变负如果不重置控制律里 φ̂(k)/(λφ̂(k)²) 的符号会突然翻转等价于把一个正反馈回路瞬间接进系统发散是必然的。6.2 λ、μ、η 三个参数的配合关系新手上手最容易犯的错误是三个参数一起调最后完全不知道是谁起作用。我建议按顺序调。第一步固定 λ1.0、η1.0、μ0.5确认闭环稳定这一步的目标是跑通而不是跑好。第二步调 λλ 相对 φ̂(k)² 越大学习增益 φ̂/(λφ̂²) 越小控制动作越温和λ 相对 φ̂ 越小增益越接近 1/φ̂等效于做前馈逆模型补偿这时候系统对模型误差会变得敏感。实际调试中λ 从 1.0 往下降每降一半看一次跟踪曲线的振荡程度直到临界振荡出现前停住。第三步微调 η 和 μη 太大 PPD 估计值会来回跳现象是控制量高频抖动而输出误差并没有明显改善μ 主要压制估计值的突变在输出噪声明显时把 μ 调大到 1~2 是安全的。还有一个容易忽略的点λ 的作用和 Np 是耦合的。Np 拉大之后同一组 λ 下控制动作会变得更大因为在预测时域内累积的误差更多了。所以调完 Np 之后一定要回头重新看一眼 λ否则可能出现换了个 Np 就振荡的莫名其妙现象。6.3 预测时域 Np 和控制时域 Nu 的取舍MFAPC 的预测时域 Np 一般取 3~10。Np1 时退化成单步 MFAC没有任何预测优势Np 加大到一定程度后PPD 恒定假设的误差开始累积预测输出越来越假优化出来的控制序列也就不靠谱。我的经验法是先观察对象的开环响应时间常数 T再估算采样周期 TsNp 大致取 T/Ts 的量级再加 2~3 步的余量。对强滞后对象可以尝试 Np8~10但必须同步把 λ 调大一些来压制动作幅度。控制时域 Nu 在简化实现里一般取和 Np 相等代码最简洁。如果计算资源紧张或者控制量变化过于频繁可以将 Nu 减小让后几拍控制增量保持一致代价是构造矩阵时要做矩形化处理代码复杂度上升收益却不一定明显。我在仿真阶段始终用 NuNp因为滚动时域本身每步只执行第一个控制增量计算量完全可接受。6.4 迭代学习最隐蔽的坑迭代轴上的 PPD 漂移MFAILC 跑前期几代时一切正常误差快速下降但跑到第 30 代以后会出现一种隐蔽现象误差不再下降甚至偶尔反弹。我一开始以为是算法本身的问题反复查了代码最后定位到根因在迭代轴的 PPD 估计上。随着迭代进行控制输入逐渐稳定相邻两次迭代的输入差 Δu_i(k) u_i(k)-u_{i-1}(k) 越来越小逼近机器精度甚至变成纯噪声。此时估计器分母 μ|Δu|² 里噪声主导PPD 估计值开始漂移学习律得到的修正量也就失去了正确方向。解决思路有三条我在程序里都试过。第一给迭代轴 PPD 估计加遗忘因子让过于久远的历史数据对当前估计的影响衰减第二设置一个误差下界阈值当某次迭代的 RMSE 低于阈值时就停止学习避免在噪声水平上继续过度学习第三引入验证批次机制每隔几轮用一组不被更新的输入跑一次仿真以验证批次的误差为准判断是否继续学习。后面两条更重要因为它们对应一个工程常识学习算法应该在泛化误差最小的点停住而不是在训练误差最小的点停住。6.5 我自己的体会这套程序从搭框架到出结果不到两天但真正把它调得让人放心花了我好几天。我的体会是做无模型控制的仿真验证最忌讳的是拿一组精心调过的参数跑出一条漂亮的收敛曲线就宣布工作完成。我会习惯性地在程序里加上三项压力测试输出叠加幅值 0.01 的高斯白噪声、初始状态偏移到非零值、参考轨迹在中途突然切换。这几项不通过算法就不具备可交付的价值。另外MFAPC 和 MFAILC 的分类意识很重要多数实际任务并不是非此即彼而是时间轴和迭代轴的问题交织在一起。我的下一版程序已经跑通了两者的组合式更新批内用 MFAPC 抗扰批次间用 MFAILC 学习跟踪效果比我单独使用任何一个都要稳。这个组合的思路和代码等我有空再单独整理一篇出来。