语音指挥农夫自动寻路!用行空板 K10 + MCP + Three.js 打造一个 AI 3D 农场
语音指挥农夫自动寻路!用行空板 K10 + MCP + Three.js 打造一个 AI 3D 农场(创客社团教学实战)
本文同步记录了一个完整的创客教学项目:从 2D 到 3D,用一个网页把 MCP 协议调用、AI 语音对话、A 自动路径规划、WebGL 三维渲染* 串到了一起。项目可用于创客社团课堂演示,学生可以直接对着行空板 K10 说话,屏幕里的农夫就会自己绕开障碍、走到目标旁边。

一、项目背景:为什么要做这个项目
在创客社团的教学中,我一直在找一个能把几个"时髦但抽象"的知识点一次性讲清楚的载体:
- MCP 到底是什么,AI 是怎么"调用外部工具"的?
- AI 语音对话不只是聊天,能不能让它真正去"做事"?
- 游戏里的角色是怎么自动寻路、自动找目标的?
- 网页除了平面 UI,还能做真 3D 实时渲染吗?
如果分开讲,学生很难建立整体认知。于是我设计了"小智农场 MCP 控制台"这个项目:
- 屏幕里是一个 20 × 14 的农场,有农舍、谷仓、池塘、水渠、农田、树木、岩石,还有南瓜、绵羊、苹果树、母鸡、干草捆 5 个目标;
- 一个戴草帽的农夫站在农场大门;
- 学生对行空板 K10 说一句"让农夫去找南瓜",云端把语音意图转成 MCP 工具调用,网页收到后用 A 算法自动规划路径*,农夫就会绕开水渠和障碍,一步一步走到南瓜旁边,并冒出对话气泡。
整个过程没有任何人工点击,学生能亲眼看到"语音 → AI → 工具调用 → 路径规划 → 角色动作 → 结果回报"的完整闭环。
演示视频
二、整体方案与工作原理
先上一张系统架构图,理解数据是怎么流动的:
学生语音
│ "让农夫去找南瓜"
▼
┌──────────────┐ WebSocket ┌────────────────┐
│ 行空板 K10 │ ◀───────────▶ │ 小智 AI 云端 │
│ (小智固件) │ │ 语音识别/大模型 │
└──────────────┘ └────────┬───────┘
│ MCP (JSON-RPC 2.0)
│ initialize / tools/list / tools/call
▼
┌────────────────────────┐
│ 网页(MCP Server) │
│ 7 个农场工具 + A* 寻路 │
│ Three.js 3D 渲染 + 动画 │
└────────────────────────┘
这里有一个反直觉但非常关键的设计点,也是课堂上我重点强调的:
网页并不是 MCP 客户端,而是扮演 MCP Server(工具提供方)。网页主动连小智的 WebSocket 接入点后,是小智云端反过来向网页发起
initialize握手、tools/list拉取工具清单、tools/call调用工具。网页把"移动农夫"的能力作为工具暴露出去,AI 才能调用它。
技术选型上刻意保持简单,方便教学和学生课后复现:
- 纯原生 HTML + CSS + JavaScript,不使用任何框架、不做构建、不用 npm,浏览器直接双击打开即可运行;
- 所有第三方库(Three.js、OrbitControls)都下载到本地
lib目录,离线可用,教室网络不稳定也不影响; - 采用普通
<script>标签(非 ES Module),对初学者更友好。
工程目录结构如下:
xiaozhi-farm/
├─ index.html # 唯一入口
├─ css/styles.css # 三栏布局、木纹相框风格
├─ js/
│ ├─ map.js # 地图数据、实体、目标、邻格计算
│ ├─ pathfinder.js # A* 寻路算法
│ ├─ farm.js # 2D/3D 渲染与角色动画(两次迭代的核心)
│ ├─ mcp.js # MCP 服务端行为、握手、心跳、重连
│ └─ app.js # 7 个行为实现、对话、指令解析
├─ lib/ # three.min.js、OrbitControls.js(3D 阶段新增)
└─ assets/ # 2D 阶段的 10 张棋子图片

