
象棋打谱和 AI 分析软件真正做起来之后你会发现它并不是一个必须会算法竞赛才能碰的项目。它的核心价值是把一盘棋变成程序里的着法序列再借助一个本地象棋引擎对每一步局面给出打分和推荐招法。最值得关注的是它不需要高端显卡不需要部署大模型普通电脑就能本地跑通。适合两类人一类是想深度复盘象棋对局、又不满足于现成 App 功能的学习者另一类是刚开始做工具型应用、想体验子进程通信和协议解析的开发者。下面我按自己的实现顺序拆一遍。整个过程不算复杂但有几个地方特别容易绕弯尤其是“棋盘库选型”和“引擎协议解析”这两块。我会把每一步为什么要这么做、做到什么程度算通过都写清楚。1. 打谱软件到底要解决哪三件事1.1 打谱不是“摆棋子”是回放和记录很多初学者把打谱理解成“把棋子在棋盘上摆出来”。实际上打谱的核心是两层记录和回放。记录要求每一手棋都有明确编码。不管用户是手动输入着法还是从棋谱文件导入程序都必须同时维护两个东西当前局面、着法历史。当前局面用来决定下一步是否合法着法历史用来回放和导出。回放则要求你可以前进、后退、跳到某一手并且在跳转时能把棋盘恢复到对应局面。少了这一层打谱软件就只是一个棋盘画布谈不上复盘工具。这里最容易做错的地方是只记着法字符串没有记录每一步之后的完整局面。比如用户连续悔棋十步再重新走另一路如果程序只维护一个“当前着法列表”后面所有分支都会丢失。所以我的建议是底层的棋盘状态最好交给现成棋类库维护程序本身只负责操作历史。1.2 AI 分析不是聊天是引擎给打分和推荐招法另一个常见误解是AI 分析等于接入一个大模型让模型告诉我这步走得好不好。实际上象棋分析的主流做法是连接本地 UCI 引擎。引擎通过搜索树计算返回当前局面的分数和最佳着法本质上是“搜索 局面评估”和聊天式 AI 完全是两套东西。好处很明显无需联网、无需 GPU、结果稳定、可重复验证。一台普通办公电脑就能跑。坏处是你要先学会读懂引擎输出。我做这个项目时最大的体会就是先搞清楚“分析结果是什么格式”再去写界面。拿到一行类似info depth 18 score cp 45 pv h2e2 ...的输出你就知道引擎已经算到第 18 层认为当前局面红方有约 45 分的优势推荐从 h2 到 e2 的着法。不理解这个格式后面解析、展示、写报告都会卡住。1.3 初学者最容易忽略的功能边界一个“能用”的版本和一个“永远做不完”的版本差别在于是否提前划定了边界。初学者常犯的错误是边做边加功能今天想加棋盘贴图明天想加局面注释后天想加网络对局采集。结果核心的记谱和引擎通信反而没做稳。更稳妥的路线是先做单局棋谱的读取和回放。再做单局面的引擎分析。最后扩展成批量分析和报告导出。每一步都能独立验证再进入下一步。我见过的翻车项目绝大多数是倒过来界面抄了一堆核心协议一行没通。2. 技术选型Python 能省掉多少重复工作2.1 棋盘状态交给库不要自己写棋规象棋规则看起来简单实际实现非常琐碎马的蹩脚、象的塞眼、将帅不能对脸、循环长将的判断这些如果全部自己写会让初学者一下子掉进棋规深渊。正确的做法是找现成的棋类库。国际象棋领域python-chess很成熟它也支持多种棋类变体如果你安装的版本里对中国象棋支持不完整就换一个支持中国象棋的库或者只实现最基础的局面维护把合法性校验放到引擎一侧。关键是不要自己重复造棋规轮子。自己做一遍棋规学习价值确实有但会拖慢整个项目进度对初学者并不友好。2.2 界面选择tkinter、PyQt 还是网页界面是初学者第二个纠结点。我给一个简单的选择标准界面方案适合场景上手难度备注纯命令行先验证核心逻辑最低最适合第一阶段tkinter本地小工具、课程设计低Python 自带无需额外安装PySide6 / PyQt想要专业桌面软件中事件驱动和布局更可控Flask 简单前端想做成网页或报告页中高引擎跑在服务端方便共享我一般会建议先做纯命令行版本或者只用一个最简单的 tkinter 窗口。因为界面不是这个项目的难点逻辑才是。等分析流程稳定了再回来补界面两三天就能补完。2.3 引擎连接走 UCI 协议目前象棋开源引擎大多支持 UCI 协议也有使用 UCCI 的。UCI 是一种基于文本行的协议外部程序只要做到三件事启动引擎子进程、通过标准输入发送命令、读取标准输出解析结果就能完成调用。这个设计跨语言、跨系统都适用。初学者会在这里第一次接触到“子进程通信”的开发概念也是这个项目最有学习价值的部分。很多现成 App 把这一步封得死死的自己做一遍之后你会对“软件如何调用外部算法程序”有一个非常直观的理解。3. 环境准备与最小可运行骨架3.1 环境与依赖最低配置一台能装 Python 3.9 以上版本的电脑2GB 内存就能跑只是分析速度慢一点。想跑更深层的分析建议 8GB 内存让引擎的 Hash 开到 256MB 或 512MB。系统方面 Windows、macOS、Linux 都可以但要注意引擎程序要下载对应系统的版本Windows 版不能直接在 Linux 上跑。依赖上我建议只装最少的库一个支持目标棋类的棋盘库用于局面维护和着法推进。Python 自带的subprocess用于启动引擎。可选的一个 GUI 库。不要把项目一开始就引入一堆框架。依赖越多初学者排查问题的范围就越大。3.2 项目目录结构一个清晰的目录结构能省掉很多排查成本。我的参考结构是这样xiangqi_analyzer/ ├── main.py ├── board_manager.py ├── engine_client.py ├── analyzer.py ├── games/ ├── reports/ └── engines/games放棋谱文件engines放引擎程序reports放分析报告。启动后生成的临时文件不会和源码混在一起删除重来也方便。3.3 最小可运行示例先写一个能把一局棋读进内存、再回放一遍的最小程序。假设棋谱输入是每行一个着法h2e2 h9g7 h0g2 i9h9读取后逐手推进局面打印每一步之后的局面描述。示例骨架可以这样写# board_manager.py 示例骨架 class GameRecorder: def __init__(self): self.moves [] # 着法列表 self.fen_list [] # 每一步后的局面快照 self.current 0 def append_move(self, move_str): # 调用棋类库推进局面得到一个新的局面快照 fen self._make_move(move_str) self.moves.append(move_str) self.fen_list.append(fen) self.current len(self.fen_list) - 1 def back(self): if self.current 0: self.current - 1 return self.fen_list[self.current] def forward(self): if self.current len(self.fen_list) - 1: self.current 1 return self.fen_list[self.current]能跑通这一步说明棋盘库、着法格式、局面恢复三个基础链路已经通了。这个阶段不要连接引擎先确认日志输出正常再进入下一层。4. 核心功能实现记谱、回放、存档4.1 着法记录着法记录是打谱软件的地基。要决定的一件事是程序内部到底用什么格式存着法。常见选择有两种坐标格式例如h2e2简洁且适合直接传给 UCI 引擎。中国象棋文字谱例如炮二平五适合展示给用户看但需要额外的坐标转换逻辑。我的建议是内部统一用坐标格式展示层再做转换。因为引擎通信、棋谱存档、局面推进全都依赖同一个稳定格式。如果界面显示什么内部就存什么后面会很痛苦。4.2 回放控制回放控制的核心是“当前指针”这个概念。不要一上来就想着做复杂的树形分支先做线性回放就够用前进current 1恢复fen_list[current]对应的局面。后退current - 1同样恢复局面。跳转直接把current设置为目标手数再恢复局面。这里的要点是所有跳转都通过局面快照恢复而不是从头重新推演。否则棋谱一长点击“回到第 100 手”就要重算 100 次体验很差。4.3 存档格式存档格式决定了你的棋谱能不能被其他工具读取。如果目标是通用性可以导出 PGN 格式如果只是自己用一个简单的文本文件也足够。我自己会同时保留两层原始着法文件纯文本每行一个着法方便脚本处理。带分析的报告文件包含着法、局面分数、推荐着法、评注。千万不要把分析结果和原始着法混在同一个结构里改来改去否则一次解析失败整局棋谱都可能报废。5. 接入 AI 分析引擎5.1 UCI 协议的基础流程UCI 协议的核心流程并不复杂初次接触时照着这个顺序走启动引擎子进程。发送uci等待引擎返回uciok。发送isready等待返回readyok。发送position startpos moves h2e2 ...指定要分析的局面。发送go depth 18或go movetime 3000开始分析。持续读取引擎输出直到收到bestmove开头的行。示例骨架# engine_client.py 示例骨架 import subprocess class UciEngine: def __init__(self, engine_path): self.proc subprocess.Popen( [engine_path], stdinsubprocess.PIPE, stdoutsubprocess.PIPE, textTrue, encodingutf-8, errorsignore, bufsize1, ) self.proc.stdin.write(uci\n) self.proc.stdin.flush() def analyse(self, fen, depth12, movetimeNone): cmd fposition fen {fen}\n if movetime: cmd fgo movetime {movetime}\n else: cmd fgo depth {depth}\n self.proc.stdin.write(cmd) self.proc.stdin.flush() info_lines [] while True: line self.proc.stdout.readline().strip() if line.startswith(bestmove): break info_lines.append(line) return parse_info(info_lines)注意parse_info需要根据你实际拿到的引擎输出来写。不同引擎返回的信息行字段顺序可能不同最好先打印原始输出再写解析逻辑。5.2 一次分析请求怎么发最简单的分析方式是给引擎发送一个完整的局面描述让它自己搜索。这里要区分两种情况分析当前正在打谱的局面直接用当前棋局的局面快照生成position fen ...命令。分析整个棋谱从初始局面开始逐手发送 moves 列表或者直接跳到某一步再发go。我建议第二种情况拆开做先把整局棋谱的每一步局面快照生成好再逐个发送给引擎。这样即使中间某一步失败了也能通过快照列表定位到具体是哪一手的问题。5.3 参数怎么选深度、时间、多方案引擎分析有四个常用参数初学者容易一上来就把数值拉满参数作用学习场景建议depth搜索深度10 到 15movetime固定思考时间毫秒1000 到 3000multipv返回几条候选着法2 到 3hash引擎哈希内存大小128 到 512 MB深度越大分析越准但耗时增长非常明显。不要一上来就跑到 30 层。先设 12 层跑通全流程确认解析和存档正常再慢慢加大。注意这里不要一上来就把深度和 Hash 拉满。先用一条样例确认输入、输出和日志都正常再考虑更高配置。6. 做一个简单的“分析报告”功能6.1 单局面分析到全文批注能把单局面分析跑通后就可以做全文批注遍历整局棋谱的每一个局面快照逐个发送给引擎把返回的分数和推荐着法收集起来整理成结构化结果。示例骨架# analyzer.py 示例骨架 def analyze_game(recorder, engine, depth12): report [] for index, fen in enumerate(recorder.fen_list): result engine.analyse(fen, depthdepth) report.append({ move_no: index // 2 1, side: 红 if index % 2 0 else 黑, fen: fen, score: result.get(score), bestmove: result.get(bestmove), }) return report这个阶段的核心是“可重复”。同一局棋跑两次结果应当一致或接近一致。如果每次结果差异很大先检查引擎是否在同一局面下收到了相同的position命令再检查参数是否设置正确。6.2 批量分析棋谱时的命名和结果合并批量分析是很多人踩坑的地方。第一坑是输出文件重名第二坑是部分棋谱分析失败后没有记录。我的建议是每个输入棋谱文件对应一个独立输出目录文件名里带上时间和原文件名前缀例如reports/20250216_1530_game01.md reports/20250216_1530_game02.md分析失败时不要直接中断整个任务而是把失败原因记录到汇总日志里继续处理后面的棋谱。等全部处理完再统一看失败列表。这样批量任务才不会因为一份格式错误的棋谱就全军覆没。6.3 输出格式建议对初学者来说我最推荐两种输出格式Markdown 报告直接生成可读的复盘表格方便写博客或自学。纯文本报告包含着法、分数、推荐着法方便脚本继续处理。表格格式可以参考手数方着法引擎分数推荐着法备注1红h2e245h2e2正常开局2黑h9g738h9g7正常应对不要一开始就做花哨的 HTML 和图表。文本报告能看清楚再往上加展示层。7. 常见问题排查顺序7.1 引擎启动失败先看路径和权限初学者最常见的报错是FileNotFoundError或者引擎进程一启动就退出。这时候先不要怀疑代码按顺序检查引擎文件路径是否正确相对路径是否基于当前工作目录。文件是否有执行权限Windows 下是否需要管理员权限。引擎是不是下载错了系统版本。终端里手动执行引擎命令看能否正常启动。手动启动引擎这一步非常重要。如果引擎在终端里都起不来代码写得再对也没用。7.2 输出为空先看输入记谱格式引擎能启动但没有返回任何分析结果最常见的原因是position命令里的着法格式和引擎预期不一致。不同引擎对着法编码的要求可能不同有的用坐标有的用中心点坐标有的要求moves中间不能有空格以外的字符。排查顺序是先打印你发送给引擎的完整命令。再打印引擎的原始输出。对比引擎文档里的示例格式。很多问题看起来像协议不兼容实际上是输入串里多了一个空格、少了一个换行或者编码不对。7.3 界面卡顿资源占用与任务队列如果界面和分析放在同一个线程里分析时界面几乎一定会卡住。因为引擎搜索是阻塞式操作会占满 CPU。解决办法有两条分析放到单独线程界面主线程只负责更新状态。把界面和分析完全分开命令行负责分析界面负责展示结果文件。对我个人来说第二阶段用方案二最省心。等分析结果生成完界面再去读报告文件界面和引擎之间没有直接耦合问题范围小很多。7.4 结果不合理先看深度和参数如果分析结果出现明显的“错招”不要急着怪引擎。常见原因包括深度太低比如只有 5 层引擎只看到眼前几步。Hash 太小导致搜索过程中频繁丢缓存。multipv设置异常返回的不是最优解。局面描述错误例如 fen 字段顺序不对引擎分析的根本不是你想要的局面。判断标准是先用一个已知的、简单的中局局面跑引擎对比引擎给出的推荐着法和常见棋书结论。如果已知局面都分析不对说明是参数或格式问题如果已知局面正确再去看复杂局面。8. 边界和进阶路线8.1 低配置环境能学到什么程度在我的测试场景里一台 4 核 CPU、8GB 内存的普通笔记本跑 12 层深度分析单局面耗时通常在几十秒到几分钟不等具体取决于局面复杂度和引擎实现。这个速度对学习完全够用但不适合大批量复盘。如果只是验证流程我建议把深度降到 10甚至先跑 6 层确认链路通了再往上加。低配置环境能跑不代表它适合批量任务。批量分析之前先用小样本估算单局面平均耗时再决定一次开多少个任务。不要一上来就开最大并发否则内存和 CPU 都会被拖垮。8.2 从“能用”到“好用”需要补什么如果核心链路已经跑通后面可以按优先级补齐这些能力着法输入校验用户输入非法着法时给出明确提示。局面注释在分析结果上手动补充自己的复盘笔记。分支管理支持从某一手分叉比较不同走法的优劣。棋谱导入导出支持常见棋谱格式方便从外部工具导入。引擎参数配置把深度、Hash、思考时间做成可视化选项。这些功能里最值得优先做的是“着法输入校验”和“局面注释”。前者决定易用性后者决定能否真正沉淀复盘知识。8.3 不建议一上来就做的事情最后说几个我见过很多初学者踩进去的坑建议直接避开不要一开始就做华丽的立体棋盘贴图棋盘用简单文字或色块代替先验证逻辑。不要一开始就接入大模型写“棋评”棋评质量不稳定且把核心问题复杂化了。不要把所有棋谱和引擎输出放在同一个文件里反复改写分开存储。不要同时兼容国际象棋和中国象棋第一版只支持一种棋类否则棋规、界面、存档全都要做两套。踩过几次之后我发现很多问题不是工具能力不够而是前置环境和输入材料没有处理干净。这个项目真正的难点不是“能不能写出来”而是“能不能在边界清晰的情况下逐步推进”。把单局棋谱跑稳把引擎协议解析清楚再考虑批量、界面和复杂功能新手也能做出一个可以自用的象棋打谱与 AI 分析工具。