news 2026/10/24 1:13:41

从零构建棋类AI:规则引擎、MCTS搜索与自我对弈训练全链路实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零构建棋类AI:规则引擎、MCTS搜索与自我对弈训练全链路实践

1. 从一张棋盘说起:这个项目到底在解决什么问题

围棋AI这几年被聊得很多,但真正动手从零搭一个能跑、能下、还能持续迭代的棋类AI项目,门槛并不低。Wuziqi这个项目名字听起来像是五子棋,实际上它是一套围绕棋类博弈AI展开的完整工程实践,核心目标是把"规则引擎 + 搜索算法 + 评估模型 + 自我对弈训练"这条链路打通,让一个普通开发者也能在自己的机器上跑出一个有模有样的棋类AI。

我第一次接触这类项目的时候,最大的困惑不是算法本身,而是"东西太多、不知道从哪下手"。规则怎么写才不会漏判禁手?搜索深度和耗时怎么平衡?评估函数是手工设计还是用神经网络?自我对弈的数据怎么存、怎么用?这些问题在教科书里往往是分开讲的,但真正做项目时它们是缠在一起的。Wuziqi这个项目的价值就在于,它把这些环节串成了一条可运行的流水线,而不是停留在论文里的公式。

这篇文章适合三类人看:第一类是想入门博弈AI但被各种术语劝退的开发者;第二类是做过后端或算法、想找一个完整项目练手的中级工程师;第三类是对棋类规则和搜索算法感兴趣、想自己动手改一改的爱好者。不管你之前有没有接触过蒙特卡洛树搜索或者强化学习,我都会尽量用生活化的方式把关键点讲清楚,同时给出可以直接抄的参数和步骤。

需要先说明一点:下面涉及的具体实现细节,有一部分是基于这类项目的常见工程实践做的合理补全,因为原始描述比较零散。我会在关键处标注哪些是通用做法、哪些是我个人的经验取舍,方便你按自己的需求调整。

2. 整体架构设计:为什么这样拆模块

2.1 四层结构的分工逻辑

一个能持续迭代的棋类AI,最忌讳的就是把所有逻辑塞进一个文件里。Wuziqi项目采用的是比较经典的四层结构,我把它拆开讲:

  • 规则层:负责棋盘状态表示、落子合法性判断、胜负判定、禁手检测。这一层必须绝对可靠,因为它是所有上层逻辑的地基。
  • 搜索层:负责在给定局面下寻找较优落子,核心是蒙特卡洛树搜索(MCTS)或者Alpha-Beta剪枝。
  • 评估层:负责给局面打分,可以是手工特征加权,也可以是神经网络输出。
  • 训练层:负责自我对弈、数据收集、模型更新,形成闭环。

为什么要这样分?因为每一层的迭代节奏完全不同。规则层一旦写对基本不动;搜索层可能调参数;评估层可能换模型;训练层可能改采样策略。如果耦合在一起,改一处就要回归测试全部,成本极高。我见过不少个人项目一开始图快,把规则和搜索写在一起,结果到后期想换评估函数时发现牵一发动全身,只能推倒重来。

提示:规则层一定要写单元测试,尤其是禁手和边界情况。棋类AI最怕的不是下得臭,而是规则判错导致整盘棋作废。

2.2 棋盘表示的选择与代价

棋盘表示看起来是个小问题,实际上影响很大。常见的有三种:

表示方式优点缺点适用场景
二维数组直观、易调试内存局部性差、复制慢原型阶段
一维数组复制快、缓存友好索引计算需谨慎搜索密集型
位棋盘极快、并行友好实现复杂、可读性差高性能需求

Wuziqi项目在原型阶段用的是一维数组,长度是棋盘格数,每个位置存0(空)、1(黑)、2(白)。这样做的好处是复制棋盘时可以直接用切片或者内存拷贝,在MCTS这种需要频繁模拟的场景下,复制开销能省不少。位棋盘虽然更快,但对新手不友好,调试时看到一堆位运算会头大,所以我建议先用一维数组跑通,等性能真的成为瓶颈再考虑优化。