三、材料清单
| 名称 | 说明 | 数量 |
|---|---|---|
| 行空板 K10 | 集成 ESP32-S3、2.8 寸 LCD、双麦克风、扬声器、摄像头、光感、温湿度 | 1 |
| USB-C 数据线 | 用于烧录和供电 | 1 |
| Windows 电脑 | 推荐 Win10/11,需能上网 | 1 |
行空板 K10 是 DFRobot 专为 AI 教育设计的高集成开发板,100% 采用国产芯片,主要规格:
- MCU:ESP32-S3,32 位双核 240MHz;
- 2.8 英寸彩色 LCD,板载摄像头、双麦克风、扬声器、数字环境光与温湿度传感器;
- 支持 2.4G Wi-Fi、蓝牙 5.0,支持 MicroPython 和 Mind+ 图形化编程;
- 原生支持 MCP、DeepSeek、边缘计算,非常适合本项目。

第一阶段:先做一个能跑通的 2D 版本
我没有一上来就追求 3D,而是先用 Canvas 2D 把整条业务链路跑通。这也是教学上的建议——先验证逻辑,再升级表现层,能把问题分层、降低排错难度。

四、地图与数据结构设计
地图是 20 列 × 14 行,用一串字符来描述地形,每种字符对应不同的可通行性和移动代价:
| 字符 | 地形 | 通过代价 | 说明 |
|---|---|---|---|
. |
草地 | 1 | 普通地面 |
R |
土路 | 1 | 大门道路 |
F |
农田 | 3 | 可走但更耗时(泥泞) |
W |
水渠 | 0(阻挡) | 不可通行 |
P |
池塘 | 0(阻挡) | 不可通行 |
B |
建筑位 | 0(阻挡) | 农舍/谷仓占地 |
T |
障碍位 | 0(阻挡) | 树木/岩石占地 |
地图地形布局:
var TERRAIN_ROWS = [
"....................",
"....................",
"..BB...........BB...",
"..BB...........BB...",
"....................",
"....................",
"..T.................",
"......WWWWWWWW......",
".......FFFFFFF......",
"..PP...FFFFFFF......",
"..PP...FFFFFFF......",
"....................",
"......T......TT.....",
"..........R........."
];
var COST = { ".": 1, "R": 1, "F": 3, "W": 0, "P": 0, "B": 0, "T": 0 };
除了地形阻挡,实体(如南瓜、绵羊这些本身占据格子的物体)也会被加进一张"动态阻挡表" EXTRA_BLOCKED,农夫同样不能穿过它们。
农夫的起点设在大门 (10, 13),5 个目标和 6 个地名到达点都在数据里写死,方便检索。
2D 阶段的 10 张棋子图片(农夫、绵羊、母鸡、南瓜、苹果树、干草捆、谷仓、农舍、大树、岩石)用 AI 生成为统一的等距风格,Canvas 负责把它们绘制到对应格子上。

角色图片:


