云天 AI 小镇:用 EasyIoT +行空板K10,驱动一座“会生活”的数字家庭小镇
斯坦福 AI 小镇(Smallville)式的生成式智能体小镇,被我用 DFRobot EasyIoT 平台的三个环境传感器(温度、湿度、光照)“接上了真实现实”——这些数据由行空板 K10(Mind+ 2.0 编程,自带温湿度与环境光传感器)实时采集。两户人家的六口人住进小镇,他们的穿衣、喝水、开灯、作息、对话和记忆,全部由实时环境数据 + 系统时间 + 大模型自主决定。
一、灵感:为什么做一个“AI 小镇”
去年看斯坦福的《生成式智能体》(Generative Agents)论文,里面 25 个 AI 小人住在一个叫 Smallville 的小镇上:他们早上按计划起床、上班、做饭,会约朋友喝咖啡,记得昨天聊过什么,还会在圣诞派对上互相邀约……让我特别着迷的不是“他们像人”,而是 记忆 和 社交 这两套机制让世界真正“活”了起来。
于是我想:能不能把小镇接到真实世界里?DFRobot 的 EasyIoT 平台正好提供了温度、湿度、光照三个环境数据通道,而行空板 K10 一块板子就自带了这三个传感器的采集能力(板载 AHT20 温湿度 + ALS 环境光传感器,详见后文 3.1 节)。我把三者接进小镇——高温了就少穿、干燥了就多喝水、天黑了就开灯。小镇不再只是代码里的一串幻觉,而是和窗外天气同步的生活场景。
项目取名“云天 AI 小镇”:云山县·云天镇·桂花苑小区,两家住户,六位成员,全部由大模型自主决策。
二、系统长什么样
浏览器网页(小镇平面图 + 成员卡片 + 生活日志 + 对话气泡 + 记忆数)
▲ WebSocket 实时推送(环境消息 / 状态帧)
│
本地桥接 server.js(Node.js)
├─ MQTT 订阅 ──► DFRobot EasyIoT(iot.dfrobot.com.cn:1883)
│ 温度 z4ksqL6Ig / 湿度 74F2EvvNg / 光照 XZzRM3OHR
├─ /api/ai 转发 ──► SiliconFlow(DeepSeek-V3)大模型
└─ HTTP 静态服务(网页本体 + 历史缓存 + 调试发送)

三个 EasyIoT 消息是谁发的都行:本文用行空板 K10(自带温湿度 + 环境光传感器,见 3.1 节)往 Topic 一发,小镇立刻感应到;换成任意 ESP32/Arduino + 传感器也一样。没传感器时,网页还有“演示模式”,拖动三个滑块就能模拟酷暑、严寒、大暴雨、深夜,随时看小镇怎么反应。


