← 返回博客2026-06-26

我们管它叫 GTO,但它不是

我们 App 里有个按钮写着「GTO」。它在读一张 169 格的表,根本没在解一局博弈。诚实的分级——开局表 vs CFR 近似 vs 真解出来的树——以及我们为什么写了个单测:只要「GTO」这个词敢回来,就让构建挂掉。

问题

在扑克软件里,「GTO」大概是被滥用得最狠的三个字母。放到 AI 工具里,那个对应的词叫「最优」——同一个谎,换了件外套。它们都承诺一件很具体的事:博弈论最优、不可被剥削的答案——而几乎从不兑现。我知道,因为有一阵子,我们自己也有个按钮写着 GTO。

它根本没在解任何东西。它读一张预先算好的范围表——169 个格子,一手起手牌一个——然后把表里写的念给你听。有用!但查表不是求解。所以这篇真正想问的,不是「你的工具好不好」,而是:你那个「最优」的输出,到底算了什么?

营销话术底下藏着一套真实的分级,而大多数工具——包括不久前的我们——把这一整套都糊成了一个词。

三个层级,以及各自能说什么

不管你拿到的是哪一个,点下按钮的手感都一模一样。标签是唯一能告诉用户「手里这个是什么」的东西——这恰恰是它不能拿来当营销的原因:只有标签能分开「我读了张表」和「我把你的局面解出来了」。

层级 — 它实际算的东西诚实的标签不允许说
1 · 表 / 启发式 — 读一张预先算好的范围,或套一条规则(牌力、底池赔率、SPR)。请求时刻没有解任何博弈。「一个基线范围」GTO · 均衡 · 不可剥削
2 · CFR 近似 — 在一个刻意简化的博弈上跑反事实遗憾最小化,跑完固定预算就停,残余可剥削度靠事后实测——并非收敛到零。是真的均衡数学;方向上靠谱。「简化博弈的近似均衡」「GTO 解」
3 · 真解出来的树 — 在某个明确局面的完整、未抽象的博弈树上跑 CFR,跑到实测可剥削度足够小。是软件能逼近 GTO 的极限。「在这些范围 + 下注尺度下、达到此可剥削度的 GTO」不带任何输入的、光秃秃的「GTO」

我们到底发了什么——以及改了什么名

最近我们做了一次全 App 的诚实化。三个具体动作。

改名

翻前那个方法本来叫 gto_chart。改名分了两步:先改成 preflop_chart;一天后真正的 DCFR 求解落地,标识符和 API 字段随之改成 cfr_equity_realization——用实际生成这些数字的方法来命名。原本写着「这手牌在该位置的 GTO 开池范围内」的那句,现在是「……在该位置的翻前基线范围内」(英文里 GTO rangepreflop baseline range)。「GTO」这个词,已经从标识符、API 字段和中英两种语言的 UI 文案里彻底消失。

背后真有个求解器——但仍是第 2 层

那张表读的数据,不是我们抄来的某家厂商的图。它是我们自己的求解器净室生成的:在简化的 6-max / 100bb 翻前树上跑一个折扣-CFR(DCFR)变体,终局用全下胜率加每类的胜率实现模型。数据文件的 _source 字段带着这份声明,而一个契约测试会在声明里出现任何已知厂商名字的瞬间把它打回。但是——重点来了——我们给它贴的标签是 「CFR 近似均衡 · 胜率实现模型」(英文「CFR approx. equilibrium · equity-realization」),第 2 层,绝不是第 3 层。这个层级说的是数字的来历——一次离线跑完的 CFR 求解——而不是你请求那一刻发生的事:请求时刻依然是查表。所以答案文案本身只说「基线范围」,声称 CFR 的只有方法徽章。代码注释写得很直白:它绝对不能被叫做「GTO」。

真正的求解,单独围起来

