尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

离线AI人格实战:本地LLM+国际象棋引擎打造Abby Steele

离线AI人格实战:本地LLM+国际象棋引擎打造Abby Steele 先聊一个比较常见的场景本地跑了一个开源大模型对话体验不错但只要一聊到“精确计算”或者“规则推演”它就很容易一本正经地胡说八道。比如你问它国际象棋怎么走它能说出“e4 e5 不错”但继续追问几步就会出现非法走法甚至棋子在棋盘上“瞬移”。之所以会这样是因为大语言模型的本质是“根据上下文预测下一个 token”它擅长生成自然语言却不擅长做确定性的规则推演。而像国际象棋这种非常吃规则、深度和计算的场景真正靠谱的解法是把两套东西拼在一起本地 LLM负责角色人格、对话表达、情绪和点评国际象棋引擎Stockfish负责真正的棋力计算。本文要动手实现的 Abby Steele就是这样一个离线 AI 人格她住在你自己的电脑里不需要联网不调用云端 API能陪你聊天也能跟你下一盘正经的国际象棋。你可以把她理解成一个小型 Agent 实战项目LLM 负责“大脑与嘴巴”Stockfish 负责“肌肉记忆与精确计算”。本文会从原理讲到完整代码最后给出可运行的 Python 工程。无论你是刚接触本地 LLM 的新手还是想练手 Agent 架构的开发者都可以照着搭一遍。1. 背景与核心概念1.1 什么是离线 AI 人格离线 AI 人格简单说就是运行在你自己电脑上的数字角色。它由本地 LLM 驱动具备稳定的性格设定、说话风格和交互模式。和云端助手最大的区别是数据不出本机隐私边界清楚不按 Token 付费推理成本可以忽略不依赖外网 API断网也能用。但“有性格”只是第一层。如果这个角色只能聊天那它更像一个本地版聊天机器人。真正有意思的是把“对话能力”和“专业工具”结合起来让这个角色既能说话也能做事。Abby Steele 的定位就是一个“会下棋的离线人格”。她能像朋友一样和你聊天也能切换成棋手状态认真和你下一盘棋。这个设计非常适合作为 Agent 开发的入门案例因为它足够小却覆盖了“LLM 工具调用”的核心路径。1.2 为什么需要国际象棋引擎而不是让 LLM 直接下棋先看一个关键问题国际象棋的走法为什么不能完全交给 LLM第一个原因是正确性。国际象棋有严格的移动规则比如“马走日”“王车易位有条件”“吃过路兵只能立即执行”。LLM 能背出这些规则但在长对局中很难稳定推理。早期用 LLM 直接下棋的实验里最常见的失败就是“吃到不存在的子”和“走到非法格子”。第二个原因是计算深度。局部战术、杀棋、长线弃子都需要大量搜索。Stockfish 这类引擎使用 alpha-beta 剪枝、局面评估函数、开局库和残局库复杂度远超 LLM 直接生成。第三个原因是表达与计算的解耦。让 LLM 计算棋步是一种资源错配。更好的分工是Stockfish 负责找最优走法LLM 负责把这个走法翻译成 Abby 说的话。举个例子用户走了一步漂亮的弃子Stockfish 能算出“这步有补偿”但不会说人话。LLM 可以把局面翻译成“你这一手有点意思我要是贪吃接下来会被你抽车。”这就是两者结合的价值。1.3 技术组成概览整个项目可以拆成三层用户输入 ↓ Abby 主循环Python ├── 本地 LLM人格对话、下棋意图判断、局面点评 └── 国际象棋引擎计算并返回最佳走法 ↓ 控制台输出Abby 的话 棋盘可视化在本文的代码实现中本地 LLM 使用 Ollama 作为推理服务HTTP 接口方式调用国际象棋引擎使用 Stockfish棋盘状态、走法合法性校验、FEN 与 SAN 转换使用python-chess库主程序是一个命令行交互工具用户输入文字Abby 回复文字。接下来按照这个架构从环境准备开始一步步搭建。2. 环境准备与项目架构2.1 硬件与运行环境本地 LLM 对硬件有一定要求但门槛不高。一个 3B 到 7B 参数的量化模型在普通笔记本上就可以运行内存 8GB可以运行 3B 左右的小模型内存 16GB可以运行 7B 到 14B 的量化模型NVIDIA 显卡 8GB 以上显存推理速度会明显提升但不是必须。本文示例是一个“最小可运行方案”优先保证流程能跑通。硬件条件更好的话可以换更大的模型来提升对话质量。2.2 安装本地 LLM 推理服务本文使用 Ollama 作为本地 LLM 推理服务原因是安装简单、API 接口清晰。如果你更熟悉 llama.cpp 或 llamafile基本原理相同只需要替换掉 HTTP 调用层。建议按照 Ollama 官方文档的安装方式安装不要随意使用第三方脚本。安装完成后打开终端验证ollama --version然后拉取一个适合入门的小模型。以下命令以llama3.2:3b为例具体模型名以你本地实际可用的模型为准ollama pull llama3.2:3b拉取模型需要联网模型下载到本地后后续对话推理都发生在你的电脑上。之后启动服务ollama serve执行后 Ollama 会监听本机的11434端口。这里需要重点说明一下“离线”的边界首次下载模型文件需要联网模型加载、推理、对话完全在本机完成如果处于无外网环境可以提前在联网机器上下载模型再拷贝到本机 Ollama 的模型目录。2.3 安装 StockfishStockfish 是目前最常用的开源国际象棋引擎遵循 UCIUniversal Chess Interface协议。安装方式因系统不同而不同# macOS brew install stockfish # Debian / Ubuntu sudo apt install stockfish在 Windows 上可以从 Stockfish 官网下载对应平台的二进制文件然后把可执行文件所在目录加入 PATH。安装完成后在终端手动验证一下引擎是否可用stockfish进入引擎交互界面后输入uci并回车如果看到包含uciok的输出说明安装成功id name Stockfish ... uciok然后输入quit退出。2.4 项目目录结构创建一个项目目录这里命名为abby-steeleabby-steele/ ├── requirements.txt ├── config.py ├── llm_client.py ├── chess_engine.py ├── abby.py └── main.py其中requirements.txt内容如下requests python-chess安装依赖pip install -r requirements.txtpython-chess负责棋盘状态管理、走法解析与合法性校验Stockfish 负责计算走法两者是互补关系。3. 核心原理拆解3.1 本地 LLM 的对话接口Ollama 提供/api/chat接口请求结构与 OpenAI 兼容。最小请求如下curl http://localhost:11434/api/chat -d { model: llama3.2:3b, messages: [ {role: user, content: 你好} ], stream: false }Python 中可以直接用requests封装。关键参数有两个stream是否流式返回。控制台演示时用false即可追求交互体验时可以用true。options.temperature控制随机性。人格对话建议设置为 0.7 左右棋局点评时建议降到 0.3 以下让话术更稳定。在真实项目中你需要维护一个messages列表它是所有对话历史的载体。系统提示词放在第一项之后是新消息。3.2 国际象棋引擎与 UCI 协议UCI 协议是脚本、GUI 与引擎之间的通信标准。它的核心是文本命令和文本回显。手动交互时最常用的命令uci请求引擎返回能力信息并以uciok结束isready检测引擎是否就绪position startpos初始化棋盘到初始局面go movetime 500让引擎思考 500 毫秒并输出bestmove。一个典型的过程是position startpos go movetime 500 info depth 15 score cp 32 ... bestmove e2e4如果自己写底层通信需要处理大量info行并且要异步读取标准输出否则主线程会卡住。下面是一个用于理解原理的最小片段# uci_demo.py —— 仅用于演示底层通信原理 import subprocess import threading import queue def start_engine(pathstockfish): p subprocess.Popen( [path], stdinsubprocess.PIPE, stdoutsubprocess.PIPE, stderrsubprocess.DEVNULL, textTrue, bufsize1, ) out_q queue.Queue() def read(): for line in p.stdout: out_q.put(line.strip()) threading.Thread(targetread, daemonTrue).start() return p, out_q def send(p, cmd): p.stdin.write(cmd \n) p.stdin.flush()为什么要用queue和后台线程因为引擎的输出是持续不断的如果直接readline()输出多的时候会阻塞主流程。后台线程把输出放进队列主线程可以按需获取。这是底层的标准做法。在实际项目中我们不必自己写完整的 UCI 客户端python-chess已经封装了这些细节。理解底层协议仍然很重要因为排查问题时你会需要知道“bestmove 在哪里”“info 行是什么意思”。3.3 聊天与下棋的状态切换Abby 的工作流需要区分两种模式聊天模式用户输入普通文本交给 LLMLLM 以 Abby 人格回复。下棋模式用户输入一个符合 SANStandard Algebraic Notation的走法比如e5或Nf6程序先校验走法再让 LLM 生成点评最后让 Stockfish 计算应手。在项目中切换时机可以这样设计默认处于聊天模式当用户消息包含“下棋”“国际象棋”“对弈”等关键词时进入下棋模式当用户说“不下了”“结束棋局”时退出下棋模式回到聊天。关键词判断虽然简单但可控性最好。你也可以让 LLM 做意图分类只是那样会增加一次本地推理并且返回不稳定。先跑通关键词方案后续再升级成 LLM 判断是更稳妥的思路。4. 完整实战案例搭建 Abby Steele4.1 编写配置文件配置文件统一管理路径和参数避免把硬编码散落在各个模块里。# 文件路径config.py import os OLLAMA_URL os.getenv(OLLAMA_URL, http://localhost:11434) LLM_MODEL os.getenv(LLM_MODEL, llama3.2:3b) STOCKFISH_PATH os.getenv(STOCKFISH_PATH, stockfish) ENGINE_SKILL_LEVEL int(os.getenv(ENGINE_SKILL_LEVEL, 10))说明如果你没有拉取llama3.2:3b可以把LLM_MODEL换成你本地已有的模型STOCKFISH_PATH默认是系统命令stockfish如果你的 Stockfish 不在 PATH 中就填完整路径ENGINE_SKILL_LEVEL是 Stockfish 的等级参数范围是 0 到 20值越低越弱适合陪练。4.2 编写 LLM 客户端LLMClient负责所有本地 LLM 的调用。这里封装一层后续主程序不需要关心 HTTP 细节。# 文件路径llm_client.py import requests from config import OLLAMA_URL, LLM_MODEL class LLMClient: def __init__(self, base_urlOLLAMA_URL, modelLLM_MODEL): self.base_url base_url.rstrip(/) self.model model self.session requests.Session() def chat(self, messages, temperature0.7, max_tokens512): payload { model: self.model, messages: messages, stream: False, options: { temperature: temperature, num_predict: max_tokens, }, } resp self.session.post( f{self.base_url}/api/chat, jsonpayload, timeout180, ) resp.raise_for_status() data resp.json() return data[message][content].strip()这里需要注意timeout180是因为本地模型在小内存机器上生成速度可能较慢超时放宽一些data[message][content]是 Ollama 非流式响应里的文本字段。4.3 编写国际象棋引擎封装ChessEngine把 Stockfish 和棋盘状态绑定在一起。它负责重置棋盘校验并执行用户走法调用 Stockfish 计算应手返回 FEN 和棋盘字符串用于展示。# 文件路径chess_engine.py import chess import chess.engine from config import STOCKFISH_PATH, ENGINE_SKILL_LEVEL class ChessEngine: def __init__(self, engine_pathSTOCKFISH_PATH, skill_levelENGINE_SKILL_LEVEL): self.engine chess.engine.SimpleEngine.popen_uci(engine_path) self.engine.configure({Skill Level: skill_level}) self.board chess.Board() def reset(self): self.board chess.Board() return self.fen() def fen(self): return self.board.fen() def board_to_text(self): return str(self.board) def apply_user_move(self, move_text): try: move self.board.parse_san(move_text) except ValueError: return False, 这个走法我看不明白请使用国际象棋代数记谱法例如 e5 或 Nf6。 if move not in self.board.legal_moves: return False, 这个走法不符合规则换个思路试试。 self.board.push(move) return True, self.board.san(move) def apply_engine_move(self, movetime1.0): result self.engine.play(self.board, chess.engine.Limit(timemovetime)) uci_move self.board.uci(result.move) self.board.push(result.move) return uci_move, self.fen() def is_game_over(self): return self.board.is_game_over() def result(self): return self.board.result() def close(self): self.engine.quit()说明几个关键点board.parse_san会把e5、Nf6这种人类记谱解析成Move对象board.legal_moves是当前局面的合法走法集合用它做最终校验engine.play(board, chess.engine.Limit(time1.0))让 Stockfish 思考 1 秒并返回最优走法result.move是引擎建议的走法board.uci(move)将其转成 UCI 字符串便于展示。这里movetime1.0是单步思考时间在本机性能不足时可以降到0.5体验更流畅棋力会弱一些。4.4 编写人格提示词Abby 的人格定义放在独立的模块中。系统提示词决定了 AI 人格的说话方式。# 文件路径abby.py SYSTEM_PROMPT 你是 Abby Steele一个运行在用户电脑上的离线 AI 人格。 你的性格特点冷静、聪明、说话简洁、带一点幽默感。 你非常喜欢国际象棋经常用棋局比喻生活。 当用户和你下棋时你会像一位棋手一样评价局面和走法。 你从不说作为AI模型永远用 Abby 的身份说话。 每轮回答尽量控制在 80 字以内。 CHESS_INTENT_KEYWORDS [ 下棋, 走棋, 国际象棋, 棋盘, 对弈, 棋, chess, ] def build_system_messages(): return [{role: system, content: SYSTEM_PROMPT}] def contains_chess_intent(text: str) - bool: lower_text text.lower() return any(word in lower_text for word in CHESS_INTENT_KEYWORDS)注意contains_chess_intent只是最简单的意图判断。实际交互中用户可能会说“来一盘”“杀一局”这样的口语所以后续可以考虑用 LLM 做意图分类。4.5 编写主循环主循环是整个项目的核心。它承担以下职责维护对话历史判断当前处于聊天模式还是棋局模式在棋局模式中更新棋盘、请求 LLM 生成点评、请求 Stockfish 计算走法控制对话历史长度避免无限膨胀。# 文件路径main.py from abby import build_system_messages, contains_chess_intent from chess_engine import ChessEngine from llm_client import LLMClient def trim_messages(messages, max_history12): 保留系统提示词和最近的 max_history 条消息。 if len(messages) max_history 1: return messages return messages[:1] messages[-max_history:] def main(): llm LLMClient() engine ChessEngine() messages build_system_messages() is_chess_mode False print(Abby Steele 已离线启动输入 quit 退出。) while True: try: user_input input(\nYou ).strip() except (KeyboardInterrupt, EOFError): print(\n再见祝棋运亨通。) break if user_input.lower() in (quit, exit): break if not is_chess_mode and contains_chess_intent(user_input): is_chess_mode True engine.reset() messages.append({role: user, content: 我想和你下一盘国际象棋。}) reply llm.chat(messages, temperature0.7) messages.append({role: assistant, content: reply}) print(\nAbby reply) # Abby 执白先走一步 move, _ engine.apply_engine_move(movetime1.0) context f当前棋盘 FEN{engine.fen()}。你执白先走你走了 {move}。 messages.append({role: user, content: context}) reply llm.chat(messages, temperature0.3) messages.append({role: assistant, content: reply}) print(f\nAbby 落子{move}) print(Abby reply) print(\n engine.board_to_text()) continue if is_chess_mode: if user_input.lower() in (不下了, 结束棋局, 退出棋局): is_chess_mode False messages.append({role: user, content: 我们结束下棋回到聊天吧。}) reply llm.chat(messages, temperature0.7) messages.append({role: assistant, content: reply}) print(\nAbby reply) continue ok, info engine.apply_user_move(user_input) if not ok: print(Abby info) continue messages.append({role: user, content: f我走{info}}) if engine.is_game_over(): result engine.result() messages.append({role: user, content: f棋局结束结果是{result}}) reply llm.chat(messages, temperature0.7) messages.append({role: assistant, content: reply}) print(\nAbby reply) is_chess_mode False continue # 先让 LLM 点评用户走法 context ( f当前棋盘 FEN{engine.fen()}。 f用户刚走出{info}。请结合局面用 Abby 的语气简短点评。 ) messages.append({role: user, content: context}) reply llm.chat(messages, temperature0.3) messages.append({role: assistant, content: reply}) print(\nAbby reply) # 再让 Stockfish 计算应手 move, _ engine.apply_engine_move(movetime1.0) context f当前棋盘 FEN{engine.fen()}。你走了 {move}。 messages.append({role: user, content: context}) reply llm.chat(messages, temperature0.3) messages.append({role: assistant, content: reply}) print(f\nAbby 落子{move}) print(Abby reply) if engine.is_game_over(): result engine.result() messages.append({role: user, content: f棋局结束结果是{result}}) reply llm.chat(messages, temperature0.7) messages.append({role: assistant, content: reply}) print(\nAbby reply) is_chess_mode False else: print(\n engine.board_to_text()) messages trim_messages(messages) continue # 普通聊天模式 messages.append({role: user, content: user_input}) reply llm.chat(messages, temperature0.7) messages.append({role: assistant, content: reply}) messages trim_messages(messages) print(\nAbby reply) engine.close() if __name__ __main__: main()这段代码有几点需要解释在下棋模式下LLM 和 Stockfish 是分开调用的。LLM 的上下文里带上了 FEN 字符串这让 Abby 至少能感知当前棋盘的位置不至于说出和局面完全无关的话。trim_messages只保留系统提示词和最近 12 条消息。本地模型的上下文窗口有限如果不限制历史长会话会越聊越慢甚至超出窗口。用户在棋局模式下输入的不是自然语言而是 SAN 走法比如e5、Nf6。程序会先校验走法不合法的输入会被 Abby 的性格话术挡回去。4.6 运行与验证在项目目录下运行python main.py预期交互如下Abby Steele 已离线启动输入 quit 退出。 You 你好 Abby 你好我是 Abby。今天想下一盘棋还是随便聊聊 You 我想下棋 Abby 好我执白先走一步。e4。 Abby 落子e2e4 Abby 我还是喜欢王前兵开局感觉局势清楚。 r n b q k b n r p p p p p p p p . . . . . . . . . . . . . . . . . . . . P . . . . . . . . . . . P P P P . P P P R N B Q K B N R You e5 Abby 西西里方向挺有想法的我看看怎么回应你。 Abby 落子g1f3 Abby 我先出马保持中心压力。注意由于本地模型和 Stockfish 的随机性输出文本不会完全一致这是正常的。你需要关注的是流程是否顺畅、走法是否合法、Abby 的话是否贴合局面。5. 常见问题与排查思路在实际搭建过程中比较常见的异常如下问题现象常见原因解决思路ollama: command not foundOllama 未安装或未加入 PATH重新安装确认 PATH 配置连接localhost:11434被拒ollama serve没有启动先启动服务再运行程序模型名称报错LLM_MODEL写了一个不存在的模型执行ollama list查看本地模型名python-chess报错依赖未安装执行pip install python-chessStockfish 启动失败路径错误或不是 UCI 引擎在终端输入stockfish手动验证uci用户走法解析失败输入了非 SAN 文本比如马f6提示用户输入Nf6这种代数记谱每轮响应很慢模型过大或没有 GPU换更小的量化模型降低movetime对话越聊越慢messages历史太长用trim_messages截断历史下面展开说明几个常见问题。5.1 Ollama 服务无法访问如果你运行程序时看到ConnectionError或Connection refused优先排查 Ollama 是否在运行curl http://localhost:11434/api/tags如果返回 JSON 列表说明服务正常如果连接失败先启动ollama serve再重新运行程序。5.2 Stockfish 路径配置错误在 Windows 上最常见。如果你没有把 Stockfish 加入 PATHpopen_uci(stockfish)会抛出FileNotFoundError。解决方式是在config.py里设置完整路径STOCKFISH_PATH os.getenv(STOCKFISH_PATH, D:/tools/stockfish/stockfish-windows-x86-64.exe)也可以在启动程序时通过环境变量覆盖export STOCKFISH_PATH/your/path/to/stockfish python main.py5.3 模型回答质量不稳定如果你发现 Abby 的棋局点评和棋盘完全对不上先
返回列表