三、数据接入:EasyIoT 的三种身份
做之前先把 EasyIoT 的接口摸了个透:
| 能力 | 方式 | 说明 |
|---|---|---|
| MQTT 订阅 | iot.dfrobot.com.cn:1883 |
用户名/密码用平台控制台给的 Iot_id / Iot_pwd,Topic 即设备 ID |
| REST 轮询 | POST api.dfrobot.work/easyiot/apiv3/messages/search |
官方网页本身就是轮询,需要登录 Token |
| 浏览器直连 | 不支持 | 平台没有 MQTT-over-WebSocket 端口,浏览器无法直接订阅 |
所以网页不能直接收 EasyIoT——这也催生了“本地桥接”这一层:server.js 用 MQTT 订阅三个 Topic,把每条消息通过 WebSocket 实时推给浏览器;页面也可以走 REST 轮询作备用(Token 手动填或自动获取)。
温度主题、湿度主题、光照主题数据层做了噪声过滤:只认数值(或 JSON 里的数值),文本这类消息静默忽略,不刷屏不污染环境状态。
3.1 硬件端:行空板 K10 + Mind+ 2.0
数据从哪里来?我用的是 DFRobot 行空板 K10——这块主控板自带了三路环境感知:AHT20 温湿度传感器、板载 ALS 环境光传感器,还有一个可编程 RGB 灯,一颗板子正好把温度/湿度/光照三个数据源集齐了。采用 Mind+ 2.0 图形化编程(代码模式),程序逻辑很直接:
- 连 WiFi;
- 初始化 EasyIoT(
iot.dfrobot.com.cn:1883,用控制台分配的 Iot_id / Iot_pwd); - 每秒读一次三个传感器;
- 变化超过阈值才发布(温度 ±0.3℃、湿度 ±10%、光照 ±100)——不掉线的场景下既省流量又不让 IoT 后台刷屏;
- 每发一路数据,RGB 灯闪一下对应颜色(温度红/湿度绿/光照蓝),肉眼可见“数据在飞”。
/*!
* MindPlus 2.0 生成 · DFRobot 行空板 K10
* 每秒采样三路传感器,变化超过阈值发布到 EasyIoT 三个 Topic
*/
#include <DFRobot_Iot.h>
#include "unihiker_k10.h"
// 动态变量
volatile float mind_n_WenDu, mind_n_GuangZhao, mind_n_HuoQuShuJu, mind_n_ShiDu;
// 静态常量(topic 数组由发布时显式指定,这里留空占位)
const String topics[] = {"", "", "", "", ""};
// 创建对象
DFRobot_Iot myIot;
UNIHIKER_K10 k10;
AHT20 aht20;
void setup() {
k10.begin();
myIot.wifiConnect("你的WiFi名称", "你的WiFi密码"); // ← 替换为实际 WiFi
while (!myIot.wifiStatus()) {}
myIot.init("iot.dfrobot.com.cn", "你的Iot_id", "你的Iot_pwd", topics, 1883); // ← 替换为控制台凭据
myIot.connect();
while (!myIot.connected()) {}
mind_n_WenDu = 0;
mind_n_GuangZhao = 0;
mind_n_ShiDu = 0;
Serial.begin(9600);
}
void loop() {
// --- 温度:变化 ≥ 0.3℃ 才发布 ---
mind_n_HuoQuShuJu = aht20.getData(AHT20::eAHT20TempC);
if (abs(mind_n_HuoQuShuJu - mind_n_WenDu) >= 0.3) {
mind_n_WenDu = mind_n_HuoQuShuJu;
myIot.publish("z4ksqL6Ig", mind_n_HuoQuShuJu); // 温度 Topic
k10.rgb->write(-1, 0xFF0000); delay(500); k10.rgb->write(-1, 0x000000); delay(200);
}
// --- 湿度:变化 ≥ 10% 才发布 ---
mind_n_HuoQuShuJu = aht20.getData(AHT20::eAHT20HumiRH);
if (abs(mind_n_HuoQuShuJu - mind_n_ShiDu) >= 10) {
mind_n_ShiDu = mind_n_HuoQuShuJu;
myIot.publish("74F2EvvNg", mind_n_HuoQuShuJu); // 湿度 Topic
k10.rgb->write(-1, 0x00FF00); delay(500); k10.rgb->write(-1, 0x000000); delay(200);
}
// --- 光照:变化 ≥ 100 才发布 ---
mind_n_HuoQuShuJu = k10.readALS();
if (abs(mind_n_HuoQuShuJu - mind_n_GuangZhao) >= 100) {
mind_n_GuangZhao = mind_n_HuoQuShuJu;
myIot.publish("XZzRM3OHR", mind_n_HuoQuShuJu); // 光照 Topic
k10.rgb->write(-1, 0x0000FF); delay(500); k10.rgb->write(-1, 0x000000); delay(200);
}
delay(1000);
}

与小镇联动的三个核对点:
| 硬件侧 | 小镇侧 |
|---|---|
| 发布的就是浮点数转字符串(如 "25.6") | 数据层 parseMessage 识别纯数字 |
| 三个 Topic 与 config.json 一致 | WS 帧按 topic 映射到温度/湿度/光照 |
| 光照夜间读数可能升高(灯光/路灯) | 小镇"时间优先":夜晚不显示阳光充足 |
两个小建议:① 湿度阈值 ±10 偏大,想看到更灵敏的“喝水提醒”可以改成 ±3;② 发布用
myIot.publish(topic, 数值)的纯数字格式最省事,如果以后想发 JSON(比如含电池电量),小镇解析层也能兼容。
四、小镇居民:两户人家六口人
桂花苑 3 栋 502 · 王家
| 成员 | 身份 |
|---|---|
| 👨💼 王建国 38 岁 | 县住建局公务员 |
| 👩🏫 李秀兰 36 岁 | 县一中高中英语老师 |
| 👧 王小朵 9 岁 | 实验小学三年级(2 班) |
桂花苑 5 栋 301 · 陈家
| 成员 | 身份 |
|---|---|
| 👨🚒 陈海峰 40 岁 | 县消防大队消防员(住得离单位近,5 分钟出警) |
| 👩⚕️ 宋雪梅 37 岁 | 县人民医院护士长 |
| 🧒 陈果果 9 岁 | 实验小学三年级(2 班),和王小朵同班同桌 |
两家女孩是同班同学——上学结伴、放学同行、课间跳绳,这些“交集”都会出现在小镇里。