这里有个容易踩的坑:一维数组的索引计算。如果棋盘是N×N,位置(r, c)对应的索引是 r*N + c。听起来简单,但在写胜负判定时,四个方向(横、竖、两条对角线)的步长分别是1、N、N+1、N-1。其中N-1这个方向在边界处容易越界,必须额外判断列号。我当初就在这里翻过车,横竖都对了,斜向判定在边缘位置误判,导致AI以为自己赢了。

2.3 搜索与评估的解耦设计

搜索层和评估层的接口设计是关键。Wuziqi项目定义了一个统一的评估接口:输入是棋盘状态和当前轮到谁,输出是一个浮点数分数。搜索层不关心这个分数是手工算的还是神经网络吐的,它只管用。这样一来,你可以先用简单的手工评估跑通流程,之后再无缝替换成神经网络。

这种解耦带来的另一个好处是方便做A/B测试。你可以固定搜索参数,只换评估函数,对比胜率变化。我在实际调优时就是这么做的:先用手工评估跑基线,然后换成小网络,再换成大网络,每一步都记录胜率,这样能清楚知道每个改动的贡献。

3. 核心算法拆解:搜索与评估怎么配合

3.1 蒙特卡洛树搜索的四个阶段

MCTS是这类项目最常用的搜索算法,它的核心思想是"用随机模拟的结果来指导搜索方向"。整个过程分四步,我结合棋类场景讲:

  1. 选择(Selection):从根节点出发,每次选一个"最值得探索"的子节点,直到到达叶子节点。选择依据是UCT公式,它平衡了"利用"(选当前胜率高的)和"探索"(选访问次数少的)。
  2. 扩展(Expansion):在叶子节点上,把所有合法落子作为新节点加进去。
  3. 模拟(Simulation):从新节点开始,双方随机落子直到分出胜负,得到一个结果。
  4. 回传(Backpropagation):把模拟结果沿着路径往回更新每个节点的访问次数和胜率。

UCT公式长这样:

UCT = (w / n) + c * sqrt(ln(N) / n)

其中w是胜场数,n是该节点访问次数,N是父节点访问次数,c是探索常数。c越大越偏向探索,越小越偏向利用。经验值一般在1.0到1.5之间,棋类项目里1.414(根号2)是个常见起点。

注意:模拟阶段如果完全随机落子,在围棋这种复杂棋类里效果很差,因为随机走法几乎不可能走出有意义的局面。所以Wuziqi项目在模拟阶段引入了轻量级的启发式策略,比如优先考虑靠近已有棋子的位置、避免明显送死。这一步的改动对棋力提升非常明显。

3.2 评估函数的两种路线

评估函数决定了AI"看得懂"多少局面。两条主流路线:

手工特征路线:提取棋型特征,比如活三、冲四、连五等,给每种棋型赋分,最后加权求和。优点是快、可解释、不需要训练数据;缺点是需要大量领域知识,且难以覆盖复杂局面。

神经网络路线:用卷积网络直接输入棋盘,输出局面分数和落子概率。优点是表达能力强、能学到人类难以描述的模式;缺点是需要训练数据和算力,推理也有延迟。

Wuziqi项目的做法是先用手工特征跑通,再逐步引入神经网络。手工特征的评分表我整理了一个参考版本:

棋型分值说明
连五100000直接获胜
活四10000两端开放的四连
冲四1000一端被封的四连
活三1000两端开放的三连
眠三100一端被封的三连
活二100两端开放的二连

这些分值不是随便定的,核心原则是"能形成必胜威胁的棋型分值要远高于普通棋型"。活四和冲四的分值差距之所以大,是因为活四几乎无法防守,而冲四可以被堵。

3.3 搜索深度与耗时的权衡

搜索越深,AI越强,但耗时也越长。在实时对弈场景下,每步棋通常有时间限制,比如1秒或3秒。Wuziqi项目采用的是"时间控制 + 迭代加深"的策略:先设定一个时间预算,然后从深度1开始逐步加深,每次加深前检查剩余时间,超时就停止。

