把 AI 的思考过程做成一棵能翻的树
给搜索过程做可视化,难点不在画树,而在数据要是真的、面板不能把棋盘顶来顶去、以及它得按需加载不拖累不点它的人。
五子棋 和中国象棋 的 AI 都会搜好几层。我加了个「工具树」按钮,点开能看到它每一层搜到什么、选了哪一步、有多确定。
功能本身不复杂,但有四个坑,每个都值得单独说。
坑一:展示的分值必须是真分值
第一版做完,界面上八个根候选的分值全是 -4517.7,一模一样。看着像坏了,其实没坏——那是 alpha-beta 的边界值。
alpha-beta 剪枝的本质是:当某个候选已经确认不会比当前最好更优时,立刻停止搜索并返回一个「界限」,而不是真实分。所以根层循环里,除了第一个候选,其余大多拿到的是边界。
修法是:用户要看的这一次搜索,根层每个候选都用全窗口(-Infinity, Infinity)搜一遍。剪枝在顶点层本来也省不了多少,而正确性值得这个代价。
const value =
atRoot && rootDepth > 1
? negamax(ctx, depth - 1, -Infinity, Infinity, !isMax, ai, rootDepth)
: negamax(ctx, depth - 1, alpha, beta, !isMax, ai, rootDepth);
给用户看的数字必须是真数字。用边界值凑出来的「过程」比没有过程更糟。
坑二:置信度不能是个恒定值
置信度我当时是这么算的:最优与次优的分值差,除以一个「活四」的分值当作归一化。结果永远在 0.1 到 0.5 之间。
原因是量纲错了:「活四」是三十万量级,而正常局面里最优与次优差个一两万,除下来只有 0.06。置信度被永久压在低位,等于一个恒定的数字,不如不显示。
改成用一个冲四(两万六)当单位,再加一点点深度权重:
const gap = sorted.length > 1 ? Math.abs(top.score - sorted[1].score) : PATTERN.FOUR * 2;
const depthPart = Math.min(1, d / 6);
const gapPart = Math.min(1, gap / PATTERN.FOUR);
const confidence = Math.max(decided, Math.min(1, depthPart * 0.35 + gapPart * 0.65));
这样开局对攻那类「怎么走都差不多」的局面给到七成,而必杀手直接给满分。数字开始有区分度了。
坑三:面板不能把棋盘顶来顶去
这是用户提的问题:嘲讽提示原本插在棋盘上方的正常流里,它一出现一消失,棋盘就上下跳一次。下棋时视线跟着棋盘走,跳一下非常烦。
两条一起改:
- 固定高度。
.notes写死height: 5.5rem,内容多长都不许撑高。这是组件契约,不是样式细节。 - 书页式翻页。提示不再「出现几秒后消失」,而是进队列,用左右箭头翻。这样还能回答「刚才那步触发了哪条提示」。
翻页游标的状态机抽成了纯逻辑(logic/notes.ts),因为这里最容易错的是新增一条时光标去哪。规则是:
| 用户当前 | 新增一条后 |
|---|---|
| 停在最新一页 | 跟到新条 |
| 正在往回翻 | 原地不动 |
| 发生容量淘汰 | 左移被丢弃的条数 |
我第一版把第三条无条件套用了,结果没淘汰时也左移一格——用户正盯着第 1 条看,新来一条,画面上的字换成了第 2 条。自检里那条断言「看历史时追加不抢光标」就是这么抓出来的。
坑四:按需加载,别拖累不点它的人
工具树带着一套 SVG 布局代码。不点这个按钮的玩家不该为它付流量。用 defineAsyncComponent 包起来,构建后它是独立 chunk:
const ToolTree = defineAsyncComponent(() => import('@/games/gomoku/ToolTree.vue'));
注意这里不能配 client:*。水合指令是编译期转换,作用在动态组件上会失效——这是项目里已记录过的坑。应用页本来就走「静态空壳 + createApp 挂载」,异步组件在运行时自己加载即可。
构建产物里能看到 _astro/ToolTree.xxxx.js 是单独一个文件,说明按需加载真的生效了。
树要真的长得像树
既然叫工具树,就不能是列表。我用 SVG 手画了「根 → 候选 → 应手」三层:
- 节点是硬边方框(符合站点全直角的观感),等宽字体显示落点与分值
- 连线用三次贝塞尔,最优分支用荧光橙加粗,其余用细墨线
- 被剪枝的节点用虚线边框,明确「这条没搜完」
- 默认只画前 5 个候选与最优分支的应手,点「展开完整过程」才全画——不然线会糊成一片
手写布局而不引 d3:结构固定三层,引一个图形库只为画这个不划算,那还会多一个按需 chunk。
还有一件事:事后点开怎么办
搜索迹只在搜索时收集才存在。玩家往往是 AI 走完了才想看——这时手上没有迹。
做法是对当前局面重算一次纯分析(不改棋盘),而不是拿空面板糊弄。代价只在主动点开时付一次,并且面板上会显示「正在为当前局面重新推演…」。
一个只会显示「暂无数据」的面板,等于没有这个功能。

评论
…