五、环境决定生活:三个传感器怎么“指挥”家庭
| 数据 | 决定 | 举例 |
|---|---|---|
| 🌡️ 温度 | 全家穿衣 / 出行安排 | ≥35℃ 背心短裤躲空调房;<5℃ 羽绒服加围巾、减少外出 |
| 💧 湿度 | 喝水频次 | <30% 干燥:定时提醒“口干,去接杯水”,饮水杯数实时累计;>70% 开窗防潮 |
| 💡 光照 | 昼夜与开灯 | 时间或光照判黑 → 客厅灯亮;全家入睡 → 灯灭(人睡灯灭,这是合理性检查修出来的规则) |
光照还有一个“防呆”:时间优先——晚上哪怕传感器读数飙到 1200,也只会显示“夜晚(光照读数升高为灯光等非阳光)”,绝不会说“阳光充足”。
六、合理性:不是演戏,是过日子
这套系统最花功夫的不是“画得好看”,而是把逻辑做合理。用户提的好几条意见我都改成了规则:
- 走街道,不穿楼:小镇是 2D 平面图(县政府、住建局、县一中、实验小学、消防大队、人民医院、菜市场、云山公园、中心广场,两条横路两条竖路四个路口)。出门的人用 Dijkstra 最短路沿街道走,蓝色虚线画出路线。
- 行程时间 = 路程 ÷ 交通工具:王爸骑电动车 4 分钟到住建局、两女孩步行 14 分钟到学校(陈家 11 分钟,近一点)、陈爸摩托车 1 分钟到消防大队。时刻表里的“到岗时间”是推算出来的(07:40 出门 → 07:44 到岗),不是写死的。
- 同场不重叠:每个场所配“座位点阵”,同一地点的人自动分散站位。
- 对话要讲“相遇合理性”:一家人只在同场(家里/公园一起)才聊;爸爸在单位、女儿在学校——不聊;两家人路上相遇、公共场合偶遇、到对方单位办事(比如陈妈受邀去县一中讲急救知识,碰见王妈)——可以聊。
- 学生规矩:两个小学生在学校上课期间只能“讨论学习问题”(AI 会被明确提示“课上不许聊闲天”),课间、午休、放学路上才正常聊天。
- 称呼前后一致:每个成员都有固定的“称谓表”——妈妈永远叫自己女儿“闺女/小朵”、叫邻居家女孩“果果”,爸爸、陈爸陈妈同理。AI 提示里强制“前后保持一致,不要混用”,杜绝了实测里出现过的“一会叫闺女、一会叫儿子”的硬伤。
- 一件事做完不重复:同一时间段内,已经做过的事(量体温、提醒喝水、关窗这类)AI 不会再做第二遍——动作相似度检测会拦截重复记录,提示里也会写明“最近已完成:……”。
- 话题连贯:一次对话锁定一个话题聊深聊透(“家里近况”“天气穿衣”“小区趣事”等话题池),聊完自然收尾;下一轮对话自动换一个话题,不重复不东一句西一句。有关联的事(比如从天气聊到穿衣)可以自然接下去。
七、AI 小镇三件套:计划 / 对话 / 记忆
这部分基本照搬了斯坦福 Smallville 的机制,落地成规则 + 大模型:
- 计划与行动:每人每天自动生成“今日计划”(清晨/白天/晚间三段),显示在卡片上;环境剧变(气温从 25℃ 跳到 38℃)时,日志会提示“今日计划灵活调整:户外改室内”。
- 对话状态机:一方发出邀请 → 对方接受(70%)或婉拒 → 两人互相走近 → 开始 LLM 交替聊天,最多 8 条消息或 10 分钟 → 道别结束。对话提示里包含:发言者身份、当前计划、对方身份、以及最多 3 条最相关记忆。
- 记忆:聊天结束后,LLM 为每人各自生成第一人称摘要 + 重要性评分(0-9),向量化(2-3 gram 哈希袋,256 维)存入记忆库;下次对话前用复合评分检索 Top 3:
分数 = 相关性(余弦相似度) × 时近性(每小时 0.99 指数衰减) × 重要性(LLM 评分) - 单一操作约束:每人同一时刻只能做一件事——对话中的人不会并行“走回家”,路上的人不搭话,睡觉不打扰。保证了行为逻辑永远清晰。
八、关键实现片段
桥接服务核心(server.js)——MQTT 订阅 → WS 推送:
mq.on('message', (topic, payload) => {
history.push({ topic, ts: Date.now(), payload: payload.toString() });
broadcast({ type: 'message', message: history[history.length - 1] });
});
AI 对话的提示构造(对话状态机内):
系统:你是王建国,住建局公务员……说话一口家常话,简短自然,只输出你要说的那句话。
用户:你正和女儿王小朵在家聊天。刚才的对话:…你记得的相关往事:…
现在轮到你说一句。
记忆检索的复合评分:
score = cosSim(记忆向量, 查询向量) * Math.pow(0.99, 经过的小时数) * 重要性
九、怎么运行
项目全部在本目录(easyiot-dashboard/),零前端依赖,单文件网页。
cd easyiot-dashboard
npm install # 首次
node server.js # 或双击 auto.bat