五、核心算法:A* 自动路径规划
这是整个项目最"硬核"的部分。我让学生手写了一个 A(A-Star)算法*,而不是直接调用现成库。
5.1 A* 的基本思想
A* 是一种启发式搜索算法,每个格子用一个综合评分 f 决定优先探索谁:
f = g + h
g:从起点走到当前格子已经付出的真实代价(考虑地形,农田一格代价 3);h:从当前格子到终点的估计代价(启发值),这里用曼哈顿距离;- 每次优先扩展
f最小的格子,直到找到终点,再沿"父节点"回溯出完整路径。
因为只允许上下左右移动,启发函数用曼哈顿距离:
var h = Math.abs(nc - goalCol) + Math.abs(nr - goalRow);
5.2 用最小二叉堆提升性能
为了高效地每次取出 f 最小的节点,我实现了一个最小二叉堆(而不是每次都遍历整个数组排序),这也是一个很好的数据结构教学点:
MinHeap.prototype.push = function (node) {
var a = this.a, i = a.length;
a.push(node);
while (i > 0) {
var parent = (i - 1) >> 1;
if (a[parent].f <= a[i]) { break; }
var tmp = a[parent]; a[parent] = a[i]; a[i] = tmp; // 上浮
i = parent;
}
};
5.3 带地形代价的主循环
主函数返回一条由 {col, row} 组成的路径,不可达则返回 null。核心扩展逻辑:
for (var i = 0; i < 4; i++) {
var nc = current.c + DIRS[i][0], nr = current.r + DIRS[i][1];
if (!Map.isWalkable(nc, nr)) { continue; } // 阻挡格跳过
var step = Map.tileCost(nc); // 农田代价 3
var tentative = gScore[ck] + step;
if (gScore[nk] === undefined || tentative < gScore[nk]) {
gScore[nk] = tentative;
came[nk] = ck; // 记录父节点用于回溯
var h = Math.abs(nc - goalCol) + Math.abs(nr - goalRow);
open.push({ c: nc, r: nr, f: tentative + h });
}
}
5.4 一个真实的坑:目标格本身是阻挡的
这是 2D 阶段最有价值的一次"踩坑",强烈建议在课堂上让学生先自己踩一遍。
问题现象:调用 find_target 找南瓜时,A* 直接返回"不可达"。
原因分析:南瓜所在的格子被设成了阻挡(农夫不能站到南瓜身上),而最初的逻辑把"目标格本身"当成了路径终点。A* 发现终点不可走,自然判定不可达。
修复思路:农夫不需要站到目标上,只要走到目标旁边就可以。于是:
- 在
map.js增加approachPoints(),返回目标上、下、左、右四个邻居中可通行的格子; - 在
app.js增加approachMove(),对每个候选邻格分别算路径,用findNearest选出路径最短的那个邻格走过去。
function approachPoints(col, row) {
var result = [];
[[1, 0], [-1, 0], [0, 1], [0, -1]].forEach(function (d) {
var nc = col + d[0], nr = row + d[1];
if (isInside(nc, nr) && isWalkable(nc, nr) && !EXTRA_BLOCKED[nc + "," + nr]) {
result.push({ col: nc, row: nr });
}
});
return result;
}
"巡逻"功能就是复用这个逻辑:对 5 个目标依次异步执行 approachMove,农夫会一个接一个地走到每个目标旁边。
这个 bug 的教学意义在于:真实项目里,数据约束和算法假设往往对不上,需要在两者之间做适配。
六、MCP 网页:把"移动农夫"注册成 7 个工具
6.1 七个农场行为
我在网页里实现了 7 个行为,每个都对应一个带标准 JSON Schema 的 MCP 工具:
| 工具名 | 功能 | 主要参数 |
|---|---|---|
goto_place |
按地名前往(农舍/谷仓/池塘/麦田/水渠/大门) | place |
goto_coordinate |
按网格坐标前往 | x、y |
find_target |
自动查找目标并走到旁边 | target |
patrol |
自动巡逻,依次访问全部 5 个目标 | 无 |
stop |
停止当前移动 | 无 |
get_status |
获取农夫位置与农场状态 | 无 |
say |
让农夫说话(气泡+对话面板) | text |
工具定义里用 inputSchema 描述参数,AI 才知道该怎么填:
{
name: "goto_place",
description: "指挥农场中的农夫前往指定地点,自动规划路径并沿路径走到目标。",
inputSchema: {
type: "object",
properties: {
place: { type: "string", enum: ["农舍", "谷仓", "池塘", "麦田", "水渠", "大门"] }
},
required: ["place"]
}
}
6.2 MCP 握手与消息分发
网页连上 WebSocket 后,按 MCP 协议(版本 2024-11-05,基于 JSON-RPC 2.0)处理云端的请求:
- 收到
initialize→ 返回协议版本、能力和服务端信息; - 收到
notifications/initialized→ 握手完成,进入就绪状态; - 收到
tools/list→ 返回 7 个工具的完整定义; - 收到
tools/call→ 根据工具名找到对应的异步处理函数执行,再把结果通过 JSON-RPC 回传。
if (msg.method === "initialize") { this.handleInitialize(msg); return; }
if (msg.method === "tools/list") { this.handleToolList(msg); return; }
if (msg.method === "tools/call") { this.handleToolCall(msg); return; }
此外还实现了心跳保活(每 30 秒发一次 ping)和断线自动重连(指数退避,最多 30 秒),保证长时间演示的稳定性。
6.3 移动的节奏控制
农夫在格子间移动时做了平滑插值,而不是生硬地"跳格"。并按地形区分耗时:
- 普通格(草地/土路):约 250ms 走一格;
- 农田格:约 470ms 走一格,体现"泥泞走得慢"。
新指令到来时,如果农夫正在移动,会先 stop 旧任务再接管,避免动作冲突。
行空板 K10 小智配置(硬件环节)
2D 逻辑跑通后,就可以配置硬件,让学生用语音来驱动了。
七、刷入小智固件
小智 AI 来自开源项目 xiaozhi-esp32(GitHub:https://github.com/78/xiaozhi-esp32)。官方为行空板 K10 提供了免编译固件,直接用烧录工具刷写即可。
- 长按 K10 背面的 Boot 键,同时用 USB-C 连接电脑;
- 打开 Flash Download Tool,芯片类型选
ESP32-S3,工作模式Develop,加载模式UART; - 选择解压出的
.bin固件,起始地址填0x00,勾选对应行; - 选择正确的 COM 口,波特率设为
1152000; - 先点 ERASE 擦除原有固件,完成后再点 START 开始烧录。

八、配网与设备激活
- 烧录完成后按一下背面 RST 键重启,K10 会进入 Wi-Fi 配置模式,屏幕显示热点名称和配置地址(
192.168.4.1); - 用电脑或手机连接 K10 的热点,在弹出的配置页面填入教室 Wi-Fi 的 SSID 和密码(注意:K10 只支持 2.4GHz Wi-Fi);
- 连接成功后 K10 自动重启,屏幕显示一个 6 位设备验证码;
- 浏览器打开
xiaozhi.me,用手机号注册并进入控制台,点击「添加设备」,输入屏幕上的 6 位验证码完成绑定; - 在「配置角色」中可以选择角色音色、语言模型(如 Qwen 实时、DeepSeek V3)和角色介绍,保存后重启设备生效。
九、获取 MCP 接入点并连接
这是把硬件和网页连起来的最后一步:
- 在小智控制台进入智能体的「配置 → 扩展能力 → MCP 接入点」,复制 WebSocket 地址,格式形如:
wss://api.xiaozhi.me/mcp/?token=你的专属令牌; - 把这个地址粘贴到网页左侧的输入框,点击「连接并注册行为」;
- 当状态显示「就绪」、工具卡片圆点变绿,说明 7 个农场行为已经注册成功。
之后用唤醒词「你好,小智」唤醒 K10,就可以直接语音对话了。板载两个按键:A 键短按可中断/唤醒、长按调音量加;B 键短按中断/唤醒、长按调音量减。
第二阶段:升级为真 3D,让角色动起来
逻辑和硬件都验证完毕后,第二阶段专注表现层升级:用 Three.js 把 Canvas 2D 替换成真正的 WebGL 三维场景,并给角色加上丰富的动态效果。
十、Three.js 本地 3D 场景
10.1 为什么选 Three.js r128
我选用 Three.js r128,原因是这个版本提供非模块的 UMD 构建(three.min.js),可以直接用普通 <script> 引入,和现有工程结构完全一致,不需要 importmap 或打包工具。
index.html 中把库放在 farm.js 之前加载:
<script src="lib/three.min.js"></script>
<script src="lib/OrbitControls.js"></script>
10.2 坐标系统与场景搭建
3D 世界里每格对应 1 个单位,y 轴向上,网格坐标和世界坐标互相转换:
function wx(col) { return col - COLS / 2 + 0.5; }
function wz(row) { return row - ROWS / 2 + 0.5; }
场景包含:
- 透视相机斜俯视,配合 OrbitControls 实现拖拽旋转、滚轮缩放,并限制极角(不会转到地面以下)、开启阻尼;
- 灯光:半球光 + 环境光 + 带阴影的平行光(阴影贴图 2048),地面接收阴影、物体投射阴影,立体感很强;
- 地形:每格一个薄方块,按地形用不同材质,农田加垄沟、水面半透明,再点缀小草和小花;
- 实体全部用几何体搭建,不再依赖图片贴图:农舍/谷仓用墙体 Box + 四棱锥坡屋顶,大树用圆柱树干 + 多面体树冠,岩石用十二面体,南瓜用压扁的球体加棱,干草捆用横放圆柱加绳环……
10.3 保持接口不变,渲染层无缝替换
这是工程上的一个关键设计:重写 farm.js 内部实现,但对外的方法签名全部保留(start、moveAlong、stop、setBubble、markTarget、getFarmer 等),所以 app.js 的业务逻辑几乎不用改,大大降低了升级风险。
十一、角色动态效果(本项目的亮点)
3D 农夫是一个组合 Group,由腿、身体、手臂、头、草帽等部件组成,每个可动部件都保留引用,用一个 walkPhase 变量驱动动画。
11.1 行走动画
移动时 walkPhase 随时间累积,双腿、双臂绕关节用正弦函数反相摆动,身体随步伐上下弹跳:
u.walkPhase += dt * 0.013;
var p = u.walkPhase;
u.legL.rotation.x = Math.sin(p) * 0.85;
u.legR.rotation.x = -Math.sin(p) * 0.85;
u.armL.rotation.x = -Math.sin(p) * 0.7;
u.armR.rotation.x = Math.sin(p) * 0.7;
u.inner.position.y = Math.abs(Math.sin(p)) * 0.12; // 步伐弹跳
11.2 平滑转向
移动时根据格子间的方向计算目标朝向,再用最短角度差做插值,农夫会自然地转身而不是瞬间换向:
var targetYaw = Math.atan2(dx, dz);
root.rotation.y = lerpAngle(root.rotation.y, targetYaw, 0.18);
11.3 待机与动物动画
- 农夫待机时有轻微的呼吸浮动,四肢缓缓回正;
- 母鸡会周期性低头啄米,绵羊慢慢晃头,树木和树冠随微风摇摆;
- 路径用白色路径点 + 一个红色流动光点沿路径循环移动,目标用脉冲圆环标记;
- 对话气泡用 Canvas 生成纹理、以 Sprite 跟随农夫头顶,始终面向相机、不被遮挡。
这些动态效果我在验证时通过实时读取关节角度确认:移动中腿臂角度在 ±0.85 之间持续变化、身体弹跳高度最高约 0.12,数据和画面一致。

十二、迭代中的两个小需求
课堂演示后又根据反馈做了两处实用迭代,这里一并记录:
- 苹果树上要真的结苹果:最初苹果被埋在树冠球体内部看不见。我把单个树冠改成三个错落的叶球留出缝隙,把苹果增加到 10 个,分三层长到树冠外表面,并用带高光的鲜红材质,从任何角度都能清楚看到果实。
- 农场场景全屏显示:用标准 Fullscreen API(兼容 webkit 前缀)给地图框加了全屏按钮,点击后场景以 1920×1080 填满整个屏幕、去掉边框和侧栏,方便在投影仪上做沉浸式演示,按
Esc或再点按钮即可退出。
十三、课堂教学应用
这个项目在创客社团里的实际用法非常灵活:
- 演示模式:全屏投影,老师或学生语音下达指令,大家一起观察农夫如何规划路径、绕开障碍;
- 互动提问:让学生预测"农夫会走哪条路""为什么农田走得慢""目标挡住了怎么办",再实际验证;
- 分层拓展任务:
- 入门:修改地图字符,增加一堵墙或一条水渠,观察寻路变化;
- 进阶:新增一个目标实体和对应的 MCP 工具;
- 挑战:优化 A*(如支持斜向移动、加入更多地形代价),或给角色设计新的动画。
一个项目就能覆盖数据结构(二叉堆)、算法(A* 搜索)、网络协议(MCP/JSON-RPC/WebSocket)、人工智能(语音+大模型)、计算机图形学(WebGL/3D) 五大模块,知识点密度很高,又足够有趣,学生参与度明显比纯讲理论高。
十四、总结与展望
回顾整个制作过程,最大的体会是分层推进的价值:
- 先用 2D 跑通 MCP + A* + 业务逻辑,把"目标格阻挡"这类逻辑问题解决掉;
- 再配置行空板 K10 小智固件,打通语音链路;
- 最后用 Three.js 无缝升级 3D 表现层和角色动画。
每一步都可独立验证,出了问题能快速定位,这对教学和初学者尤其重要。
后续还可以继续拓展的方向:
- 接入板载摄像头,让 AI 结合视觉识别来下判断;
- 加入"种植、浇水、收获"等可改变地图状态的行为,形成完整的经营玩法;
- 支持多角色、多设备协同;
- 把场景对接到 VR/AR,进一步提升沉浸感。
希望这篇教程能给同样在做 AI 创客教学的老师和同学一些启发。代码已整理,欢迎大家在 DF 创客社区交流、复现和二次创作!
附:参考资源
- 小智 AI 开源项目:
https://github.com/78/xiaozhi-esp32 - 小智官网与控制台:
https://xiaozhi.me - 行空板 K10 小智配置官方教程:行空板官网 Wiki(搜索"K10 小智 AI")
- Three.js 官网:
https://threejs.org