我们确实造了一个第 3 层的求解器:在完整、未抽象的翻后树上做真正的逐局面 CFR,并把它实际达到的可剥削度报告出来。它住在自己单独的 crate 里,之所以隔离,是因为它包了 AGPL 许可的代码,我们要把这个「爆炸半径」锁死在一个工作区成员里。它的输出带着和翻前不同的方法类型——CfrEquilibrium vs CfrEquityRealization,分属两个 crate——代码内部混不到一起;线上的字段名再由测试钉死。它发布时带着闸门:只有当运营方显式开启并配置 AGPL §13 源码要约链接时才挂载——这个要约是结构上强制的,而不是对实际发布的校验;公开入口也受内存、并发和输入规模限制。即便到了这一层,分级给出的上限也只是「在指定范围和下注尺度下的 GTO」——而实际发布的徽章比这还保守:「翻后 CFR 近似均衡」,范围、下注尺度、迭代上限和实测可剥削度都放在详情行里。绝不是光秃秃的「GTO」。

连诚实的求解器也藏着的那条边界

有一点是改名也盖不住、还在绊人的:第 3 层也不是「GTO」——它是对某个抽象而言的 GTO。一个解只对你允许的下注尺度、你假设的范围、你跑的深度而言才是最优。换一组下注尺度,你得到的是另一个均衡。把「GTO」当成一个不加限定的名词——「这就是那个 GTO 打法」——在任何工具里都永远是过度宣称。「在这些输入下的 GTO」才是诚实的写法,而且这个差别不是抠字眼:两个下注尺度的解和六个尺度的解是不同的策略,一个看着「GTO」的新手根本分不清自己被卖的是哪一个。

我们第 2 层那张表底下还摞着自己的限定条件。其中两条是有记录在案的精度限制:胜率是蒙特卡洛估计(8,000 次试验,量化到整数百分点),不是精确值;终局胜率用的是固定代表性花色组合,不是真正的类内平均。再往下是一整族固定的模型输入,数据文件的 _source 自己就一条条列着——开池 2.5bb、3-bet 3.2 倍、4-bet 2.2 倍、5-bet 全下的尺度;对手 3-bet 频率约 11%、4-bet 约 5.5%;一个「最强防守者 + 最低防守频率下限」的防守模型;再加一层规则覆盖(超强牌强制包含、垃圾牌排除)——其中就有胜率实现系数(R 在 0.70 到 1.12 之间,徽章里的「胜率实现模型」说的就是它)。这些都没有一个是解出来的,而 _source 自己把话挑明——换一套尺度/R 模型,得到的就是另一组范围。这正是我们把输出标成依赖模型的原因——「CFR 近似均衡」,而不是「那个解」——并把每一条限定都按其本来面目记录下来,因为大致正确、标得精确,胜过说得精确、悄悄错了

把诚实的标签写成测试,而不是写成口号

靠一份风格指南去守的标签,忙起来的第一周就会漂回去。所以我们不靠风格指南守。这份诚实由每次合入前都必须过的测试托着:

# 渲染中英两种语言的求解面板,断言一个都匹配不上:
solver_view_preflop_baseline_copy_has_no_gto   ->  /\bGTO\b/  不得出现

# 翻前数据必须诚实地描述它自己:
preflop_chart_contract  ->  _source 必须含 "CFR" + "equity-realization"
                            _source 不得宣称光秃秃的 "GTO" 或 "canonical DCFR"

只要那个词以宣称的形式回来——无论是出现在面向用户的文案里,还是作为数据对自己来历的正面断言——合入前的测试门就变红。这才是能复用的部分,而它跟扑克毫无关系。如果你要发任何带「AI」、「最优」、「智能」、「GTO」字样的东西:用它实际算的东西来给输出命名——是那个抽象和那个容差,而不是你的愿景——再把这个诚实的名字放到一个测试后面,一旦有人去够那个更大的词,它就崩。营销词永远会想方设法回来。让测试门去拦它。

小结

  1. 「GTO」/「最优」是关于「算了什么」的断言,不是一种气质。它底下藏着三层:记下来的表、简化博弈的 CFR 近似、完整树的真解。
  2. 查表不是求解。我们把翻前的 gto_chart 改掉了(今天它叫 cfr_equity_realization),标签写成「CFR 近似均衡」(第 2 层)——它读的是一张净室生成的表,并没有解你的局面。
  3. 哪怕是真解,也只是「在这些输入下的 GTO」——下注尺度、范围、可剥削度。不加限定的「GTO」,在任何工具里都是过度宣称。
  4. 用一个会因为禁词而挂掉的测试去守诚实的标签——而不是一份风格指南。那个愿景词总会想回来;让测试门去拦它。