浏览器打开 http://localhost:8080:
- 填好
config.json:MQTT 的 Iot_id / Iot_pwd / 三个传感器 Topic(页面也可直接改); - 网页自动进入“桥接实时”模式,MQTT 消息经 WebSocket 实时上屏;
- 没有设备?打开“演示模式”,拖温度/湿度/光照滑块,小镇立刻“变天”;
- 想验证连通性?控制台有“调试发送”,一条消息发到 Topic 上,1 秒内小镇就能收到。



十、实测效果
- 三个 Topic 全部订阅成功、实时收到消息:温度主题、湿度主题、光照主题——非数值全部静默过滤,环境面板只认数值。
- AI 真实决策示例(DeepSeek-V3):深夜降雨场景下,消防员陈海峰自主输出
{"action":"关窗防止雨水潲入","speech":"天气预报说今晚有大雨,得赶紧把窗户关好","mood":"警惕"}—— 与环境严丝合缝。 - 全流程自动测试:日程调度、照明合理性、街道寻路、动态到达、对话状态机(8 条上限/10 分钟)、记忆生成与检索、单一操作约束,共 50+ 项断言全部通过。

十一、可以怎么玩下去
- 接真实硬件:行空板 K10 开箱即用(自带温湿度+环境光,3.1 节代码直接烧录);可增加空气质量传感器、紫外线传感器等定时发数据,小镇就是你自己家的“镜像居民”;
- 加成员/加小镇:地图坐标、日程表、AI 人设都是数据化的,扩展一个家庭只需要加一份配置;
- 语音/微信通知:把小镇日志推到手机,出门前看一眼“镇民们穿了几件衣服”;
- 多 Agent 任务:比如让“邻居”给“你家”送菜、送作业,用现有记忆与对话机制就能编排剧情。
十二、开发花絮:一整座小镇是“对话”出来的
最后聊一件挺有意思的事:这个网页(以及整套小镇引擎)并不是一句一句敲出来的,而是作者用自然语言跟 Kimi Code(AI 编程助手)一段段对话“写”出来的。整个过程很像在“养”小镇:
- 先提出需求:“做一面网页,能接收 EasyIoT 三个主题的数据”;
- 再逐步细化:“把小镇画成平面图,两户人家,成员在地图上沿街道走”“行程时间按路程和交通工具算”“加上对话状态机、记忆检索、单一操作约束”“角色在家、上班、上学时位置要显示对”“晚上光照读数高别说阳光充足”“称呼前后一致、一件事不重复做、话题要聊连贯”……

每一轮需求,Kimi Code 都会先检查现状、给出修改方案,我确认后落地代码,再自动跑冒烟测试验证逻辑(光对话质量这一轮的测试就有 11 项断言)。代码量约 1700 行 JavaScript,全部在对话中迭代完成——这也算是生成式智能体项目的一种“自指”:小镇里的居民由 AI 扮演,小镇本身也是由 AI 帮着写出来的。
小镇居民“开口说话”用的大模型是硅基流动(SiliconFlow)平台的 DeepSeek-V3,通过本地桥接的 /api/ai 接口转发调用;API Key 只保存在本地 config.json,网页端不接触密钥。

注册赠代金券:https://cloud.siliconflow.cn/i/KwyEBX3e
安全保障提醒:config.json 里保存了 MQTT 凭据和 AI API Key,切勿上传到公开仓库或论坛附件。API Key 可以在 SiliconFlow 控制台随时吊销重置。
本文基于真实项目整理,代码与完整功能位于本地 easyiot-dashboard/ 目录(node + 无前端依赖)。






