
Q-Learning这个词很多人在入门强化学习RL时都会先碰到它但真正把它跑通、跑稳、跑出效果的人没那么多。我最早接触RL是从一个很朴素的问题开始的机器怎么在没人告诉它对错的情况下自己学会一套动作序列Q-Learning给的答案是——让它在每个状态里试把试出来的结果记下来然后反复修正这份记录。这篇文章我打算把它从头到尾拆一遍它是什么、为什么这么设计、核心公式里每一项代表什么、代码怎么写、参数怎么调、踩过的坑怎么填。不管你是刚看完入门课程想动手写第一份代码还是已经在做机械臂控制、自适应PID、离线强化学习这类实战项目想回头补基础这篇都能当参考。需要说明的是我不会把它讲成教科书式的推导而是按真实开发时遇到的顺序来组织——先建立直觉再深挖公式然后上手写最后讲那些文档里不会写的经验。RL这个领域热词很多SFT还是RL、离线RL、联邦RL、图强化学习、基于模型的强化学习各种路线眼花缭乱但Q-Learning类方法始终是理解这些分支的地基因为它的思想最直白价值迭代 时间差分。1. Q-Learning在整个强化学习版图里的位置1.1 强化学习到底在解决什么问题先把场景说清楚。监督学习是给你一堆带标签的数据你学一个映射强化学习没有现成标签只有一个环境你做动作环境给你一个奖励和一个新状态你要在这个循环里找到一套长期回报最大的行为方式。这个长期回报最大是核心也是最反直觉的地方——有时候当下奖励高的动作长期来看是坑比如贪吃一个小奖励导致后面连大奖励都拿不到。所以强化学习的数学工具是马尔可夫决策过程也就是MDP它把问题抽象成五元组状态集合、动作集合、状态转移概率、奖励函数、折扣因子。Q-Learning就是在MDP框架下求解最优策略的一种方法属于值函数方法里的off-policy、model-free分支。off-policy的意思是你的行为策略和目标策略可以不一样model-free的意思是你不需要知道环境的转移概率模型。这两点让它非常好落地——真实世界里你几乎不可能拿到精确的转移概率但你可以和环境交互拿数据。我见过很多人一上来就想用策略梯度直接学策略结果训练曲线抖得没法看。反而是先用Q-Learning跑通一个小环境把价值、贝尔曼方程、TD误差这些概念摸清楚后面再看策略梯度算法会顺很多。这是我把Q-Learning放在第一课讲的原因。1.2 Q-Learning能被单独拎出来讲的原因强化学习算法少说几十种为什么大家总把Q-Learning单独拿出来当经典我的理解有三个层面。第一是它的更新规则极其简洁核心就是一个式子 Q(s,a) ← Q(s,a) α[r γ·max Q(s,a) - Q(s,a)]没有别的了。你把这个式子里的每一项都理解透你就理解了TD学习、bootstrapping、off-policy纠正这三个核心概念。很多看上去复杂的深度强化学习算法剥开外壳内核还是这个式子加了一套函数逼近。第二是它的收敛条件有理论保证只要每个状态-动作对被无限次访问、学习率满足一定条件Q值就会收敛到最优值。这一点对工程落地很重要你知道它理论上能收敛出问题的时候才能有方向地排查而不是瞎调。第三是它天然支持离线数据。你手里有一批历史交互数据不需要继续和环境交互也能用Q-Learning的更新方式去学。这点和现在热门的离线强化学习IQL这类是同一根源。所以别看Q-Learning老它的思想延伸出来的东西一点都不老。1.3 几种主流路线的横向对比为了让你对整体有张地图我把常见的几条路线列个表重点看和Q-Learning的关系。这张表是我自己做技术选型时整理的不追求全面只覆盖日常最容易纠结的选项。路线代表方法是否需要环境模型策略与价值关系典型适用场景表格型值迭代Q-Learning、SARSA不需要先从价值推策略状态动作离散且数量小深度Q网络DQN及其变体不需要网络拟合Q值状态连续、图像输入策略梯度REINFORCE、PPO不需要直接优化策略连续动作、高维动作基于模型的RLDyna-Q、MBPO需要用模型做规划样本获取代价高离线RLIQL、CQL不需要从固定数据学无法在线试错场景看这张表你会发现Q-Learning和SARSA只差一个max还是实际动作但on-policy和off-policy的区别就在这儿。DQN是把Q表换成神经网络内核没变。Dyna-Q是给Q-Learning加了一个想象的世界模型用真实数据学的同时也用模型生成的假数据练。IQL则是在Q-Learning的目标里加了一项约束防止对数据外动作高估。所以整个家族是围绕同一个内核长出来的理解了根枝叶就不难理解。提示选型时先问自己两个问题——状态空间能不能整形成有限个离散格子能不能反复和环境交互两个都是能Q-Learning就够用别上来就搬深度网络。2. 把Q-Learning拆到骨头里2.1 Q表用一张表格装下所有经验Q-Learning的核心数据结构是Q表本质就是一个二维数组行是状态列是动作格子里的数字代表在状态s下做动作a之后一直按最优方式走能拿到的期望累计回报。这个定义很关键它不是做这个动作当场能拿多少奖励而是做完之后还能捞到多少。举个生活化的例子你可以把它想成一张城市路况表横轴是你现在在哪个路口纵轴是你选了哪个方向走表格里填的是从这个方向走一路最优决策下到目的地的预计总耗时。你每走一次就用实际耗时去修正这个预计值。修正多了这张表就越来越准最后你只要在每个路口挑预期耗时最小的方向走就行。表格型的优点是不需要任何函数逼近存储和查表都极快调试也有迹可循——你可以直接把Q表打出来看。缺点是状态一多表就爆炸。如果状态是4个变量的连续值每个离散成100份那状态数就是一亿表直接内存溢出。所以表格型Q-Learning的适用边界非常清晰离散、低维、有限状态。像网格世界、简单的库存管理、状态机式的调度问题它是最省事的选择没必要上神经网络。2.2 贝尔曼最优方程与更新公式的来历更新公式那一行很多人背下来了但没想过为什么。我们一步步推。定义最优价值函数Q*(s,a)表示在状态s做动作a之后按最优策略行动能获得的期望折扣回报那么它满足贝尔曼最优方程Q*(s,a) E[ r γ·max_{a} Q*(s,a) ]右边这个期望是因为环境转移可能有随机性同样的动作可能到不同状态、拿不同奖励。实际操作里我们拿一次采样的结果来估计这个期望也就是用 r γ·max Q(s,a) 当作目标值。这就构成了时间差分TD的思路不用等整条轨迹结束走一步就能更新一次。那么更新就是拿估计值往目标值挪一小步Q(s,a) ← Q(s,a) α · [ 目标值 - Q(s,a) ]中括号里那项叫TD误差。α是学习率控制每步挪多远。γ是折扣因子控制未来奖励的重要程度。这里有个细节常被忽略max这个是针对下一状态所有动作取最大而不是针对当前选择的动作。这一步是Q-Learning off-policy属性的来源——它假设下一步会按最优动作走不管你实际怎么走。所以哪怕你的行为策略是纯随机的学到的Q值仍然在逼近最优值。这也是为什么Q-Learning可以在离线数据上跑数据从哪来不重要更新规则假设的是最优后续。2.3 ε-greedy探索还是利用的两难光有更新公式还不够你得先有数据。如果一开始就完全按Q表选动作Q表全是初始值通常是0那选出来的动作是随机的但这不叫探索这叫没有探索机制而且一旦某个动作偶然被选到、Q值稍微高一点之后就会一直选它其他动作永远没机会被更新学出来的策略会非常差。这就是经典的探索-利用困境。最常用的解法是ε-greedy以概率ε随机选一个动作以概率1-ε选当前Q值最大的动作。ε通常是衰减的比如从1.0开始随着训练步数线性或指数衰减到0.01。这样做的好处是前期以探索为主把每个状态动作都覆盖到后期以利用为主稳定收敛。这里有几个实操细节值得说。一是ε的衰减速度要匹配你的训练总步数如果环境有10万个状态你只跑1万步那再好的衰减策略也覆盖不完学不出来。二是最好记录每个状态-动作对被访问的次数训练完后看一眼是否有大量格子从没被更新过如果占比很高说明要么ε太小要么训练步数不够。三是有些实现用乐观初始化把所有Q值初始化成一个正值天然鼓励探索没见过的动作这个在确定性环境里偶尔比ε-greedy更好用但在随机环境里要小心容易造成过度探索。2.4 超参数的物理意义与取值直觉学习率、折扣因子、ε这三兄弟的取值直接影响结果我给一张表说明它们的物理含义和常用范围。这张表是我自己反复调过之后总结的不是照抄文档。参数物理含义常用范围调大后果调小后果α 学习率每步朝目标挪动的比例0.01 到 0.5学得快但容易震荡不收敛学得稳但需要更多步γ 折扣因子未来奖励的衰减力度0.9 到 0.99更看重长期方差大收敛慢短视只顾眼前奖励ε 探索率随机行动的概率1.0 衰减到 0.01探索充分但学得慢容易卡在次优策略衰减步数ε从初始降到下限的步数总步数的 50% 到 80%后期探索过多过早收敛到局部最优我一般会先把γ设为0.95α设0.1ε从1.0按指数衰减到0.05先跑一遍看曲线。如果曲线前期上升太慢说明探索不够或者状态太多先把ε的下限提高一点同时检查状态覆盖。如果曲线后期剧烈震荡八成是α太大降到0.05或者0.01再看。如果收敛到一个明显不是最优的平台通常是ε衰减太快或者γ太小导致只看短期。这几个判断顺序基本能解决八成调参问题。还有一个容易踩的坑γ不能设成1.0除非你的任务有明确的终止状态且回合长度有限。γ等于1在无限回合任务里会导致回报期望发散Q值会一直往上涨没有上界训练直接失控。3. 从零写一个能跑起来的Q-Learning3.1 环境挑选与问题定义写代码之前先挑一个足够小、能一眼看穿对错的环境。我推荐用网格类环境比如经典的FrozenLake或者自己手写一个走格子到终点的小世界。理由是它的最优策略你可以人工推出来跑出来的策略对不对一眼就知道不用等看曲线猜。我下面用自己写的一个小环境做演示规则是一个5x5的格子起点左上角终点右下角中间放几个障碍每走一步奖励-1撞墙原地不动也扣-1到达终点奖励10并结束回合。这个设定下最优策略就是最短路径长度是8步理论最优回报是 -7 10 3。我先说明为什么这么设计奖励。每步-1是为了逼它走最短路径如果不给步数惩罚它可能绕圈也能到终点学出来的策略不实用。终点给正奖励是为了形成正反馈。障碍设计成撞上去扣分或者原地不动都行我选原地不动避免它学会故意撞墙来规避其他风险。奖励设计是强化学习里最需要经验的部分后面第4节还会专门讲。顺便提一句用现成的gymnasium也是可以的但要注意新版API的接口变化比如reset和step的返回值包装方式变了老教程代码直接搬会报错。自己写环境的好处是完全可控调试时你能精确知道每一步发生了什么。3.2 完整代码实现与逐段说明代码我写得尽量直白把关键逻辑都拆开方便你对照着改。语言用Python依赖只有numpy。import numpy as np import random # 环境定义5x5 网格 GRID 5 OBSTACLES [(1, 1), (2, 2), (3, 1), (1, 3)] START (0, 0) GOAL (4, 4) ACTIONS [(-1, 0), (1, 0), (0, -1), (0, 1)] # 上下左右 def is_valid(pos): r, c pos if r 0 or r GRID or c 0 or c GRID: return False if pos in OBSTACLES: return False return True def step(state, action): dr, dc ACTIONS[action] nxt (state[0] dr, state[1] dc) if not is_valid(nxt): nxt state # 撞墙或撞障碍原地不动 if nxt GOAL: return nxt, 10.0, True return nxt, -1.0, False # 状态编号把二维坐标压成一维索引方便放进Q表 def to_index(pos): return pos[0] * GRID pos[1] N_STATES GRID * GRID N_ACTIONS 4 # 超参数 alpha 0.1 gamma 0.95 epsilon 1.0 epsilon_min 0.05 epsilon_decay 0.995 episodes 3000 Q np.zeros((N_STATES, N_ACTIONS)) history [] for ep in range(episodes): state START total_reward 0.0 for step_count in range(200): s_idx to_index(state) # epsilon-greedy 选动作 if random.random() epsilon: action random.randint(0, N_ACTIONS - 1) else: action int(np.argmax(Q[s_idx])) nxt, reward, done step(state, action) n_idx to_index(nxt) # Q-Learning 更新 best_next np.max(Q[n_idx]) td_target reward gamma * best_next * (0 if done else 1) Q[s_idx, action] alpha * (td_target - Q[s_idx, action]) state nxt total_reward reward if done: break epsilon max(epsilon_min, epsilon * epsilon_decay) history.append(total_reward) # 打印学到的策略 arrows [↑, ↓, ←, →] for r in range(GRID): row [] for c in range(GRID): if (r, c) in OBSTACLES: row.append( X ) elif (r, c) GOAL: row.append( G ) else: best_a int(np.argmax(Q[to_index((r, c))])) row.append( arrows[best_a] ) print(.join(row))这段代码里有几个点我要特别说明很多人第一次写会写错。第一终止状态的处理。在计算td_target时乘了(0 if done else 1)这一步不是可有可无的。如果不乘终止状态后面已经没有未来了但代码还是会把终止状态的max Q加进去而终止状态的Q值往往因为更新被抬得很高导致最后一步的目标值虚高整个Q表都会偏。这是新手最容易漏的一处。第二状态索引压缩。Q表是二维数组行数等于状态总数我用to_index把坐标映射成整数。如果直接用元组当字典键也能跑但用数组查表更快而且能直接argmax。状态数不大时两者差不多状态上万时数组优势明显。第三双重循环里的最大步数限制。内层for循环给了200步上限是为了防止策略学坏时一直在原地打转导致死循环。这个上限在调试期非常有用可以帮你发现策略一直在原地这类问题。3.3 训练结果与曲线解读跑完3000个回合你会看到history里的每回合总回报从早期的-100左右逐步爬升后期稳定逼近3。打印出来的策略图应该是一条清晰的从起点到终点的路径箭头方向指向最短路线。如果打印出来是一堆箭头指向墙或者绕远路说明训练有问题需要排查。我在调试时一般会看三条线。第一条是每回合总回报的滑动平均看整体趋势是不是单调上升。第二条是每回合步数最优解应该是8步如果一直停在20步以上说明它在绕圈。第三条是ε的衰减曲线确认它有没有按预期降到下限。有个细节要注意单回合回报曲线抖动很大是正常的因为ε-greedy本身带随机性而且早期状态访问分布不均。看趋势一定要做滑动平均窗口取50到100回合。我见过有人直接看原始曲线然后以为没收敛其实滑动平均已经明显上升了。如果跑完发现回报停在0附近上不去最可能的原因有两个一是ε衰减太快前期没探索够二是障碍物把最短路径挡住了而你设置的障碍让最优路径变长回报上限本来就低。第二种情况要先手算一下理论最优值再和实际对比。3.4 参数调优的实测记录我把几组参数的实际表现整理成表方便你对照。这些是我自己跑出来的结果同一环境同一随机种子下重复5次取平均。αγε衰减收敛回合数最终平均回报现象说明0.10.950.995约12003.0稳定推荐起点0.50.950.995约8002.6学得快但后期有小震荡0.010.950.995约25003.0很稳但慢适合状态多时0.10.80.995约15002.2短视路径偏长0.10.950.99约20002.4探索不足偶尔卡次优0.10.950.9不收敛0.5ε降太快大范围状态没覆盖看这张表能得出两个结论。一是α和γ的搭配比单个参数重要α大配合γ稍小有时候比两个都大更稳。二是ε衰减速度对结果影响非常大这一项经常被新手忽略但它其实是探索预算的直接控制。我的习惯是先固定α0.1、γ0.95只调ε衰减找到能让曲线平稳上升的衰减率再去微调α。还有一点这张表是针对这个25状态的小环境别照搬到别的任务上。状态数翻十倍同样的ε衰减率可能就不够用了因为你需要覆盖的组合数是指数级增长的。判断标准不是参数值本身而是训练完后有多少状态动作对从没被更新过。4. 实战排错那些让Q表死活收敛不了的坑4.1 常见症状速查表实际做项目时报错很少直接告诉你原因都是症状。我把遇到过的高频症状整理成表附上排查方向。这张表我建议你贴在手边出问题先扫一遍。症状可能原因优先排查项Q值一直涨没有上界γ设成了1或接近1且无终止检查折扣因子和回合终止回报曲线长期在低位平台ε衰减过快或状态覆盖不足统计未访问的状态动作对后期剧烈震荡α过大降到0.05以下观察策略学会绕圈缺少步数惩罚或惩罚太小检查每步奖励设置训练初期就收敛到次优探索不足Q初值太乐观提高ε下限或改初值相同状态动作Q值忽高忽低环境随机性大且样本少增加采样或降低α离线数据训练效果差数据分布覆盖不足检查行为策略的探索程度这里面我踩得最狠的是Q值一直涨。当时做一个库存优化的小项目γ设了0.99回合没有明确终止结果Q值每轮都在往上飘最后数值溢出。后来才想明白没有终止状态的无限任务必须让γ严格小于1而且最好是0.9到0.95之间。这一步理论上是收敛条件工程上是保命线。4.2 状态离散化的坑真实项目里状态往往是连续的比如位置、速度、温度这类。用表格型Q-Learning就必须离散化。这里有两个坑。一是分桶粒度。分得太细状态数爆炸训练根本覆盖不完分得太粗本来需要精细区分的状态被合并了学出来的策略粗糙。我的经验是先粗分比如每个维度分5到10个桶跑通了再看性能瓶颈在不在离散精度上是的话再逐维细化。不要一上来就追求高精度。二是边界处理。连续量的取值范围通常是无界的离散化前要设上下限超出范围的值要截断到边界桶。如果忘了截断会出现大量落在边界外的取值被分到同一个桶里或者直接索引越界。我在做自适应PID控制器时就遇到过误差值偶尔会超出预设范围没做截断导致一部分极端情况被错误归类控制效果在特定工况下明显变差。离散化的另一个隐藏问题是维度灾难。5个维度各分10个桶就是10万个状态每个状态4个动作就是40万个格。看着不多但要每个格都被充分访问训练步数得上百万而且采样效率极低。这种时候就该考虑函数逼近了也就是后面第5节讲的方向。4.3 奖励设计与终止状态处理奖励设计是强化学习里最像手艺的部分没有标准答案但有几个原则可以用。第一奖励要稀疏但不要过于稀疏。纯稀疏奖励只在成功时给正奖励在很多任务里根本学不动因为随机探索撞到成功的概率太低。可以加一些引导性的中间奖励比如距离目标变近给一点正反馈但引导奖励要小心给多了会诱导agent反复刷这个奖励而不去完成真正任务。第二惩罚要克制。有些实现喜欢用大额负奖励来惩罚撞墙、超时结果agent学会了一个什么都不做的策略因为做任何动作都可能被罚不动反而惩罚最小。我一般把每步惩罚设得很小靠时间累积来逼它走最短路径而不是设一个大罚值。第三终止状态的处理要一致。不管你是到达目标终止还是超时终止都要在更新时正确区分。超时终止和真正终止不一样超时终止后面的状态其实还有价值只是被截断了所以严格来说超时那一步不应该把后续价值清零。我一般做法是真正达成目标用done标记并清零后续超时用一个单独的truncated标记保留后续价值估计。这一点在通用环境接口里是有区分的自己写环境时容易忽略忽略之后学出来的策略会在临近超时时行为异常。注意奖励和终止处理一旦写错曲线可能仍然会上升只是上升到一个次优平台你很难察觉。调试时务必用一个小到能人工算出最优值的环境验证一遍。5. 从表格到神经网络Q-Learning的延伸路线5.1 Q表撑不住时怎么办当你面对的是图像输入、连续状态、高维传感数据时Q表这条路走不通了因为状态无穷多。解法是放弃存表改用函数来近似Q值输入状态和动作输出一个数值用参数化的函数比如神经网络来拟合。这就是值函数逼近的思路Q-Learning的内核完全没变只是把查表取值换成了前向计算。这里有个理论上的大坑叫致命三要素函数逼近、bootstrapping、off-policy三者同时出现时训练可能发散。Q-Learning恰好同时具备后两者一旦加上函数逼近就踩到了这个组合。DQN之所以能成功很大程度是靠三个工程补丁把这个不稳定性压住了。所以理解DQN不是去理解一个新算法而是理解怎么让带函数逼近的Q-Learning稳住。5.2 DQN的三个关键补丁第一个补丁是经验回放。把交互产生的(s, a, r, s)四元组存进一个缓冲区训练时从中随机采样一批。这样做打破了样本之间的时间相关性。Q-Learning的更新假设样本独立同分布但连续交互的样本高度相关直接拿去做梯度更新会让网络沿着一条轨迹的方向走偏。随机采样让每批数据更接近独立梯度方向更稳。第二个补丁是目标网络。更新目标值里的max Q用的是目标网络的输出而目标网络的参数是隔一段时间才从主网络同步过来的。如果不这样做目标值本身也在随着主网络快速变化相当于你在追一个自己一直在跑的靶子容易发散。目标网络让靶子在一段时间内固定追起来稳。第三个补丁是奖励裁剪或梯度裁剪。把奖励统一缩放到一个合理范围或者在反向传播时限制梯度范数避免个别大奖励把网络参数一下带偏。这一步不是DQN原始论文的重点但在实际工程里几乎是必备的尤其是奖励尺度差异大的任务。这三个补丁的思想可以迁移到很多地方。我后来做离线强化学习时发现IQL这类方法本质上也是在处理同样的问题——怎么在数据分布有限的情况下让Q值估计不过度乐观。它的做法是学一个上分位数或者用expectile回归来估计价值避免对数据里没出现过的动作给出过高估计。5.3 Q-Learning类方法还在哪些场景发光很多人以为Q-Learning类方法只适合教学和小游戏其实在不少工程场景里它依然是首选。我接触或了解到的几个方向。一是自适应控制。比如自适应PID控制器的参数整定可以把系统的误差和误差变化率离散成状态把PID参数的调整量当成动作用Q-Learning在线学习最优调整策略。这类场景状态维度不高离散化后规模可控而且控制任务对可解释性有要求表格型的Q表能直接看出来每个状态该调什么比黑盒网络更让人放心。二是资源调度和库存管理。这类问题的状态通常是库存量、需求量等级几个离散量动作是补货量或分配方案天然适合表格型。而且这类业务往往不允许在线随便试错所以更适合用历史数据做离线Q-Learning这正好对应前面说的off-policy优势。三是简单的机器人行为决策。机械臂的强化学习实战里高精度连续控制一般交给策略梯度或DDPG、SAC这类但上层的行为选择比如先抓哪个物体、走哪条路径常常用离散的Q-Learning来做下层再用传统控制做细跟踪。这种分层结构在工程上很常见上层慢、离散、需要可解释下层快、连续、要求精度。四是作为基线。任何新算法做出来和表格型Q-Learning在简单环境上比一比是论文和工程里的常规操作。它的结果稳定、可复现是很好的参照。我评估一个复杂算法时第一步永远是先在小环境上和Q-Learning对齐看复杂算法有没有带来实际提升还是只是引入了更多不稳定因素。5.4 离线强化学习与IQL的取舍现在离线强化学习很热IQL、CQL这些方法讨论度很高。我的看法是如果你的数据量不大、状态离散、任务简单不要急着上这些方法老老实实表格型离线Q-Learning可能效果更好因为它的偏差来源清晰容易诊断。IQL这类方法的价值在于数据是连续高维的、无法在线交互、数据质量参差不齐的场景它用一些统计手段来抑制对分布外动作的高估。这里有个实际选型的判断标准看你的数据覆盖度。如果行为策略探索得比较充分数据里各种状态动作组合都有那么普通Q-Learning的估计就还算靠谱。如果数据是某个人为策略采出来的覆盖很窄那Q-Learning会对没见过的动作给出高估学出来的策略一放到真实环境就崩这时候才需要IQL这类带保守性的方法。我自己在做离线项目时的流程是先用Q-Learning跑一版当基线记下它在验证集上的表现再上IQL对比。如果IQL提升不明显说明数据覆盖度其实够用简单的就行如果IQL明显更好说明数据分布偏窄保守性约束起了作用。这个对比过程本身就是对数据质量的一次体检比直接拍脑袋选算法靠谱得多。最后分享一个我自己的习惯每学一个新版本的算法不管是SFT到RL的路线切换还是从表格到深度网络的升级我都会回到这个5x5的小网格上重写一遍最朴素的Q-Learning。环境小到能在脑子里推演任何一步的数值对不对都能立刻验证。这个习惯帮我抓出过不少看起来没问题但实际写反了的bug比如done标记乘错位置、argmax用在了错误的状态索引上。地基打牢了上面盖什么楼心里都有底。