这种策略的好处是任何时候中断都能拿到一个可用的结果。我实测下来,在普通笔记本上,MCTS跑1秒大概能模拟几千到几万次,具体取决于棋盘大小和模拟策略的效率。如果模拟函数写得重,次数会明显下降,所以模拟阶段的代码要尽量精简。

实操心得:不要一上来就追求深度。先把模拟次数提上去,往往比加深搜索更有效。因为MCTS的强度很大程度上取决于模拟的覆盖广度,而不是单条路径的深度。

4. 实操过程:从零跑通一个可对弈的版本

4.1 环境准备与依赖选择

这类项目的依赖其实很轻。核心只需要Python(建议3.8以上)和NumPy。如果要用神经网络,再加PyTorch或TensorFlow。我建议先用纯Python + NumPy跑通,确认逻辑无误后再引入深度学习框架,否则调试时会分不清是算法问题还是框架问题。

目录结构建议这样组织:

wuziqi/ rules/ # 规则层 board.py judge.py search/ # 搜索层 mcts.py eval/ # 评估层 heuristic.py network.py train/ # 训练层 self_play.py tests/ # 单元测试 main.py # 入口

这种结构的好处是每层可以独立测试。比如你可以只测规则层的胜负判定,不用启动搜索。

4.2 规则层的实现要点

规则层最关键的是胜负判定。我的做法是:每次落子后,只检查经过该落子的四条线,而不是全盘扫描。这样每次判定的复杂度是O(1)级别,而不是O(N²)。

具体做法是以落子点为中心,向四个方向各延伸4格,统计连续同色棋子数量。如果某方向达到5个,就判胜。这里要注意边界处理,越界的位置直接跳过。

禁手检测是另一个重点。不同棋类规则不同,有的有禁手有的没有。Wuziqi项目默认不启用禁手,但预留了接口。如果你要做标准规则,禁手判定会复杂不少,需要单独处理三三、四四、长连等情况。

提示:写规则层时,一定要用"落子-判定-撤销"的循环做压力测试。随机落子几千次,检查有没有异常状态,这能提前发现大部分边界bug。

4.3 搜索层的参数配置

MCTS的核心参数就几个,我给出一个经过实测的起点配置:

