用 AI Coding + 强化学习,打造一个漫威终极逆转陪玩 AI
—— 算法、工程,与驾驭 Claude
一套用 AI Coding、从零训练出一个卡牌对战 PVP AI 的完整记录,分三部分:算法 —— Transformer 状态编码、决策注意力指针头、公平对手与课程学习,如何让它在一个不完全信息、动作组合爆炸的对抗环境里打到超过中高段位人类玩家;工程 —— 几十台机器的分布式训练、模型热替换、可信度量,怎么把它稳稳跑起来、数字信得过;驾驭 Claude(人在回路) —— 把 AI coding 协作搭建成一套 Loop Engineering 体系:6 层嵌套 loop 把每个"一次性动作"都嵌进闭环,人类反馈作为元控制信号经由 6 条工程化通道(握手门 / 选项问询 / 方案审查 / 体感警报 / 一句话扳回 / 复盘归档)注入;项目里那堵困了很久、最后发现根本不是"算法墙"的墙,正是被这套体系拆穿的。如今这套方案已从单卡组验证规模化到 21 套上线,在真人对局里拿到整体 70.6%、对最强无限段玩家 66.2% 的胜率(完整线上答卷见文末尾声)。
这篇分享分三部分 + 一段尾声。第一部分 · 算法讲怎么把一个会打牌的 AI 训出来 —— 重点是两件容易被忽略的事:为什么对手的"公平性"比"强度"更重要,以及一个看似无关紧要的动作建模缺陷如何伪装成一堵"能力上限"。第二部分 · 工程讲怎么让它在几十台机器上稳稳跑起来、数字信得过。第三部分 · 驾驭 Claude讲怎么把 AI coding 协作搭建成一套 Loop Engineering 体系:6 层嵌套 loop 让每个动作都嵌进闭环,人类反馈作为元控制信号经 6 条工程化通道注入 —— 这是 AI Coding 时代真正落地的样子。尾声 · 线上答卷回到最实际的一个问题:玩家买账吗 —— 从上一代人机的真实不满,接到 21 套上线打真人的成绩单。下面是目录。
算法:把一个会打牌的 AI 训出来
这一部分讲算法本身:从把一局牌面编码成什么、用什么网络,到为什么对手的"公平性"比"强度"更重要,再到那堵伪装成"能力上限"、其实是动作建模缺陷的"墙"是怎么被穿过去的 —— 最终打到超过中高段位人类玩家。
为什么卡牌对战是个难啃的 RL 问题
训练的目标环境是一个回合制集换式卡牌对战:双方各用一套 12 张的卡组,对局共 6 个回合,每回合能量从 1 递增;场上有 3 个地点(路),每回合双方同时把牌面朝下打到某个地点,然后一起翻开结算;占下 2 路者赢。听上去简单,但对 RL 来说它同时把几个最棘手的特性叠在了一起:
它是一个 不完全信息 + 组合动作 + 高方差 + 长程依赖 的对抗博弈。任何一条单独都不算新鲜,难的是四条同时在一个不到 60 维动作空间里纠缠。
系统总览:训练端、数据通道、训练集群
整体是一套经典的分布式 PPO:少量 GPU 在中心做策略更新,大量纯 CPU 机器在边缘并行产对局数据,模型以 ONNX 为统一载体在两端流转。
(状态, 动作, 回报) 轨迹打包。几个值得强调的工程选择:
- 双语言、单载体。训练侧是 Python(PyTorch + PPO),推理侧(训练对局、评估、线上)是 C#,两端通过 ONNX 对齐。轨迹数据有一套跨语言序列化契约,保证 C# 写出的样本 Python 能逐字段还原。
- worker-pull 数据通道。副机只做"单向出站 HTTP POST"上报轨迹、并主动"拉"模型,不在副机上开服务端口。新机器自注册、撤机靠心跳自动摘除,集群规模可弹性增减。
- 推理用游戏引擎当"免费的、零误差的环境模型"。这点在第 4、7 节会变成关键 —— 训练对局里每打一张牌都真正落进引擎、刷新状态,而不是让网络去"脑补"出牌后的局面。
状态表示与神经网络
核心网络是一个把"局面"编码成实体 token 序列、再用 Transformer 做关系建模的 actor-critic。规模约 15M 参数(实测约 15.1M;hidden 384 / 10 层 / 12 头 / FFN 768)—— 刻意走"小而专",验证范式靠的是博弈深度而非参数堆叠。
为什么选 Transformer
这不是赶时髦。先看清楚"状态"到底长什么样,架构选型几乎是被它逼出来的 —— 一局牌面本质上是一组带类别、无固定顺序、数量还会变的实体(手里几张牌、场上哪些卡、3 个地点),而胜负又高度依赖实体之间的关系(这张牌配那个地点、combo 卡彼此的协同、我方与对方的对位)。把这几条诉求摊开,候选架构里只有一个全中:
| 候选 | 建模实体间关系 | 无序集合(置换等变) | 变长输入 | 动作头自洽 |
|---|---|---|---|---|
| MLP | ✕ 拍平成定长向量、丢结构 | ✕ | ✕ 定长 | ✕ 外挂 |
| CNN | △ 只擅长局部空间 | ✕ | ✕ | ✕ |
| RNN / LSTM | △ 需强加一个顺序 | ✕ 顺序敏感 | ✓ | ✕ |
| Transformer | ✓ 注意力建模两两关系 | ✓ 顺序不敏感 | ✓ padding mask | ✓ pointer 头 |
三点正好命中:① 注意力就是为"实体两两之间谁影响谁"而生的;② 牌和卡是一个无序集合,不像图像有空间网格、也不像文本有先后,模型靠 token 的"类别 embedding"而非位置来区分它们,天然置换等变,不会强加一个并不存在的顺序;③ 手牌数、场上卡数随对局变化,padding mask 直接吃下变长。MLP 要先把局面拍平成定长向量、当场丢掉结构;CNN 假设的是局部空间平移不变(3 个地点并非图像网格);RNN 则要硬给一组无序实体安排先后 —— 三者都和状态的真实形态不合。
选了 Transformer,动作头能原生复用同一套注意力 —— "哪张牌打到哪个地点"本就是"主体 token × 目标 token"的配对,正好是一张注意力图(下一节详述)。换成别的架构,动作就得外挂一个输出层去硬记 58 个动作。也就是说:Transformer 让状态编码和动作决策共用一套语言,首尾自洽。后面会看到,正是这一点让我们最终拆掉了那个困住胜率的瓶颈。
把局面拆成实体 token
不像图像那样喂一个扁平向量,我们把局面里每一个"实体"做成一个 token,让注意力机制去学它们之间的关系(这张牌和那个地点、我方和对方)。序列大致由这几类 token 组成:
关键不是"有多少 token",而是它们的索引和后面的动作语义一一对齐:场上卡槽用 loc×4+c 编址,这套编址在状态编码器和动作解码器里是同一套坐标。这个对齐让下一节的"动作即注意力"成立。
两个输出头:策略 + 价值
- 价值头(critic)从全局 token 读出当前局面的胜率估计
V,用于 PPO 的优势估计;它同时也是对局中"要不要加倍/撤退"这类元决策的天然信号。 - 策略头(actor)不是普通的
Linear(58),而是一个"决策注意力指针头"—— 这是这套方案在网络结构上最不一样的地方,单独拎到下一节讲。
决策注意力指针头:动作空间即注意力图
这个游戏的合法动作天然是"主体 × 目标"的结构:把某张牌(主体)打到某个地点(目标),或把场上某张牌(主体)移动到某个地点(目标)。既然结构本身就是一张"谁指向谁"的图,那它就该用注意力来表达,而不是把它拍平、外挂一个全连接输出层去硬背。
于是策略头直接复用 Transformer 的 scaled dot-product attention,做成一个 pointer network:
这样设计有三个好处:
- 结构归纳偏置正确。"哪张牌去哪条路"本就是"主体 × 目标"的配对,注意力天生就在算这种配对。早期版本恰恰相反 —— 把 19 个主体和 3 个目标先 mean-pool 压平、再用一个全连接层硬背 58 个动作,这正是第 3 章说的信息瓶颈;换成 attention pointer 才把它拆掉。
- 不丢 per-token 信息。早期一版用 mean-pool 把所有 token 压成一个向量再过
Linear(58),这一步把每个 token 的细粒度信息压平了,事后复盘认为它是一个长期存在的信息瓶颈。换成 per-token 注意力后瓶颈解除。 - 合法性由引擎兜底。每一步出牌前,从游戏引擎取一份合法动作掩码(地点满了、移动是否合法……),盖在 logit 上。网络只在真正合法的动作里选。
一回合出多张牌:step-wise 自回归
一个回合可以连续出好几张牌。我们不让网络"一次吐出一整套组合"(那是带顺序的排列爆炸,训练算力撑不住),而是 逐步决策:
连招的收益依赖"前一张牌已经生效"的真实局面。让引擎做这个状态推进,等于用零误差的真环境替代了网络对动力学的脑补。网络只需回答"下一张打哪",combo 的因果链由引擎保证。代价是一回合要多次前向,但每次都是一次干净的小决策。
公平性设计:战争迷雾 + 公平对手
这套方案最核心的一条原则不是某个网络技巧,而是:训练价值 = 对手的公平性 > 对手的强度。模型最终面对的是公平、同时决策的真人;如果拿一个"会偷看你出牌"的对手来训练,会同时坏两件事 —— 既把策略逼崩,又教出一身在真实对局里用不上的歪招。
① 战争迷雾:把观测做成真实视野
观测编码强制走"部分可观测"(POMDP)口径,把真实对局里玩家本就看不到的信息从神经网络输入中抹掉:
• 双方已翻开、生效的场上牌
• 3 个地点及其效果
• 当前回合、双方能量、总分
✕ 对手的牌库
✕ 对手本回合刚打下、尚未翻开的牌
✕ 自己牌库里未抽牌的顺序
② 公平对手:强,但不偷看
训练对手用的是脚本化的贪心基线(scripted greedy)。同一套贪心引擎、同样 80% 概率选最优招(probDist = {0.8, 0.1, 0.05, 0.05}),唯一被改掉的是它的"出牌时机假设":
| 对手类型 | 决策方式 | 是否公平 |
|---|---|---|
| 看牌型(AfterOpponent) | 等你把牌打下、看到你已 staged 的牌,再决定自己怎么出 | ✕ 作弊(真实对局双方同时下牌,看不到) |
| 公平型(OnStart) | 每回合假设对手不出牌,只基于公开局面暴力贪心,不偷看你本回合的牌 | ✓ 公平(即真实 PVP 的同时决策) |
在更早的阶段,训练对手会偷看本回合出牌。结果是:大部分对局变成"信息劣势下怎么打都输",策略梯度长期拿不到"哪个方向更好"的有效信号,理性的收敛结果就是熵塌缩(孤注一掷某一路)。而且这种"防全知对手"的扭曲策略,搬到真实对局完全用不上。把偷看从训练里剔除,是后续一切突破的前提。
当然,公平也意味着"上限有限"—— 一个 80% 选最优、但不偷看、也不做跨回合规划的贪心脚本,棋力天花板大致就在五成胜率附近。突破它的常规思路是自博弈(让模型和自己的历史版本互搏,制造"够得着又打得过"的对手阶梯)。不过在当前这个单卡组的正式训练里,对手池全部是公平脚本梯队(Random + 三档公平贪心),自博弈作为保留的后续方向尚未启用 —— 而结果证明,仅靠"公平对手 + 把动作建模修对"就已经把那堵墙穿过去了(见第 7、8 节)。这本身是个意外却有价值的结论:墙不在对手强度上。
训练方法:PPO + 课程学习 + 双轨评估
PPO 主循环
策略优化用 PPO(clipped surrogate + GAE 优势估计 + 价值/策略联合损失;AdamW 优化器、BF16 混合精度、8 卡 GPU 数据并行)。关键超参(生产实际值):
| 超参 | 值 | 超参 | 值 |
|---|---|---|---|
| 裁剪系数 clip ε | 0.15 | 学习率 | 1e-4 |
| GAE γ / λ | 0.99 / 0.95 | batch / minibatch | 512 / 512 |
| 每批 update epochs | 4 | 价值损失系数 | 0.5 |
| 熵正则 ent_coef | 0.045(固定) | target KL | 0.04 |
| 梯度裁剪范数 | 0.5 | 总训练步 | 6 亿 |
我们曾上线一个"自适应加熵"机制:熵一低就自动加大熵奖励、想把探索顶回去。实测它两次把训练推向死亡螺旋 —— 在连招/对抗阶段,低熵其实是"策略在收敛到确定的正确打法",强行注熵反而用大梯度搅碎已学到的策略,KL 失控、胜率崩盘。最后改回固定熵系数(0.045)+ KL 自适应学习率兜底。教训:熵低不一定是探索枯竭,要先分清是"塌缩"还是"收敛"。
课程学习:让对手难度跟着进度爬坡
对手不是一上来就最强。课程按训练进度(已消耗的样本步数 / 目标步数)推进,逐阶段提高强对手(Expert)占比,把模型推向真实对局里最常见、也最难赢的分布。下面是当前单卡组正式训练采用的 4 阶段配置:
best checkpoint:一个抗噪声的"对 Expert 加权"指标
"哪个 checkpoint 最好"由训练侧自己评出,而不是手动挑。指标对 4 档公平基线的胜率加权,且每一项都取 Wilson 95% 置信区间下界(而非裸胜率,抗采样噪声):
轨道 ①(公平锚):只用公平对手,进入 best 指标,给一个不掺水的训练进度读数。轨道 ②(人类对齐):单独跑一个会偷看牌的满血强对手,不进 best,仅用来和"人类玩家在同样劣势下的胜率"做横向对标。一句话:偷看牌是"量人类水平的标尺",不是"训练材料"。
关键突破:那堵墙其实是"动作残废墙"
在很长一段时间里,无论怎么调,模型对强对手的胜率都卡在 五成 上不去。两条完全相反的技术路线 —— "大模型多任务通才"和"小模型单任务专才"—— 本该一个赢在容量、一个赢在专注,结果撞在同一个数字上,而且都在能力接近上限时熵塌缩。
一个 bug、一次调参失误,不会让两条对立的架构长期停在同一个数字上。我们一度把它归因为"任务定义本身的上限"。直到回头审视一个最基础、却从没被怀疑过的环节:动作到底是怎么映射的。
真凶:出牌和"结束回合"被捆死了
排查发现,早期的动作映射里,"出一张牌"这个动作被和"结束回合"绑定在了一起。后果是致命的:
反复撞的五成墙,很可能根本不是"任务难度墙",而是"动作残废墙" —— 一个每回合只能出一张牌的智能体,结构上就打不过出满牌的对手,再怎么调网络、调超参都没用。把动作建模修对、配上前面讲的"决策注意力指针头 + step-wise 多动作"之后,胜率立刻开始单调往上爬,一路穿过那个数字。
这也解释了为什么之前的所有努力像在原地打转:我们一直在错误的地基上换姿势。网络结构、对手公平性、课程设计都没错,但底层的动作语义残缺,让上层一切优化都被封顶。
结果:单调穿墙,打到超人类
修复后的模型(决策注意力 + step-wise 多动作 + 公平对手 + 战争迷雾)以冷启动从零开始训练,对公平强对手的胜率单调爬升、不卡顿、不塌缩,干净利落地穿过了那堵五成墙。
对标人类:同口径下超过中高段位人类约 12 个百分点
把模型放进"和人类完全相同的劣势"下(自己看不到对手 = 迷雾,对手会偷看自己 = 满血看牌强对手),它的胜率超过了会和 AI 对战的中高段位人类玩家:
这把"中高段位人类 ~49.8%"不是估出来的,而是从大规模线上真实对局里、用相同对手和相同信息劣势测出来的锚点。把它和模型的完整战力谱并排,更清楚它站在什么位置:
模型完整战力谱(best / argmax · 迷雾口径)
| 对手档位 | 胜率 |
|---|---|
| Random | ~97% |
| 公平 Beginner | ~84% |
| 公平 Intermediate | 81% |
| 公平 Expert | 76% |
| 满血看牌 Expert(最难) | 61.6% |
同一对手下,线上人类的真实胜率
| 对战方 · vs 满血看牌 Expert | 胜率 |
|---|---|
| 线上中高段人类(rank 50–99) | 49.9% |
| 线上无限段人类(Lv100+) | 49.8% |
| 本方案 RL | 61.6% |
口径与诚实标注:① 两表胜率都在"自己看不到对手、对手偷看自己"的相同迷雾劣势下,对手也是同一个"满血看牌 Expert",故可比;② 人类这条锚来自最新线上后台大盘 —— 从中高段(rank 50–99)到无限段(Lv100+)的人类打同一档满血强对手,胜率各段基本持平在 ~49.8%(rank 50–99 约 49.9%、无限段约 49.8%),这也正是这类脚本强对手的棋力天花板;③ 因此结论是"同口径下超过会和 AI 对战的中高段位人类约 12pp",而非"超过世界顶尖"(顶尖玩家在匹配机制下基本不与 AI 对战)。
上面这套数字是单卡组的破墙口径。同一套配方随后被原样复用到 21 套卡组、全部投产上线,并在真人对局里再次验证 —— 整体胜率 70.6%、对最强的无限段玩家仍 66.2%。这部分线上实战的完整证据、玩家口碑、以及与旧 bot 痛点的首尾闭环,见 文末尾声 · 线上答卷。
(best / argmax)
超过中高段位人类
反复塌缩的死亡点
~60MB
扛过历史"鬼门关"
历史上每次熵塌缩都精确发生在训练进度约 50%(强对手占比把期望胜率压破临界点的那个位置)。这一次,模型在同一个进度点上 熵 0.97、KL 0.007,对公平 Expert 仍稳在七成以上的高位,没有任何回落。这从反面印证了一件事:之前的塌缩是"任务被定义成怎么都赢不了"逼出来的数学必然,而不是 PPO 本身不稳定。把对手修公平、把动作修完整,塌缩就消失了。
推理性能(线上部署参考)
| 硬件 | 单次推理 | 单核 QPS |
|---|---|---|
| 服务器 CPU 单核(Broadwell 至强) | 60.5 ms | 16.5 |
| 桌面 CPU 单核(i7-12700F) | 44.0 ms | 23 |
| 入门 GPU(RTX 3050 · batch=1) | 9.2 ms | 108 |
单段 Transformer ONNX,约 60MB。一回合多动作(出 3–4 张)= 4–5 次前向,CPU 端约 250–300ms 完成整回合决策;GPU 的真正优势在多对局并发 batch。
方法论沉淀
如果说这套方案有什么能迁移到别的 RL 项目的经验,大概是这三条 —— 它们都不是"用了什么 SOTA 技巧",而是"避免了什么自欺"。
从"卡在五成"到"超过中高段位人类",中间真正起作用的不是某个漂亮的网络模块,而是把三件朴素的事做对:把动作建模建完整、把对手做公平、把尺子量准。剩下的,交给 PPO 和时间。
分布式工程实现
第一部分讲的是"AI 怎么学会打牌"。但要让它真的学得动,背后是一整套分布式工程:这套训练的本质,是用一大批廉价 CPU 不停产对局、喂给少数几张 GPU 更新策略 —— 这套基础设施决定了能不能、以及多快喂饱 GPU。下面是 5 块工程硬骨头,以及途中几次"从复杂退回简单"的取舍。
副机集群:两层进程栈 + 弹性自注册
N 台纯 CPU 副机要做到"插上电就自动加入、撤掉自动退出、崩了不连累训练"。做法是把每台副机的进程拆成控制面 + 数据面两层、全部对主机单向出站,主机端只维护一张内存里的活机表。
worker-pull:副机不开任何端口
副机与主机之间只有出站,副机上不监听任何端口 —— 三条控制出站(POST /heartbeat 注册+保活、GET /model/latest 拉模型、GET /progress 拉课程进度)+ 一条数据出站(POST /trajectory)。这让云主机、容器只要能出网就能接入,对 NAT / 防火墙极友好(为什么非得是这个方向,第 12 章有一段血泪史)。
弹性自注册:一张内存活机表
- 副机首启先
POST /register领一个 worker 编号(编号按 host 持久化、同机重启不漂移;克隆机靠boot_id+ 主机加随机后缀去重,避免共享 IP/MAC 的容器撞号)。之后每秒一次心跳。 - 主机端的活机表纯内存、零磁盘 IO,靠心跳驱动:30 秒(≈ 30× 心跳间隔,留足"漏一拍不误删"的余量)没收到心跳就把这台 lazy 剔除。加机器 = 自动出现,撤机 = 停掉进程、主机超时自动摘除,全程不用碰主机配置。
两个朴素但关键的取舍
⌊逻辑核数 × 90%⌋,异构机群(40 线程到 192 线程)自动适配,留 10% 给 agent/main 和系统。resume;只保留"进程内自愈"(supervisor 看子进程、agent 断线重连)。理由:worker 是无状态弹性资源,自动拉起反而会和部署脚本抢进程、让"老镜像复活"。三层运维命令体系
所有运维收口到三个入口,跨机层不写业务逻辑、只 SSH 进去调本机脚本 —— 保证单台机器的逻辑只有一处真源、不漂移:
| 入口 | 位置 | 职责 |
|---|---|---|
snap_worker.sh | 副机本机 | 副机进程栈的唯一控制入口:resume / stop / newtrain / remove / purge / restart-agent |
snap_master.sh | 主机本机 | 主机服务的统一门面(trainer / dashboard / server),转调既有工具不重写启停逻辑 |
cluster_ops.py | 控制机跨机 | SSH 下发,内部就是去调上面两个本机脚本(含批量 newtrain/resume、查全局状态) |
模型 A/B 热替换:进程级蓝绿 + 一局一致性
主机每 ~30 秒导出一版新模型,副机要在不中断对局的前提下换上去。我们没有在推理进程里"重载 session",而是做了一套进程级蓝绿(blue/green)双代切换 —— 这背后还藏着一条 PPO 算法正确性的硬约束。
落盘:单份文件 + 原子替换
副机 agent 每 5 秒用 ETag(= 文件大小-修改时间,只 stat 不读内容)问主机有没有新模型:没变返 304 不下载,变了才拉、sha256 校验通过后写到 latest.onnx.tmp,再 os.replace 原子改名成 latest.onnx。单份文件、原子替换 —— 这一步只保证"推理进程永远读不到写一半的文件",真正的 A/B 不在文件层,而在进程层。
切换:先起新代、再杀旧代、旧代跑完当前局
推理进程(dotnet selfplay)里的 ONNX session 只在启动时加载一次、整个生命周期不变,进程内没有任何"换模型"逻辑。真正盯着 latest.onnx 变化的是 shell supervisor:每 5 秒 stat 一次修改时间,变了就 ——
- 先用新模型起一整代新进程(gen A↔B 交替);
- 再给旧代发
SIGTERM,旧代跑完手头这一局再退出(30 秒宽限,超时才强杀,避免丢正在写的轨迹); - 新旧重叠 5–30 秒(这段副机短暂跑 2× 进程,靠内存富余扛);两次切换间至少冷却 180 秒,防止模型频繁更新导致旧代还没退完就被下一轮打断、进程越积越多。
换模型只发生在局与局之间,一局自始至终用同一个模型版本。深层原因不是工程图省事,而是算法正确性:PPO 的每条轨迹都记录了"旧策略在每一步选这个动作的概率 log πold(a|s)",更新时要算 πnew/πold 这个比值。如果一局前半段用旧模型、后半段用新模型,记录的 log_prob 就和实际动作来自两个不同的策略,比值算错、梯度学歪。所以"一局一致性"是被 PPO 逼出来的,而进程级切换天然满足它。
版本闸门:让"模型版本"和"数据新鲜度"闭环
每个推理进程在加载模型时算一个版本号 sha256前16位_时间戳,盖进它产出的每一条轨迹。主机这边维护"最近 100 个导出版本"的集合,轨迹的版本号不在这个集合里就丢弃。于是形成闭环:主机导出新模型 → 副机换代 → 新代轨迹带新版本号 → 主机版本窗口右移 → 旧代轨迹的版本号滑出窗口、被丢。这套以"版本成员资格"(而非步数算术)对齐的闸门,正好挡掉卡死的旧进程产的陈旧 off-policy 数据 —— 下一章你会看到它有多重要。
数据通道的三代演进
"副机怎么把轨迹送到主机"这件事,我们换了三代方案。每一代都是被上一代的真实痛点逼着改的 —— 这段演进本身就是一份"分布式数据通道避坑指南"。
第一代 · PUSH:被防火墙打回来
最直接的想法:副机主动把数据 push 给主机。设计、压测全过,真上灰度却当场失败 —— 主机的云安全组只放行了开发机 IP,副机经 NAT 出去的出口 IP 被直接 RST(主机网卡上的入站包计数是 0,说明包在上游就被挡了)。改安全组是跨团队大工程、还有多租户隔离风险,短期无解。
第二代 · streaming_pull:主机反向去拉
既然"副机连主机"被堵、"主机连副机"通,那就反转发起方向:主机在 GPU 进程里起一批异步客户端,主动连到副机的数据端口去拉轨迹(数据方向还是副→主,只是谁先握手反过来)。实测吞吐冲到 1020 局/秒(3.2× 老方案)。但痛点也实在:副机在 NAT 后面,主机要为每一台副机建 SSH 隧道、在一份 hosts 清单里登记,加机器就得改清单、重启通道,扩容高度手动、易错、还中断训练。
第三代 · worker-pull:副机单向出站,主机被动收
终态又把方向转回来,但这次彻底想清楚了:副机全程单向出站 HTTP,主机只当一个被动的单端口 server。控制面(心跳/模型/进度)和数据面(POST /trajectory)共用一个外网端口 + token 鉴权 —— 既绕开了第一代"入站被防火墙挡"的死结(只需放行一个端口),又干掉了第二代"每台建隧道 + 手动登记"的运维负担(心跳自注册)。而且这个 server 就跑在 GPU 训练进程的 rank0 线程里,收到的轨迹直接塞进同进程共享的队列、PPO 主循环直接消费,零跨进程 IPC。
① 防火墙友好:副机不开端口、只放主机一个外网端口;② 自注册弹性:心跳即注册、超时即摘,零主机介入,为镜像化批量扩容铺平路;③ 单端口集成:控制面 + 数据面一个 server,干掉隧道和独立同步通道;④ 零 IPC:rank0 收数据直接入队、主循环出队,没有多进程队列的拷贝开销。
吞吐优化的 A/B 实验:一次诚实的回退
这一章讲一件"做对了又撤回"的事。两组优化各自都有实测正收益,最后却被全部删掉 —— 因为它们换来的吞吐,悄悄偷走了数据的新鲜度。
诊断:8 张卡,只有 1 张在干活
先说 8 卡 DDP 怎么分工:只有 rank0 那张卡收数据,收到一批轨迹后广播给所有卡,每张卡只取属于自己的那条条带(envelope[rank::8])各自前向反向、再 all-reduce 同步梯度;另有两个 lock-step 同步点(轨迹量门取全局最小、minibatch 数截齐)防止快慢不一导致 desync。正因为"收数据"这件事压在 rank0 一张卡上,性能剖析才暴露一个刺眼现象:GPU0 利用率只有 8%,其余 7 张 92–100% —— 数据接收、解压、模型导出全挤在 rank0 这一个进程里串行,另外 7 卡在 NCCL 上空等它。三处 CPU 瓶颈:每 10 步一次的 ONNX 导出(占 rank0 主线程 58%、单次 5.5–6.8 秒、偶发 60 秒)、轨迹解压抢 GIL(接收线程 79.5% 忙)、广播序列化。结果消费速度 < 生产速度,接收队列长期撑满、丢弃约 30% 的数据。
两组优化,各有实测收益
两上两下:为什么还是全删了
| 节点 | 动作 | 结果 |
|---|---|---|
| 第 1 上 | A+B 两组同时上线 | 吞吐指标漂亮 |
| 第 1 下 | winrate 阴跌,全面回滚 | B 组把"原本与训练重叠的解压"挪到消费侧变串行,消费率反降 33% |
| 第 2 上 | 只上 A 组 + 数字化 SLA 告警 | 队列首次不再撑满,消费追平生产 |
| 第 2 下 | 再次回滚、彻底删除代码 | 根因锁定在数据新鲜度 |
异步化 + 步速加快,让副机用的模型落后了 12–14 个版本(同步时只落后 5 个)。PPO 是 on-policy 算法,数据越旧、重要性采样的偏差越被放大 —— winrate 从 0.42 一路阴跌到 0.354、4 个基线同步下滑,但 entropy / KL / clipfrac 全都正常,盯 entropy 的崩溃防线根本抓不到。
"PPO 指标正常 + winrate 单边跌" = 去查数据新鲜度,不是去调超参。entropy / KL / clipfrac 只反映"每次更新了多少",反映不出"更新的方向被旧数据带偏了"。后来正是靠第 11 章那套版本闸门(只收最近 100 个版本)把新鲜度钉死。顺带一提:同期另一个独立优化 —— 把 minibatch 从 256 加到 512、吞吐 +52% —— 因为不碰新鲜度,被保留了下来。
Model Mirror:把出口压力收成一对一
N 台副机每隔几秒就来主机拉一次模型,模型更新那一瞬间它们会同时涌来。一台镜像中继把这股扇出压力从主机卸到自己身上 —— 它几次"从复杂退回简单"的演进,比设计本身更有看头。
为什么要它
主机给副机发模型,出口 = N × 约60MB。真正烧带宽的不是稳态(95% 的请求因 ETag 没变直接返 304、不传字节),而是模型刚更新那一瞬间 N 台同时来取的尖峰。中继节点自己用一个 puller 每秒从主机拉一次最新模型、缓存到本地,副机改向它要 —— 主机的模型出口就从 N×60MB 收成 1×60MB。而副机的拉取协议一行都不用改(见下文"协议对齐")。
进程拓扑:1 个拉取 + 5 个服务,靠内核分发
中继内部是 1 个 puller_daemon(独占地从主机拉、原子写本地缓存和状态)+ 5 个纯服务进程。5 个服务进程用 SO_REUSEPORT 绑同一个端口、由内核按连接轮询分发 —— 既绕开单进程 GIL 的吞吐天花板,又对副机表现为一个端口。进程之间不用共享内存、不用 Redis,只用原子文件改名同步状态(写 .tmp 再 rename,读者要么看到旧的、要么看到新的,永不读到撕裂的半截)。
最初的设计想让 nginx 用内核 sendfile 零拷贝直接吐 60MB 文件、服务进程连文件都不碰。听上去完美,实跑却败给一个协议细节:nginx 的静态文件处理器会自己算一个 ETag、并丢掉上游带的 sha256 校验头 —— 副机每次看到的 ETag 都不一样、条件请求永远不命中,于是每次都全量重下,30 分钟烧掉 77GB。最终选择放弃零拷贝、保协议正确:反正 95% 的请求是 304 无 body,零拷贝省的那点根本不在关键路径上。连带蓝绿部署也一并简化掉了 —— 有了 5 个 SO_REUSEPORT 实例,单进程重启那 ~5 秒停机完全可接受,不必再搞复杂的端口接管。
协议对齐:副机零改动
镜像设计的硬约束是副机那套拉取逻辑一字不改。做法是:中继和主机复用同一个函数算 ETag(= 大小-修改时间)和 sha256,三端(主机 / 中继 / 独立 model server)返回的响应头字节级一致。副机在"主机直连"和"走中继"之间切换感知不到差别 —— 启动时它按"内网中继 → 公网中继 → 主机"的顺序探一次可达的源,自动选最近的一条路。
中继节点本身是台 16 核 / 31GB 的普通机器,无 sudo、无 systemd(用 nohup 守护、pip --target 装依赖)。它是全链路里唯一破例配开机自启的节点 —— 因为它是中心服务,不像无状态的副机可以"挂了人工拉起"。
断点续训:让 6 亿步训练随时停、无损接
一次正式训练要跑 6 亿步,期间机器会重启、训练会异常、超参要调 —— 全程不可能一气呵成。所以"随时停下、无损接续"不是锦上添花,而是长跑的生命线。
一个文件接住全部状态
恢复训练时,一个 state.pt 一次性接住五样东西:模型权重、Adam 优化器状态、全局 step、已消耗 timesteps、课程进度。少任何一样都会出岔 —— 丢了 Adam 动量会让 KL 短期飙高,丢了 timesteps 会让课程阶段算错、对手难度跳档。接续成功的标志很明确:日志打出 HOT RESUME: starting from step=N,train.jsonl 从 N 接着写而不是从 1 重来,课程进度不归零。
启动器带"验收后置条件"
启停训练不靠手敲 ssh + nohup torchrun(那是"看着起来了、其实静默失败"的经典坑),而是统一走一个启动器,把停旧 → 清 GPU 残留 → 拉新 → 验收做成一条流程。验收是硬性后置条件,任一不满足就非零退出、绝不假装成功(见上图)。万一 state.pt 缺失,还有三层 fallback:退化成"只恢复 step 计数"(从最新权重按 step 数字排序提取,Adam 重新开始、接受 KL 短期略高),保证训练能续上而不是从零再来。
配置即代码:SSOT + 冻结快照
训练超参由三份 yaml(trainer / worker / curriculum)做单一真源,全集群 sync 后 sha256 校验、不一致就中止。但有一个反直觉却关键的设计:续训时超参从 run 目录里的"冻结快照"加载,而不是当前 yaml。
因为训练中途万一有人改了 yaml(哪怕只是手滑),续训若直接读最新值,超参突变会让 PPO 发散。冻结快照在训练启动那一刻把超参定死,保证一次 run 全程超参自洽。要正经改超参,得显式"同步快照 + 热重启"才生效 —— 把"改配置"变成一个有意识、可追溯的动作,而不是随时可能污染长跑的暗雷。
可信的度量:把"尺子"当地基来建
第一部分讲过那个最痛的教训:项目前几个阶段栽在一把系统性高估的尺子上,长期在假信号上盲调。所以这套方案把"度量准"当成和算法同等重要的地基 —— 每一层都在防一种"自欺"。
胜率统计:宁可低估,不可高估
- 滑动窗口 + Wilson 置信区间下界:胜率取最近 1000 局的 Wilson 95% CI 下界而非裸胜率 —— 样本少时下界自动保守,防"手气好"被当成"变强了"。
- 平局算非赢:平局计入分母、不计入分子,只会拉低胜率、不会抬高 —— 一个一律往保守方向偏的口径。
- 与训练解耦:三个独立窗口(总体 / 按己方卡组 / 按对手类型)每 3 秒刷一次,节奏和 PPO 更新分开,互不阻塞。
best checkpoint:一个只升不降的棘轮
"哪个模型最好"由训练侧自动评、而非手挑。它沿用第 6 章那个 Expert 加权指标(4 档基线的 Wilson CI 下界加权 + 两道闸门:≥800 局才评估、+1pp 才更新),这里只补它作为"可信度量"最关键的一点 —— 做成棘轮(ratchet):
- 只升不降:metric 回落时 best 不退,峰值模型已存住,所以哪怕后面训崩,最好的那一版也丢不了。代价是 best 经常"很久不更新" —— 那是设计内的 noop,不是 bug。
- 责任分清:DDP 的 rank0 负责写 best、评估侧只读,写文件走原子替换,避免多进程互相踩。
评估工具梯队:5 层从滑窗到真人对打
"度量准"光靠胜率统计还不够。这套训练真正在用的是一摞 5 层评估工具,从训练侧滑窗一路上探到 Unity 里真人 vs RL 对打 —— 越往上越接近真实部署、越慢、覆盖面越广,但每一层都独立可跑、互相印证。前几章零散提过的 winrate、双轨、replay、Unity eval 全部都是它的一部分。
每一层评估工具到底在解决什么 —— 5 个互不替代的视角
| 层 | 真实工具 / 命令 | 解决的问题(单一视角看不到的盲区) |
|---|---|---|
| L1 训练滑窗 | 训练进程内的 winrate tracker,每 3 秒输出 winrate.jsonl(总体 / by_deck / by_opponent_type 三个独立窗口)+ Wilson 95% CI 下界 + 平局算非赢的保守口径 |
"在线读数":训练正在变好还是变差,秒级感知。但只反映"打过的最近 1000 局",对手是 curriculum 配出来的混合分布,不代表部署口径。 |
| L2 双轨 eval 专机 | cluster_ops.py eval {deploy/run/status/fetch} 在一台独立专机上 cycle 级跑,配置由 eval-isolated.yaml 锚定。轨道 ① 跑公平 4 baseline 进 best;轨道 ② 跑满血看牌 Expert 对齐线上人类锚点。每 deck 切换时 install_eval_isolated.py 全量重部署 self_deck(J6 教训) |
"独立干净的标尺":和训练完全旁路、用钉死的对手池,避免训练侧 winrate 因 curriculum 分布漂移而失真。两条轨道分别给"训练监控"和"对齐人类水平",互不污染。 |
| L3 大规模 meta 池 | cluster_ops.py eval-adhoc {deploy/run/status/fetch/crashes},在 eval 专机的独立目录跑自定义池(如 opponent_decks_top3000.txt,覆盖 99.4% 线上对局),复用 eval-isolated 的 dotnet runtime 但不碰 eval-loop |
"泛化稳健性":L2 用的是 top100 对手池(聚焦头部),L3 把它扩到 top3000(覆盖整个线上分布)。两者各档差 0–3pp 且单调 = 模型对头部 meta 略优、无过拟合 —— 这是单跑 L2 看不到的结论。 |
| L4 Replay 行为审查 | C# 入口 dotnet ... replay --model ... --baseline greedy_expert_fair --output trace.json --seed 42,部署脚本 _run_replay_vs_zoo.py 一键推 master DLL + 跑 + 拉 trace,渲染脚本 _render_replay_trace.py 输出中文回放(每回合出牌 + 双方 power + 收官 + 决策 warning) |
"行为级真相":聚合胜率涨可能掩盖具体动作退化。同 seed 在新旧 ckpt 各跑一遍 → 因 seed 决定全部随机性,决策分歧 100% 归因模型演化,比聚合胜率早得多给出"训练有没有真改对"的判决(穿墙翻盘那次"100 局只出 1 张"的铁证就在这里抓到)。 |
| L5 Unity 真人 vs RL | 本机 dotnet rl-server 子命令(Kestrel HTTP 监 0.0.0.0:8001,加载 ONNX),Unity 端 RemoteRLAgent(实现 IPlayerAiAgentOverride)每回合 POST /v2/decide;菜单 AITools/Run AI Game (RL) 一键拉起;每决策落 decisions.jsonl,每局落 games/<id>.json。SD AI override hook 在 Player.cs 的 NetEase-Code 块里,训练时 override=null 完全无影响 |
"主观体感":聚合数字 + 行为 trace 都过了,仍要人亲手打几局看"AI 真的会下吗、有没有奇怪习惯、卡哪个 archetype"。这是聚合数字永远代替不了的 ground truth。后续每次模型升级都拉来对抗,留 jsonl 给 AI 后期分析。 |
L1–L5 的关系不是冗余 —— 而是"任意一层都能戳穿前一层的盲区":L1 给的"训练在变好"会被 L2 旁路独立测;L2 给的"对公平 Expert 76%"会被 L3 在 top3000 上验证;L3 给的"泛化好"会被 L4 用 replay 看具体决策;L4 给的"决策合理"会被 L5 让真人亲自下场感受。越往上越慢、越广、越主观,但每一层都独立可跑且互不替代。一个 best.onnx 要骗过所有 5 层才会被相信 —— 这是"度量准"在工具层面的真正落地。
CI 下界防运气、棘轮 + 双闸门防噪声促销、双轨评估(公平锚进 best、满血 Expert 只对齐人类)防口径漂移、版本闸门(第 11、13 章,只收最近 100 个模型版本产的数据)防陈旧数据。一个数字要骗过这四层才会被相信 —— 这正是第一部分"先把尺子量准、再谈优化"的工程兑现。
回看这七章会发现两条主线交织。一是朴素往往赢过聪明:数据通道从反向拉退回单向出站、mirror 从 nginx 零拷贝退回普通流式响应、部署从蓝绿退回简单 restart、吞吐优化整组被删回基线 —— 分布式系统里,正确性、可运维性、可观测性的权重往往高于某个局部的性能最优。二是可靠与可信是地基:断点续训让长跑随时停、无损接,冻结快照防超参漂移,那一整套"宁可低估"的度量防线把"数字能不能信"做实。能把数据稳定、新鲜、不丢地喂给 GPU、并让每个胜率数字都经得起推敲,比把某一段做到极致快重要得多 —— 这和第一部分"先把尺子量准再优化"是同一个道理。
驾驭 Claude:人在回路的决策与纠偏
前两部分讲"做了什么"。这一部分讲"是谁、在哪些节点、把方向掰对的"。我们翻遍了项目全过程、2858 条人类发言的原始对话记录 —— 高频出现的不是技术术语,而是几句很朴素的话:"搞复杂了""感觉不对劲""你别走歪方向了"。下面是中肯的复盘:AI 容易在哪儿栽,人类的判断在哪些节点救了场,以及在 AI coding 时代,人类的价值到底在哪。(下文引用均为对话原话,仅清理明显笔误。)
Loop Engineering:把"一次性动作"全部嵌进闭环
AI Coding 时代真正的核心价值,不在 AI 写代码多快,而在系统能不能把每一个"一次性动作"都嵌进闭环:动作 → 信号 → 反馈 → 下一个动作。系统里没有 fire-and-forget —— 任何输出在它落地前都被另一个 loop 盯着。这套训练里实际跑了 6 层嵌套 loop,从毫秒级的推理 step、到周级的踩坑沉淀;内层失败上抛触发外层介入,外层把教训反向沉淀回内层规则。下面这章不是"流程清单",是把这 6 层 loop 摊开看它们怎么互相托底。
6 层 loop 全景:内层快、外层慢,全部具名到代码事实
每一层 loop 真正在做什么、闭环靠什么"咬住"
"loop"这个词容易听成"循环",本质是控制论里的闭环反馈:每个动作必须把结果信号送回某个决策点,否则就是开环。下面这张表,每一行回答两个问题 ——(1)这层 loop 的"反馈信号"是什么、被谁消费;(2)这层失败时怎么把警报抬到外层,而不是把错误闷在自己里。
| 层 | 反馈信号 → 决策点(闭环咬合点) | 失败时怎么上抛 / 外层怎么下放 |
|---|---|---|
| L0 推理 step-wise(毫秒) | 每出 1 张牌 → 落游戏引擎 → 引擎刷新出新局面 → 喂下一步决策。反馈信号 = "牌已经在桌上的真实局面",决策点 = 注意力头本身;引擎是零误差的"仿真器",把"网络脑补 dynamics"这条开环直接砍掉。 | 合法性 / 终止条件由引擎给出(地点满、无合法动作、触顶 8 步) → 自动退出回合,不能"无限自我循环"。每回合最多 8 步是这个 loop 的硬围栏。 |
| L1 数据流 worker-pull(秒) | 副机 GET /model/latest(etag 增量 + sha 校验)→ 跑对局 → POST /trajectory(带模型版本号)→ 主机 PPO 入队消费 → 导出新 onnx → 副机下次拉。反馈信号 = etag 变更 + 版本号匹配,决策点 = supervisor 蓝绿换代、版本闸门 drop 旧轨迹。 |
心跳 30 秒丢失 → 主机自动摘机;版本号滑出"最近 100 个"窗口 → 自动 drop(防 off-policy 漂移污染 PPO);任一不达标,警报抬到 L2 看板的 drop_rate 曲线。 |
| L2 监控 winrate + best(秒-分钟) | 每 3 秒滑窗 1000 局算 Wilson CI 下界 → 喂 dashboard + best ratchet 比对。反馈信号 = CI 下界数列 + 阈值跨越事件,决策点 = best ratchet 决定要不要"棘轮+1"。 | best 长时间不更新 / 大盘 CI 下界连跌 → 在看板触发数字化 SLA 告警,抬到 L3 让人介入跑 replay;best 锁住峰值 = 给 L4 提供"回滚到这里"的安全锚点。 |
| L3 行为 eval(分钟-小时) | eval 专机周期性拉 best.onnx → 跑双轨(公平锚 + 满血人类对齐)→ 写 eval.jsonl。反馈信号 = 同 seed 在新旧 ckpt 的决策 diff、argmax 与 sampling 双口径差,决策点 = 是否要"行为级否决一个看似涨的 best"。 |
聚合胜率涨而 replay 显示行为退化 / 两个口径差异异常大 → 直接否决 L2 的"看似变强"结论,把根因排查上抛到 L4,挂"等查清楚再继续训"标记。 |
| L4 工程开发 OpenSpec(天) | 四阶段 /opsx:explore → :propose → :apply → :archive。反馈信号 = 闸门产物:proposal.md / design.md / tasks.md 由人类审过 → CodeGraph find_callers(depth=2) 闭环 caller → train_launch.py 4 条硬性后置(GPU ≤ 100MiB、worker==0、新 PID 活、60s 内 jsonl ≥ 1 行)→ 通过才允许进 :archive。 |
任一闸门不过 → 非零退出,绝不强推下游;CLEANUP_CHECKLIST 4 项完成定义守"半完成 caller + 标记完成"。归档时把本次踩坑沉淀回 CLAUDE.md 硬约束或新 ADR,下放给 L5。 |
| L5 沉淀记忆(周-阶段) | 每个阶段 phaseN/JOURNAL.md append-only 流水 + 架构级决议进 DECISIONS.md(ADR 编号)+ 项目级硬约束写进 CLAUDE.md。反馈信号 = "下个 session 接手时能不能在 1 分钟内知道前任栽在哪",决策点 = 新 session 启动时强制阅读 CLAUDE.md。 |
这是闭环的"记忆段":把 L0–L4 踩过的具体坑(如"--new-run 未授权丢进度"、"yaml 脱钩"、"漏改 callers 致 TypeError")变成下一轮默认守的规则 —— L0–L4 的失败永远不会"无人接住"。 |
三个跨层贯穿规则:让"层"不只是层、真正合上
--confirm-cold-start 授权门。外层学到的,自动变成内层下次的拦路虎。--confirm-cold-start);所有跨层路线分歧必须等人类显式选项回答才落地(AskUserQuestion);人类的"感觉不对劲"作为独立校验信号注入任一层,与 AI 自己的结论冲突时,AI 必须先怀疑自己的采样方法。下一节把人类反馈在 6 层 loop 里走的所有真实通道列出来。人类反馈的 6 条工程化通道:每条都有真命令、真触发、真留痕
很多团队把"人类反馈"做成"人盯着看"—— 那是把人当人肉 watchdog,浪费且不可靠。这套系统把人类反馈做成项目里和 PPO 一样的一等公民信号:6 条通道分别绑到不同的 loop 层 + 不同的频率 + 不同的留痕文件。下面是它们的总览:
| 通道 | 方向 / 触发场景 | 具体命令 / 留痕 | 绑到哪一层 loop |
|---|---|---|---|
| ① 握手门 handshake gate |
AI → 人类。任何不可逆操作(cold start 丢进度、删数据、push 主分支)前 AI 必须停手,等人类明确说"重新跑 / 新 run / 删 / 推" | cluster_ops.py master --confirm-cold-start trainer cold-start;说"restart / resume"一律默认 hot resume;漏了 → 启动器直接非零退出。事故规则编号留在 JOURNAL(J78 / J80) |
L4 工程开发 + 跨 L1/L2 的进度保护 |
| ② 选项问询 AskUserQuestion |
AI → 人类。AI 对方向 / 配置 / 路线不能在两个看似合理选项里自己拍板,必须吐出 ≤4 个选项卡片让人类点选 | harness 内置 AskUserQuestion 工具(选项需互斥、无 Other);不允许"猜一个先做";用户可"Other"补充。留痕在会话本身 |
L4 工程开发 + 触发 L5 的方向决策 |
| ③ 方案审查 proposal review |
AI → 人类。每次重大改动前先出方案(不是先写代码),由人类审过 design + tasks 才允许进实施 | /opsx:propose 必产 proposal.md(动机/边界)+ design.md(含 blast radius)+ tasks.md(逐项可勾);归档前 openspec validate。所有 artifacts 入 openspec/changes/ 进 git |
L4 工程开发的入口闸 |
| ④ 体感警报 "感觉不对劲" |
人类 → AI。人类对数字有 AI 没有的体感,"这个值感觉不对劲,太小了"出现过很多次,每次几乎都指向真问题 | 规则上:AI 必须把"用户的体感数字"视作独立校验信号,与实测矛盾时优先怀疑自己的采样方法,不是反驳人类。AI 自己把这条记进了 JOURNAL | 跨层注入到 L2 / L3 —— 旁路所有自动度量 |
| ⑤ 一句话扳回 course correction |
人类 → AI。AI 走偏时人类用一句领域常识或一句质问直接掉头(详见第 21 章 19 条原话) | 典型如 "游戏规则本来就可以出多个牌"(穿墙翻盘)、"你别折腾了,胜率下降是崩溃前兆"(叫停长跑)、"argmax 才 36% 数据不对劲"(牵出虚高)。AI 必须当场停手切方向,不允许"先把当前路跑完再说" | 横跨 L0-L4,外层兜底 AI 的所有走偏 |
| ⑥ 复盘归档 retrospective |
双向。AI 把每次决议 / 实测 / 被推翻假设 append 到 JOURNAL;人类把架构级判断写进 DECISIONS(ADR);项目级硬约束沉淀进 CLAUDE.md | phaseN/JOURNAL.md(流水)+ DECISIONS.md(ADR 编号)+ CLAUDE.md(下个 session 强制读到)。AI 还要把自己的判断失误诚实记进去:"单次崩盘归因置信度天然低"这种自省也写 |
L5 沉淀记忆的全部来源 |
① / ② / ③ 三条是 AI 主动找人(握手 / 选项 / 方案),管的是不可逆 / 不确定 / 重大改动三种风险;④ / ⑤ 两条是 人类主动注入(体感 / 一句话),管的是 AI 自欺 + 走偏两种盲区;⑥ 是双向沉淀,让今天的反馈变成明天的默认规则。AI 不能"哪条都不要、自己全做";人类也不能"哪条都没有、全靠盯"。每条通道都有具体绑定的命令、触发条件和留痕文件,不靠自觉 —— 这是反馈被工程化、不再是口号的标志。
"全靠人盯"等于把所有 loop 压成一层,把人变成串行瓶颈。Loop engineering 的本意是:越内层越自动化、越外层越只剩判断。等到事情真的需要人介入时(L3 行为退化 / L4 大改方向),上层已经被 L0–L2 那些自动闭环过滤掉了 99% 噪声,人类看到的是浓缩过的"必须拍板",不是"陪跑数据流"。
一个完整案例:穿墙翻盘是怎么走完 6 层的
第一部分那次穿墙翻盘,是这 6 层 loop 一次完整 round-trip 的活样本 —— 内层 loop 先压住数据流不崩,外层 loop 把"游戏本来能出多张牌"这句怀疑变成对动作建模的根因修复,最后把教训沉淀回 CLAUDE.md。
很多 AI Coding 项目失败,不是 AI 笨,是充满了开环动作:代码改完没人查 caller、训练起来没人验后置、胜率涨了没人查行为、踩了坑下次又踩。Loop engineering 的真正功夫是:把每一个"做完就忘"的动作找出来,给它接一根回路上去 —— 接到内层(合法性掩码)、接到外层(best ratchet / 双轨 eval / 启动器后置)、接到记忆(JOURNAL / ADR / CLAUDE.md)。等所有动作都嵌进闭环,AI Coding 就从"快但不放心"变成"快且可审计"。这才是 AI Coding 时代真正能落地的样子。
"搞复杂了":把 AI 一次次拉回简单
整个项目里,AI 反复表现出同一种倾向 —— 把事情做复杂:加一堆特殊逻辑、搭多套机制、追求"完备"。而人类几乎每隔一阵就得把它拉回来一次。最值得记下的是:项目里几个最终采用的核心架构,恰恰是这样"拉简单"拉出来的。
→ 这一段话几乎就是第二部分那套 worker-pull(副机单向出站、心跳自注册摘机)的设计说明。AI 当时在往更复杂的双向方案走,人类一段话定了方向。
→ AI 本来用 override hook + 藏牌一堆特殊逻辑,被拉回"做成一等公民 AI 类型",hack 全删(第 5 章那个公平对手就是这么来的)。
→ AI 一度想搞"自回归、出一张再推理一张"的复杂结构,人类点破"就是一次输出动作数组"。
→ 第 10 章那个 ⌊核数 × 90%⌋,是人类纠正出来的,不是 AI 拍的固定值。
AI 的本能是"完备、通用、不遗漏",于是容易过度工程化 —— 多搭一层抽象、多处理一个边界、多留一个开关。人类那句"搞复杂了"也不是图省事,而是抓住了"这个场景到底需要什么"。结果被反复验证:被拉简单的方案,往往更对、更稳、更好维护。这几乎是整个项目工程侧的一条隐形主线。
看见 AI 看不见的:常识、直觉与怀疑
AI 的视角是代码和数据。它常常看不见三样东西:这游戏到底怎么玩、这个数字凭直觉对不对、这个结果是不是"好得不正常"。而项目里几次最关键的转折,恰恰是人类用这三样东西换来的。
① 领域常识:一句话点破那堵反复撞的墙
困扰 phase1–4 的"五成墙",AI 和复盘都一度归因到"任务太难 / 对手作弊"。真正点破它的,是一句游戏常识 ——
"那必然的啊,游戏规则本来就可以出多个牌的。" 紧接着: "你在本机 eval replay 跑 100 局,看看是否真的最多只出一张牌。"
AI 把"每回合只出一张牌"当成既定事实分析了很久,却没意识到这违反游戏规则。人类一眼看穿,并立刻要求用 100 局实测验证 —— 真因(动作建模把"出牌"和"结束回合"绑死了)就此浮出水面,模型随后单调穿墙。这是整个项目最大的一次翻盘,靠的不是算法,是常识加实证。
② 直觉警报:"感觉不对劲"
人类对数字有一种 AI 没有的体感。项目里"感觉不对劲"这句话出现过很多次,每次几乎都指向一个真问题:
AI 倾向于相信自己刚算出来的数字、往前推进;人类的体感是一道独立校验 —— 两者冲突时,事后往往是体感对。AI 自己也把这条记进了日志:"用户的体感数字是有效信号,与实测矛盾时优先怀疑自己的采样方法。"
③ 科学怀疑:对"太好的结果"保持警惕
最难得的是对好消息的不轻信。当模型胜率突然变得很漂亮时,是人类先按住了刹车 ——
"这个难以置信,训练白费了;所以我觉得你应该慎重诊断,给出实际证据,包括 eval 和训练的情况,你可以在副集群拿一台机器来打几场,看出的卡牌、动作是否真有问题。"
正是这种"好得不正常就先查清楚"的怀疑,逼出了对数据是否虚高的彻底溯源(最终证明数据真实、提升真实)—— 这恰好是第一部分那个"在失真度量上反复盲调"教训的反面:这一次,没有再让一个可疑的数字带着项目跑。
AI 强在"把给定的问题算到底",弱在"判断这个问题、这个结果本身对不对"。领域常识、对数字的直觉、对异常结果的警惕 —— 这些不在代码和数据里,是 AI 的盲区,也正是人类不可替代的地方。
不只是纠错,更是主导
前两章讲人类怎么"纠 AI 的错"。但翻完整段对话记录会发现,人类做的远不止纠错 —— 更多时候是在主导:每一次重大改动前先审方案、控成本、定目标、盯根治、逼 AI 审查自己的代码。这才是 AI 时代项目主导者的真实工作方式。
① "先出方案我审查"
几乎每一次重大改动前,人类都先把住一道闸 —— 不准 AI 直接上手,先出方案:
AI 又快又能改,最危险的恰恰是"擅自动手、一改一大片"。人类用"先出方案"这道程序闸,守住了每一次重大改动的方向 —— 动手之前先讲清楚要改什么、为什么、影响多大。
② 控成本、定目标
人类始终有清晰的成本意识和量化标准,不让 AI 无止境堆规模:
"我在想阶段二有必要 5 亿场么,1 亿场不行么 —— 卡组大部分还是类似的,场次足够多,泛化能力也强。"
"训练的 expert 达到 70% 胜率、打全卡组胜率能 55% 就足够了。"
"够了"是一个 AI 很难自己拍板的判断 —— AI 的本能是"更多、更全、更稳"。人类清楚这个项目到底要什么、到什么程度就该停,省下的是真金白银的算力和时间。
③ 盯根治,不留烂尾
AI 改完容易留下残留(旧配置、死代码、半完成的清理),人类盯得很紧:
"能跑就行"和"清干净了"之间的差距,往往就是下一个雷。人类要的不是"问题暂时不犯了",而是"根上没了"。
④ 逼 AI 审查自己
最关键的一道把关,是人类要求 AI 回头审查自己的代码和假设,而不只是看"跑通了没有":
"你审查下 RL 的环境感知,输入参数是否都在工作、正常工作,有没有出现阶段三的问题。"
"你要审核下代码本身是否支持、留的余量是否足够。"
第一部分那个"救下 1 亿场训练"的观测验证,源头就是这类要求 —— 人类不满足于"模型在训练",而是逼 AI 去核实"它到底看到了什么、是不是和我们以为的一样"。让 AI 审查自己,是发现地基问题最有效的一招。
这些加起来,是"项目主导"—— 人类没有在执行层和 AI 拼速度(拼不过、也没必要),而是站在决策层:审方案、控成本、定目标、盯质量、逼复查。AI 负责把决策高效落地,人类负责保证"决策本身是对的、落地是干净的"。
关键转折:AI 走偏,人类一句话扳回
前几章分类讲了人类怎么纠偏。但更直观的,是把这些"AI 走偏 → 人类一句话扳回"的瞬间排在一起 —— 你会发现,项目几乎每一个关键转折点,都卡在这么一句话上。下面整列都是对话原话。
| AI 当时的判断(看着都合理) | 人类一句话(原话) | 扳回了什么 |
|---|---|---|
| 副机模型不更新,AI 在各种折腾 | "肯定是有其他 bug 啊,你别瞎整了行不行 …… 文件都不在了,你怎么还在用 etag?" | 一句点破 etag 的真 bug |
| 性能不达标,AI 想加机器、折腾别的 | "GPU 现在没吃满在浪费 …… 你别走歪方向了" | 钉回"先剖析 trainer、提吞吐" |
| 胜率下跌,AI 归因"overtrain / PPO 崩了" | "PPO 指标都正常,怎么会 overtrain?" / "我看胜率还在上涨,PPO 在崩溃?" | 自相矛盾的归因被推翻 |
| AI 读代码、给推断式结论 | "你刚刚是读代码来确定,我要的是实际跑起来、加日志、看实际情况" | 实测才抓到真问题 |
| AI 拿聚合胜率判"行不行" | "拿胜率来评估 ok 不 ok,是颠倒黑白 —— 要看实际跑 2 局的详细日志" | 纠正整套评估方法论 |
| AI 顾虑 fog 会影响"可训性" | "PPO + transformer 本来就支持非全局信息训练,不然都作弊了,那还是强化学习么?" | 把 POMDP / fog 钉成不可动的地基 |
| 长期跟脚本贪心对手磨胜率 | "greedy 是穷举,没 combo、没反制,一直和它磨上限有限" | 点明脚本上限,指向 self-play |
| eval 用 argmax 反而更低,AI 没在意 | "argmax 模式怎么才 36%?数据不对劲" | 牵出训练端度量虚高 |
| AI 判模型"lane 病、过度集中" | "这套牌是删除流,玩法上就该先堆一路打 combo" | 收回误判,承认要懂卡组 |
| AI 给一套自圆其说的论证 | "你别想着糊弄我,这是核心逻辑问题" | 升级为设计级矛盾,重审 gate |
| 阶段四失败,AI 归因到表面参数 | "经历过那么多次调参,我并不认为是这么表面的问题" | 逼出三层根因的彻底复盘 |
| 清完残留,准备直接 cold start 重训 | "先全部停止、清空,然后跟我说;等我说『重新开始 cold start』,你才开始" | 立下授权门,挡住擅自重训丢光进度 |
| 胜率下滑,AI 还在长跑里继续调参 | "你别折腾了,胜率开始下降,这是崩溃前兆" | 当场叫停 + 回滚到 best |
| 为把看板"1000 局"显示成"3000 局"去改 trainer | "只是 web 看板统计,你怎么改了那么多东西?train 你不用改" | 展示归展示,禁止为看板动训练核心 |
| AI 又偷偷另写了一个 yaml | "不是都该统一在一个 yaml 么?你单独写了一个?" | 揪出配置脱钩,统一单一真源 |
| 用整体大盘胜率来挑 best 模型 | "best 不该跟大盘胜率挂钩,应该每个难度的胜率加权" | 纠成 Expert 加权指标(第 6 章那把尺子) |
| 把 cube count 掺进奖励信号 | "核心目标是胜率,cube count 和胜率没关系,去掉 cube" | 奖励钉死纯胜率导向,去掉噪声 |
| AI 自称没改动,却出现不兼容 | "你确定你没改?不然为啥会不兼容?" | 一句反问逼它回查自己的改动 |
| 继续在 random / 弱对手上堆训练量 | "部署用的是 argmax,继续大量喂 random 会浪费模型能力,该投到 Expert" | 从部署口径反推,把算力投给核心强对手 |
每一次 AI 的判断,当时都看着合理 —— PPO 指标确实正常、代码逻辑确实通、数字确实漂亮。但人类有 AI 当时没有的东西:对历史基线的记忆("阶段一到四都 500 g/s 跑得很稳")、对"合理却反常"的警觉("胜率涨着 PPO 怎么会崩")、对"读代码 ≠ 真相"的坚持("我要的是实际跑起来加日志")。几乎每一个关键转折,都卡在人类这么一句话上 —— 这就是"人在回路"四个字最实在的含义。
中肯的分工:谁该做什么
讲了这么多人类的纠偏,必须同样诚实地说清楚:这个项目里大量的硬活是 AI 干的,而且干得不错。把功劳算清楚,才谈得上"分工"。
AI 真实做的事(不缩水)
- 全栈代码实现:跨 C# / Python / ONNX 的观测编码、公平对手、两套多动作架构、跨语言轨迹契约 —— 一次改动常牵动十几个文件,AI 把链路打通、测试写全。
- 实验与诊断:py-spy 性能剖析定位 rank0 瓶颈、上百局 replay 行为评估、百万级采样验证分布;几个关键根因(观测是完全信息、单动作残废、GIL 饱和)的挖掘动作本身,都是 AI 执行的。
- 分布式基础设施:几十台训练机器的环境部署和操作、模型分发独立进程、断点续训与回滚 SOP,AI 一遍遍跑、一遍遍Loop验证。
- 诚实留痕:AI 还把自己的判断失误写进日志("单次崩盘归因置信度天然低""单测只验挂载会漏行为 bug"),让协作可审计。
换句话说:人类的怀疑和常识指明方向,AI 把验证做到底、把铁证拿出来。"游戏本来能出多张牌"是人类说的,但"读三处源码 + 跑 100 局 replay + 抓到对手末回合 +228 翻盘的铁证"是 AI 做的。这两件事缺一不可。
那条边界在哪
| 放心交给 AI | 必须人类把关 |
|---|---|
| 写代码、改链路、补测试 | 定方向、定第一性原则 |
| 跑实验、查代码、维护基建 | 质疑数据、对"太好的结果"保持警惕 |
| 规模化分析、机械执行 | 领域常识(这游戏到底怎么玩) |
| 把给定的问题算到底 | 判断这个问题 / 结果本身对不对 |
| 诚实记录、复盘留痕 | 坚持简单、不被局部最优困住 |
在 AI coding 时代,人类的价值已经不在敲代码 —— AI 敲得又快又多。人类的价值在于判断 AI 算得对不对、方向走得偏不偏:一句"搞复杂了"省下一堆过度设计,一句"游戏本来能出多张牌"掀翻一堵反复撞的墙,一句"难以置信,给我证据"挡住一个可疑数字带偏整个项目。最好的协作,既不是 AI 取代人、也不是人不信 AI,而是 —— AI 把活干透,人在关键节点把判断按住。这套最终打到超过中高段位人类的牌局 AI,就是这么一起做出来的。
线上答卷:从玩家的不满,到真人对局的验证
前面三部分讲了怎么把它训出来、怎么让它稳稳跑起来、怎么和 AI 协作纠偏。最后回到最实际的一个问题 —— 玩家买账吗?先看玩家对上一代人机的真实不满,再看这套 RL AI 上线后、在真人对局里交出的成绩:两者正好接成一个闭环。
① 起点:玩家对上一代人机的不满
这个项目不是凭空立项的。上一代人机是脚本 AI,重度玩家对它的满意度长期偏低、明显低于游戏大盘的预期。抱怨集中在两点,而且看似矛盾:既嫌它套路机械、不像真人,又觉得它关键回合精准得不真实、像开挂。
"精准反制、像开挂"的技术根因,是旧脚本 AI 会偷看你本回合还没翻开的牌(第 5 章那个"看牌型 AfterOpponent"对手);"套路机械、不会 combo"的根因更朴素 —— 这个游戏牌多、组合爆炸,靠人手写规则去暴力穷举,根本枚举不过来,脚本 AI 只能停在单步贪心、零前瞻。也就是说:文中反复强调的"公平性 > 强度"并不是技术洁癖 —— 它正对着玩家的头号负面体验。新方案用战争迷雾(从源头不偷看)+ Transformer + PPO(从海量自对弈里自己长出策略、不再靠人枚举),恰好把这两点从根上拆掉。
② 结果:21 套上线打真人,超人类结论再次成立
前面几部分讲的是"训一套"的方法论。而这套配方被原样复用了 21 次 —— 每个卡组只改极少的配置、其余一字不动 —— 训出 21 套单卡组专精模型,全部上线、和真人对战(线上随机分配一套当对手)。这 21 套模型累计跑了约 5.19 亿局自对弈才练成 —— 相当于一个人不眠不休、每局三分钟地打上近 3000 年;同样这些对局,训练集群约 10 天就喂完了,快约 10.8 万倍。本事不是凭空来的。
线上真人对局中的表现整体压过真人各档:整体大盘 70.6%、对 Lv50-99 高段真人 71.8%、对最强的 Lv100+ 顶尖真人仍 66.2%。21 套逐一来看,每一套的线上胜率都稳稳在五成之上 —— 最高 77.6%、最弱 60.9%,无一跌破。规模化不是单卡组的侥幸。
全部上线
真人对局
Lv100+ 真人
最强脚本当对手
这算"超人类"吗?第 8 章给过一个"超人类"的证据:在和人类完全相同的信息劣势下(自己看不到对手、对手偷看自己),模型对满血强对手 61.6%,比中高段位人类的 ~49.8% 高约 12 个百分点 —— 那是实验室同台的口径。线上这组数据给了第二个、完全独立的口径 —— 换个角度看:"都当对手,谁把这群真人压得更狠":
人类打最强的脚本 Expert,从中高段到无限段的胜率基本持平在 ~49.8%(rank50-99 约 49.9% / Lv100+ 约 49.8%)—— 反过来说,这个"会偷看你出牌"的脚本强对手,作为对手把人类各段都只压到 ~50%,就是它的棋力天花板。而我们的 RL AI 反过来当对手去打真人,公平不偷看,整体把真人压到 70.6%、对最强的 Lv100+ 仍 66.2%。换句话说:RL 压制顶尖真人的能力,已经明显强过"最强脚本 Expert 压制同群真人"的能力 —— 超人类的结论,这次是在真实对局里、用另一把尺子再次成立。
那天梯 TOP100 呢?——面对排行榜前一百的玩家,AI 仍合计小胜
"Lv100+"还是个偏宽的段位口径。再往上,面对真正站在天梯排行榜 TOP100 的顶尖玩家,AI 打得过吗?这些人在天梯匹配里几乎不会被送去打 AI,但训练模式是他们主动进来的 —— 从这部分线上真实对局里看,AI 面对 TOP100 玩家平均胜率约 56%:不是碾压、也有被赢下的,但互有胜负、合计仍站在五成之上。把三个口径连起来看,是一条干净的单调线 —— 对手越强、AI 优势收得越窄,但直到天梯 TOP100 都没被压下五成:
| 对手(越往下越强) | AI 平均胜率 |
|---|---|
| 线上真人整体大盘 | 70.6% |
| 无限段 Lv100+(宽口径) | 66.2% |
| 天梯 TOP100 玩家(最窄口径) | ~56% |
TOP100 这档是排行榜前一百玩家的合并统计,样本较小、只读"互有胜负、合计小胜"的方向,不抠小数。
把首尾接起来看:旧脚本 bot 因游戏牌多、组合爆炸,人手写规则和穷举都覆盖不过来,只能停在及格线(胜率约 49.8%、玩家满意度长期偏低)→ 换成 Transformer + PPO,让模型在海量自对弈里自己长出策略、再用战争迷雾守住"不偷看"的公平 → 线上打出真人 70%、压过顶尖玩家。这套 AI 不只是更强,它更是从设计上把"更强、更公平、更像真人"一起交付:它不偷看,所以赢得干净;它的打法是从海量对弈里自学出来的、不是人写死的规则,所以不像老脚本那样套路。强度是结果,公平和拟人,才是它真正交出的答卷。
③ 玩家怎么说
数字之外,更有分量的是玩家自己的话。训练模式外放后(约两天窗口),玩家在社媒和交流群里自发说出的反馈,恰好逐条戳中它最想证明的东西。这是外部定性的早期口碑、不是我们自测的硬指标,权作佐证:
口径与诚实标注:① 线上胜率为真人对局的合并统计,只读梯队、不抠小数;② "打脚本的训练强度"与"打真人的线上胜率"是两个独立维度、排序并不一致,不可互相换算;③ 人类锚点取自同口径的大规模线上真实对局(rank≥50 vs 满血看牌 Expert,各段扁平在 ~49.8%);天梯匹配机制下顶尖玩家基本不会被送去打 AI,但训练模式是玩家主动进入的,所以能拿到天梯 TOP100 玩家的实测对局;④ TOP100 那档是排行榜前一百玩家的合并统计,样本较小、只读"互有胜负、合计小胜"的方向,不抠小数;⑤ 玩家反馈来自外放约两天窗口的社媒/交流群自发讨论,属外部定性早期信号,只印证方向,不当硬指标。