config = { "c_puct": 1.414, # 探索常数 "simulations": 800, # 每次决策的模拟次数 "time_limit": 1.0, # 时间上限(秒) "dirichlet_alpha": 0.3, # 根节点噪声参数 "temperature": 1.0, # 落子温度 }

c_puct控制探索与利用的平衡,1.414是理论推导的常用值。simulations和time_limit二选一,谁先到就停。dirichlet_alpha用于在根节点加噪声,增加探索多样性,主要在训练时用。temperature控制落子的随机性,对弈时通常设小一点甚至趋近0,训练时设大一点。

我踩过的一个坑是:一开始把simulations设得很高,结果每步要等十几秒,调试效率极低。后来改成时间控制,设1秒,既能保证一定强度,又不影响迭代速度。

4.4 自我对弈与数据收集

自我对弈是提升棋力的关键。基本流程是:让当前模型和自己下若干盘,记录每步的棋盘状态、落子概率、最终胜负,作为训练数据。

这里有个细节很重要:胜负标签要从当前玩家视角记录。也就是说,如果最终黑棋赢了,那么黑棋的每一步标签是"赢",白棋的每一步标签是"输"。如果搞反了,训练出来的模型会学出反向策略。

数据存储建议用简单的格式,比如每行一个样本,包含棋盘状态、落子概率分布、胜负值。不要一上来就上数据库,文件存储足够,等数据量大了再考虑优化。

实操心得:自我对弈的盘数不是越多越好。我试过一次性跑几千盘,结果训练时发现数据分布太集中,模型过拟合。后来改成每轮跑几百盘,训练完再跑下一轮,效果反而更好。这叫"迭代式训练",比一次性堆数据更稳。

5. 常见问题与排查技巧实录

5.1 棋力不升反降的排查思路

这是最常见的问题:训练了几轮,发现AI反而变弱了。原因通常有几个:

  • 数据标签错误:胜负视角搞反了,模型学反了。
  • 学习率过大:模型更新太猛,把之前学好的策略覆盖了。
  • 评估与搜索不匹配:评估函数的分值范围和搜索的胜率范围不一致,导致搜索被误导。
  • 过拟合:训练数据太少或太单一,模型记住了特定局面而不是通用策略。

排查顺序建议从数据标签开始,这是最容易出错也最容易验证的。写个小脚本,随机抽几条数据,人工检查标签是否合理。

5.2 搜索速度慢的优化方向

如果每步耗时太长,可以从这几个方向优化:

优化方向具体做法预期收益
模拟函数精简去掉不必要的计算高
棋盘复制优化用一维数组或位棋盘中
并行模拟多线程或多进程高
缓存评估结果相同局面复用中
剪枝提前排除明显劣着中

并行模拟是最直接的提速手段,因为MCTS的模拟之间相互独立。Python里可以用多进程,但要注意进程间通信开销。如果模拟函数很轻,多进程反而可能更慢,需要实测。

5.3 常见问题速查表

现象可能原因解决方法
AI落子非法规则层漏判检查合法落子生成逻辑
AI反复走同一步温度太低或搜索无多样性提高温度或加根节点噪声
训练loss不下降学习率或数据问题检查标签、调小学习率
对弈时卡顿搜索耗时过长降低模拟次数或加时间限制
模型输出全同输出层或激活函数问题检查网络结构和初始化

注意:遇到问题时,先缩小范围。比如怀疑是搜索问题,就固定评估函数,只测搜索;怀疑是评估问题,就固定搜索,只测评估。不要同时改多个变量,否则无法定位。

6. 后续扩展与个人体会

这个项目跑通之后,能扩展的方向其实很多。比如把评估网络换成更大的模型,或者引入残差结构;比如把搜索从MCTS换成Alpha-Beta,对比不同算法的强度;比如加入开局库,让AI在开局阶段更快落子。我自己试过加开局库,效果立竿见影,因为开局阶段搜索最耗时,有了库可以直接跳过。

另一个值得尝试的方向是分布式自我对弈。单机跑几百盘要很久,如果能用多台机器并行,数据收集速度会快很多。不过这涉及任务调度和数据合并,复杂度不低,建议先把单机版本调稳再说。

我个人在实际操作中的体会是:这类项目最大的难点不在算法,而在工程细节。规则写错一个边界、数据标签搞反一次、参数配错一个数量级,都会让整个系统表现异常。所以我的建议是,每加一个功能就写测试,每改一个参数就记录对比,把整个过程当成实验来做,而不是当成代码来写。这样即使出了问题,你也能快速定位,而不是对着一堆日志发呆。

最后分享一个小技巧:如果你觉得训练太慢,可以先在一个小棋盘上验证流程,比如9×9或者更小。小棋盘上跑通整条链路,确认无误后再放大到标准棋盘。这样能省下大量等待时间,也能更快发现逻辑问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 1:13:17

Dify 数据备份与跨环境迁移实战:从单机容灾到生产环境完整指南

1. 为什么备份这件事,等到出事再想就晚了我见过太多团队在部署完 Dify 之后,把全部精力放在编排提示词、调试工作流、接入知识库上,却对“数据怎么保命”这件事一拖再拖。直到某天服务器磁盘告警、容器卷被误删、或者需要把整套环境从测试机搬…

作者头像 李华
网站建设 2026/10/24 1:13:16

英制度量衡全解析:从英寸到加仑的换算逻辑与实操避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 1:12:21

2024年IDEA安装与Java环境配置完整指南:从下载到避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 1:11:35

10套快速构建专业网站的模板:选型、改造与上线实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 1:11:32

基于机器学习的SPEI干旱预测流水线与工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 1:11:20

从AlphaFold预测结构到蛋白质设计:数据筛选与模型训练实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华