基于 MCU 的 Agent(二)

上一篇主要讲的是 ESPAgent 如何把 LLM、工具调用和硬件驱动接起来。那时的重点还是单块 ESP32-S3:用户从飞书发来消息,固件把消息送进 Agent loop,LLM 判断是否需要调用工具,最后由工具层去控制 GPIO、RGB 灯、舵机或者读取传感器。

这几天项目变化比较大。ESPAgent 已经不是“单板上跑一个会调用硬件工具的聊天机器人”这么简单了。现在的主线变成了:四块 ESP32-S3 组成轻量 Agent Mesh,ESP32-P4+C6 和 Android 作为显示/调试终端,Coordinator 负责理解和调度,Sensor 负责感知,Control 负责执行,Guardian 负责权限、审计和数据治理。

更关键的是,链路里补上了几个之前缺失的概念:结构化 OutputMessage、异步结果回注、Guardian policy gate、Control command queue,以及每块板都能使用的 capability/Lua runtime。也就是说,Coordinator 不只是把命令发出去,然后告诉用户“我已经发送了”;它现在可以先请求 Guardian 裁决,等待远端节点的结构化执行结果,再把这个结果注入回 Agent loop,让 LLM 继续总结给用户。这才更接近一个真正的 ReAct 闭环。

这篇文章就按当前最新进展重新梳理:四个 ESP32-S3 现在到底怎么分工,ReAct 和 subagent 做到了什么程度,OutputMessage 有什么意义,Guardian 怎么做 policy gate,ESP32-P4/Android 为什么接管显示终端,Lua 和动态硬件扩展为什么不绕过安全边界,以及最近真实压力测试验证到了哪一步。

第一章:四块 ESP32-S3 不再是四个聊天机器人

一开始我想过把四块 ESP32 都做成类似“独立小助手”的形式。但实际推进下来,这个方向并不合理。MCU 不是服务器,不能无限开进程、堆上下文、塞工具和模型。四块板更适合按职责拆开,而不是复制四个聊天机器人。

当前实物映射是:

1
2
3
4
/dev/ttyUSB0  esp32s3-coordinator-01  coordinator_agent
/dev/ttyUSB1 esp32s3-sensor-01 sensor_agent
/dev/ttyUSB2 esp32s3-control-01 control_agent
/dev/ttyUSB3 esp32s3-guardian-01 guardian_agent

这四块板仍然使用同一套 ESPAgent 固件。区别来自 build-time profile,也就是 espagent_secrets.h 里的 NODE_IDNODE_ROLENODE_CAPABILITIESNODE_RESPONSIBILITIES。这样做的好处是协议、工具、topic、调试命令和构建方式统一。缺点是 Flash 还没有按角色裁剪,四块板的 app 镜像基本一致。

角色边界大概是这样:

1
2
3
4
5
6
7
8
9
10
11
Coordinator:
Feishu / WebSocket / LLM / ReAct / dispatch / timeline / weather / search

Sensor:
AHT10/AHT20 / SGP30 / BH1750 / presence / telemetry / sensor result

Control:
WS2812 / GPIO / servo / relay / actuator / command execution

Guardian:
policy_check / policy_decision / audit / privacy / watchdog / StateBoard

当前系统可以拆成两层:

1
2
3
4
5
四块 ESP32-S3:
负责真实边缘 Agent Mesh、硬件感知、硬件执行、安全裁决

ESP32-P4+C6 / Android:
负责展示推理过程、通信过程、节点状态、最终结果和人工确认

这个分层比“四块 S3 里塞一块显示节点”更清楚。S3 做边缘控制,P4/Android 做复杂 UI。

第二章:主链路现在长什么样

现在 ESPAgent 的核心链路可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Feishu / WebSocket / Cron / Proactive / Serial

message_bus inbound queue

agent_loop

LLM tool calling / ReAct / spawn_subagent

Guardian policy_check / policy_decision

tool_registry / mesh_send_command

Sensor / Control 本地执行

OutputMessage espagent.output.v1

message_bus 内部回注 + MQTT timeline

LLM 总结结果

Feishu / WebSocket / P4 / Android

这条链路里有几个边界很重要。

第一,Feishu、WebSocket、Cron 和 Proactive 都只是消息入口。它们不直接碰硬件。真正决定是否调用工具的是 agent_loop

第二,LLM 不能随便执行 C 函数。它只能看到 tool_registry 里注册过的工具 schema。比如 get_weathermesh_send_commandread_temperature_humidityws2812_setspawn_subagent 等。

第三,跨 ESP32 的硬件动作不应该从 MQTT callback 直接打到 GPIO。现在 Control 侧已经要求本地缓存里有 Guardian 的 allow decision,才会执行白名单控制命令。Control 还补了轻量 command queue、重复 command_id 拦截、TTL 检查、单执行器互锁、emergency stop latch 和 actuator state snapshot。也就是说,“先经过 Guardian,再进入 Control 本地安全边界”已经不是 prompt 约定,而是固件路径。

第四,远端执行结果必须结构化。否则 Coordinator 只能收到一段自然语言,很难稳定关联 command_idtrace_id、执行节点、动作、状态和错误。

第三章:关于本项目的 ReAct

之前项目里已经有 ReAct 形态:

1
2
3
4
5
6
用户消息
-> LLM
-> tool_use
-> 执行工具
-> tool_result 回填
-> LLM 最终回复

USB0 上也验证过主 agent_loop 能完成一次真实工具往返,例如让模型调用 get_current_time,工具返回时间后再由 LLM 总结。

但做到这里还不够。对于单板本地工具,tool_result 基本够用;对于跨 ESP32 的 Mesh 命令,只靠 tool_result 就不够了。因为 mesh_send_command 的本地执行结果通常只是:命令已经发布到 MQTT。真正的温湿度读取、灯光控制、舵机动作发生在另一块 ESP32 上。

所以项目里补了结构化 OutputMessage。它不是 LLM provider 的原生 message,而是 ESPAgent 自己定义的节点执行结果消息。当前 schema 是 espagent.output.v1,典型结构是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"schema": "espagent.output.v1",
"type": "output",
"event": "mesh_command_result",
"msg_id": "out-...",
"node_id": "esp32s3-control-01",
"role": "control_agent",
"sender": "control_agent",
"recipient": "coordinator_agent",
"command_id": "cmd-...",
"trace_id": "trace-...",
"action": "set_status_light",
"status": "ok",
"esp_err": "ESP_OK",
"summary": "OK: status light on GPIO 48 set to blue",
"result": {"text": "..."},
"error": null,
"ts_ms": 123456
}

它的意义很直接:让结果可关联、可展示、可审计、可回注。

现在 mesh_send_command 默认是异步模式。Coordinator 调用工具后,会先返回类似这样的结果:

1
2
3
OK: queued MQTT mesh command action=set_status_light ...
async_task_id=mesh-task-cmd-...
result will be injected when OutputMessage arrives

后台 task 会继续等待同一个 command_id 的 OutputMessage。等 Sensor 或 Control 发布结果后,Coordinator 会把这个结果作为内部消息注入回 message_bus,让 agent_loop 再跑一轮,让 LLM 用一句用户能看懂的话总结结果。

这就是现在比旧稿进步最大的地方:

1
2
3
4
5
旧状态:
发送命令 -> 告诉用户“已发送”

现在:
发送命令 -> 等待远端 OutputMessage -> 回注给 LLM -> 告诉用户真实执行结果

例如飞书里让远程控制板把 WS2812 设置为蓝色,日志里已经能看到:

1
2
Mesh control command executed: id=cmd-... action=set_status_light status=ESP_OK
result=OK: status light on GPIO 48 set to blue (RGB=0,0,255 brightness=255)

然后 Coordinator 会再回复用户:

1
远程控制板(esp32s3-control-01)的状态灯已成功设置为蓝色。

这比“我已经下发命令”更像一个真正知道执行结果的 Agent。

第四章:第四角色:Guardian

第四块 ESP32-S3 现在的角色是 Guardian。它不是显示屏,也不直接控制硬件,而是给整个 Mesh 加一层安全和审计。

它主要订阅这些 topic:

1
2
3
4
5
espagent/cube1345/security/policy_check
espagent/cube1345/security/decision
espagent/cube1345/agent/timeline
espagent/cube1345/alerts
espagent/cube1345/guardian/stateboard

当 Coordinator 要下发 Mesh command 时,不是立刻发给 Sensor 或 Control,而是先发一个 policy_check

1
2
3
4
5
6
7
8
9
10
11
{
"schema": "espagent.policy_check.v1",
"event": "policy_check",
"command_id": "cmd-...",
"trace_id": "trace-...",
"source_role": "coordinator_agent",
"target_role": "control_agent",
"action": "set_status_light",
"safety_level": 1,
"ttl_ms": 30000
}

Guardian 收到后,根据白名单、安全等级和目标角色返回 policy_decision

1
2
3
4
5
6
7
8
9
10
11
{
"schema": "espagent.policy_decision.v1",
"event": "policy_decision",
"guardian_node": "esp32s3-guardian-01",
"command_id": "cmd-...",
"decision": "allow",
"allowed": true,
"reason": "allowed whitelisted low/medium-risk control action",
"risk_score": 65,
"privacy_mode": "metadata_only"
}

只有 decision=allow 时,Coordinator 才会继续发布真正的 Mesh command。Control 侧也不是只相信 MQTT payload,它会检查本机缓存里的 Guardian allow decision。这样即使有人伪造 control topic,也不能轻易绕过策略层。

这次压力测试还暴露出一个细节:Guardian 同时订阅 nodes/+/statenodes/+/telemetry、timeline 和 policy topic,如果每条 state/telemetry 都立刻生成 StateBoard 更新,就可能把 policy_decision 回流拖慢。现在已经做了两层修复:

1
2
3
4
5
6
7
8
Guardian watchdog StateBoard:
普通 state / telemetry 摘要按 30 秒节流
异常状态仍然立即上报

Coordinator policy wait:
单次等待 3-15 秒
未收到 decision 时用同一个 command_id 重发 policy_check
最多重试 3 次

因为 policy_checkpolicy_decision 都带同一个 command_id,这个重试是幂等的。真正的 Sensor/Control command 只有拿到 allow 之后才会发出去,所以不会导致硬件动作重复执行。

Guardian 还会观察 timeline。比如看到 mesh_command_result 是 error,就发布 audit 和 alert;看到 Control 执行成功,就记录观察事件。这些会进入 guardian/stateboard,串口上可以通过:

1
stateboard_show

查看最近的 policy decision、mesh result、final reply 等事件。

现在 Guardian 还不是最终的生产级安全系统,但已经不只是第一版白名单了。当前固件里已经有人工确认队列:高风险动作可以进入 approval_list,确认后生成一次性 approval_id,由 Coordinator 或用户重试同 action/role 的命令。Mesh command 也支持可选 HMAC 当前 key / previous key 验证,Control 侧支持可选 interlock GPIO。还没完成的是生产 broker ACL/TLS、真实互锁接线验证、密钥轮换 SOP 和更完整的隐私脱敏策略。

这一步的意义是:安全不再只靠“提示词里让 LLM 小心一点”,而是被拆成一个确定性的角色节点和一组可验证的固件路径。

第五章:subagent 是同一块板上的临时子任务

spawn_subagent 仍然是项目里的一个重要能力,但要说清楚它的边界。

它不是第二个物理 ESP32,也不是云端子进程,而是在同一块 ESP32-S3 上创建一个临时 FreeRTOS task。主 Agent 调用它后,子任务会运行自己的短 ReAct loop,完成任务后通过 semaphore 把结果交回来。

它的工具白名单很窄:

1
2
3
4
5
6
7
web_search
get_weather
get_current_time
read_file
write_file
edit_file
list_dir

它不能调用:

1
2
3
4
5
6
mesh_send_command
gpio_write
ws2812_set
servo_write
read_temperature_humidity
spawn_subagent

这样设计是有意的。subagent 适合做信息类、文件类、总结类任务,不适合直接操作硬件。因为硬件控制必须走主 Agent、Guardian 和 Mesh policy 边界。

USB0 上已经验证过 spawn_subagent。串口调用:

1
tool_exec spawn_subagent {"task":"Call_get_current_time_and_return_one_sentence"}

能看到子任务启动、调用 get_current_time、拿到结果、再让 LLM 总结。也就是说,subagent 的“临时子 Agent + 工具调用 + 结果返回”闭环在板端是通的。

但它目前仍是同步等待模式,不是后台长期 worker。真正的异步闭环现在主要体现在 Mesh command:async_task_id -> OutputMessage -> message_bus

第六章:每块板都有自己的 capability 和 Lua runtime

ESPAgent 现在也开始从“工具注册表”升级到更像单板 Agent runtime 的结构。

原来的 tool_registry 仍然保留,但上面加了一层 capability registry。每个 capability 会描述:

1
2
3
4
5
name
family
risk
flags
execute

这样做的意义是,工具不再只是给 LLM 看的一串 JSON schema,而是可以被不同调用者以不同身份执行。现在已经有:

1
2
3
4
tool_registry_execute()
tool_registry_execute_as()
capability_registry
role_capability_profile

也就是说,同一个固件里,不同角色看到的能力不一样。Coordinator 更偏通信、LLM、调度;Sensor 更偏环境读取;Control 更偏执行器;Guardian 更偏 policy、audit、StateBoard。这样比“所有角色都暴露所有工具”更接近真正的多节点系统。

另一个变化是 Lua runtime 已经不再只是计划。四块 ESP32-S3 都已经部署了受控 Lua 能力,并做过 smoke test:

1
2
USB0 Lua smoke: 7/7 PASS
USB0-3 跨角色 Lua smoke: 8/8 PASS

Lua 侧可以执行 inline source、SPIFFS 脚本、异步 job,也可以查看 job 状态。脚本里能通过:

1
2
3
local espagent = require("espagent")
local result = espagent.call_capability("get_current_time", "{}")
print(result)

调用 ESPAgent capability。

这里有一条安全红线:Lua 不是裸硬件逃生通道。它不能直接获得 gpio/i2c/adc/pwm/rmt/ble/display/camera/audio 这类底层模块,不能绕过 sandbox、role profile、Guardian policy 或 Control interlock。Lua 想控制硬件,仍然必须通过 ESPAgent 已注册的 capability、manifest primitive 或 Mesh 工具链。

所以这次吸收 esp-claw 的思路,不是把 ESPAgent 改成另一个 esp-claw,而是把它的单板 runtime 思想融合进当前项目:

1
2
3
4
5
6
capability registry
role-visible profile
统一事件流
Memory v2
dynamic extension catalog
受控 Lua runtime

项目特色仍然是四角色 Agent Mesh、Guardian 权限治理、OutputMessage/ReAct 闭环和 P4/Android 可视化推理链路。

同一条思路也用在动态硬件扩展上。现在 SPIFFS 里可以放设备 manifest,描述一个新硬件使用什么协议、地址、寄存器、GPIO、风险等级和权限声明。开发者不一定每次都要重新写一个完整 C 工具;对于一部分通用协议设备,可以先用 manifest primitive 描述,再通过 virtual_device_readvirtual_device_control 进入统一执行路径。

目前软件侧已经有:

1
2
3
4
5
6
I2C / UART / Modbus / SPI / ADC / GPIO input 只读 manifest 示例
GPIO output / relay / PWM / LEDC 控制 manifest 示例
manifest_lint.py dry-run / support-matrix / init-template
SHA-256 sidecar 校验
role / risk / permissions / GPIO allowlist 校验
控制类 duration / cooldown / safe-state restore

这里同样不追求“让 AI 随便操控任何陌生硬件”。更合理的目标是:开发者把 datasheet 里的协议知识整理成 manifest,固件负责校验边界,Guardian 负责 policy,Control 负责本地安全执行。这样 AI 可以理解“这个新设备应该怎么读、怎么写”,但不会绕过已有的安全链路。

第七章:MQTT Mesh 现在已经能从飞书跑到硬件

四块 ESP32-S3 之间现在主要通过 MQTT Mesh 通信。当前调试 broker 是:

1
broker.emqx.io:1883

topic prefix 是:

1
espagent/cube1345

核心 topic 包括:

1
2
3
4
5
6
7
8
9
10
11
espagent/cube1345/nodes/<node_id>/state
espagent/cube1345/nodes/<node_id>/telemetry
espagent/cube1345/nodes/<node_id>/events
espagent/cube1345/nodes/<node_id>/command
espagent/cube1345/roles/<role>/command
espagent/cube1345/agent/dispatch
espagent/cube1345/agent/timeline
espagent/cube1345/security/policy_check
espagent/cube1345/security/decision
espagent/cube1345/guardian/stateboard
espagent/cube1345/alerts

一个典型的“点亮远程控制板状态灯”流程现在是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
飞书用户:把远程控制板的 WS2812 状态灯设置为蓝色

USB0 Coordinator 收到 Feishu 消息

确定性路由 / LLM tool calling 选择 mesh_send_command

Coordinator 发布 policy_check

USB3 Guardian 返回 policy_decision=allow

Coordinator 发布到 roles/control_agent/command

USB2 Control 收到命令并检查 Guardian allow decision

Control 执行 set_status_light

Control 发布 OutputMessage: mesh_command_result

Coordinator 后台等待 task 收到结果并回注 message_bus

LLM 总结执行结果

Feishu 回复用户,P4/Android 可订阅 timeline 展示全过程

温湿度读取也是类似,只是目标角色从 control_agent 换成 sensor_agent

1
读取温湿度 -> sensor_agent/read_temperature_humidity

这里还有一个用户体验上的改进。用户不需要在飞书里说:

1
请调用 mesh_send_command,让 esp32s3-sensor-01 执行 read_temperature_humidity

现在只要说:

1
读取温湿度

Coordinator 会自动判断这是 Sensor 的事情。控制类请求也一样,例如:

1
2
点亮 WS2812 为蓝色
把远程控制板状态灯设置为蓝色

会自动路由到 control_agent。这部分不完全依赖 LLM 自己发挥,项目里已经加了确定性 Mesh 路由和“假成功”拦截。之前模型偶尔会口头说“已经发送 MQTT 命令”,但实际上没有调用工具;现在这类常见请求会优先走确定性路径。

第八章:ESP32-P4 和 Android 不替代 Coordinator,而是显示终端

ESP32-P4+C6 的定位现在更清楚了:它不是第五个 Coordinator,也不是主 LLM 节点,而是现场 Display Terminal。

P4 工程在:

1
/home/cube/WorkSpace/ESP/lvgl_traffic_control

当前已经在这个工程里加入了 AgentMesh 页面,包含:

1
2
3
4
5
6
四角色状态卡
Timeline 列表
最终结果区域
裸 TCP MQTT 订阅任务
FreeRTOS 事件队列
内存 timeline buffer

P4 侧订阅的就是 S3 发布的 Mesh 数据流:

1
2
3
4
espagent/cube1345/agent/timeline
espagent/cube1345/nodes/+/state
espagent/cube1345/nodes/+/telemetry
espagent/cube1345/alerts

目前 P4 已经烧录到 /dev/ttyACM0,并验证到 Wi-Fi 和 MQTT 连接、订阅成功。现在 S3 侧会往 timeline 里发布更多结构化信息,包括 policy_decisionrisk_score、Sensor telemetry 的 temp_avg/humidity_avg/light_lux_avg、Control 执行后的 control_state 快照。后续还要继续用四块 S3 的实时流量验证屏幕 timeline 是否稳定刷新。

Android 端则是 P4 的移动增强版。它不应该写成一个普通聊天机器人,而应该写成 Display / Debug / Confirm Terminal。也就是说,Android 端订阅同一套 MQTT topic,把事件组织成 StateBoard 和 Trace:

1
2
3
4
5
6
7
8
9
用户输入
policy_check
policy_decision
tool_use
mesh_command_queued
mesh_command_result
OutputMessage
guardian_audit
final_reply

Android 比 P4 更适合做复杂交互,例如:

1
2
3
4
5
6
7
按 trace_id 查看完整任务链路
查看原始 JSON
筛选节点和事件类型
展示历史任务
高风险操作人工确认
调试 MQTT topic
导出 trace

这个定位和云端多 Agent 项目里的 StateBoard 很像。所有事件先进状态板,再由 UI 渲染。这样就不会把 UI 写死在某个单一日志格式上。

第九章:天气、时间和 OTA 也进入了工程化阶段

这几天还补了几个看似小、但很影响稳定性的能力。

时间同步

第一角色启动后会使用 SNTP 校时,默认服务器是:

1
ntp.aliyun.com

get_current_time 会优先读取已经同步的系统时间。实机验证里,USB0 返回过:

1
2026-06-17 14:43:05 CST (Wednesday)

这比之前依赖 HTTP Date header 更稳定。

高德天气

天气工具现在是结构化工具 get_weather,默认地区是南京市栖霞区,adcode 是:

1
320113

第一角色调用 get_weather 时,最开始在串口 tool_exec 下会让 console_repl 栈溢出。根因不是高德 key 缺失,而是 ESP-IDF 默认 console REPL 任务栈只有 4KB,HTTPS + TLS + cJSON 对它来说太重。

修复方式是把 CLI 栈提升到 12KB,并真正传给 REPL:

1
2
#define ESPAGENT_CLI_STACK (12 * 1024)
repl_config.task_stack_size = ESPAGENT_CLI_STACK;

之后又遇到高德 HTTPS 证书校验失败,实机日志里是 Failed to verify certificate。当前处理方式是:优先 HTTPS,如果证书链在当前 CRT bundle 下失败,则 fallback 到高德 HTTP WebService。修复后 USB0 连续调用 5 次 get_weather 都通过:

1
2
3
Live weather for 江苏栖霞区 (adcode=320113): 阴,
temperature=30 C, humidity=55%, wind=东南 ≤3,
report_time=2026-06-17 14:35:27.

OTA

项目也加入了 OTA 固件升级底座。当前是本地维护能力,不暴露给 LLM,也不建议直接从飞书触发。

串口命令是:

1
2
ota_info
ota_update <https_url_to_ESPAgent.bin>

它会把新的 app bin 写入 inactive OTA slot,成功后重启。

这里要强调一个边界:Agent 不会在 MCU 上“写新固件代码”或“编译固件”。OTA 的正确用法是开发者或 CI 先准备好 ESPAgent.bin,Agent 未来可以负责编排升级流程,例如版本发现、角色匹配、Guardian 审批、用户确认、下发 OTA、观察重启和汇报结果。

当前四个 S3 角色还是 build-time profile。如果 OTA 镜像里写死了另一个 NODE_ROLE,就可能把板子升级成错误角色。后续更好的做法是把 role profile 迁移到 NVS,让同一个 OTA 镜像能安全适配四块板。

第十章:压力测试验证到了哪一步

最近的验证不再只是“编译通过”。现在已经跑了真实四板和飞书入口压力测试。

MQTT Mesh 基准测试

脚本:

1
tools/stress_mesh_usb0_3.py

它固定只使用:

1
2
3
4
/dev/ttyUSB0
/dev/ttyUSB1
/dev/ttyUSB2
/dev/ttyUSB3

不使用 /dev/ttyACM*,避免把 ESP32-P4 当成 S3 烧录或测试。

最新一次四板回归先重新按 USB0-3 顺序烧录:

1
2
3
4
USB0 -> coordinator_agent
USB1 -> sensor_agent
USB2 -> control_agent
USB3 -> guardian_agent

然后执行角色自检:

1
tools/verify_roles_usb0_3.py --echo

结果四块板全部 PASS。

随后执行:

1
tools/stress_mesh_usb0_3.py --rounds 5 --interval 1.5 --settle 60 --quiet

连续两轮都通过。两轮核心指标一致:

1
2
3
4
5
6
7
sent=10
queued_ok=10 queued_error=0
sensor_received=5 sensor_executed=5 expected=5
control_received=5 control_executed=5 expected=5
policy_checks=10 policy_decisions=10 guardian_audits=10
warnings=0 errors=0 crashes=0
RESULT: PASS

这说明四角色识别、Coordinator 下发、Guardian 裁决、Sensor 接收、Control 执行、OutputMessage 返回、Guardian 审计这条 Mesh 链路在当前压力强度下是通的。

这次测试还顺手修掉了一个真实压力问题:Guardian 同时处理 state、telemetry、timeline 和 policy 时,普通 StateBoard 更新会拖慢 policy decision 回流。修复后,mqtt_state_lines 明显下降,policy 链路不再因为 watchdog 摘要过多而误超时。

飞书入口端到端测试

脚本:

1
tools/stress_feishu_usb0_3.py

它会真实向飞书 bot 发消息,比如:

1
2
读取温湿度
把远程控制板的 WS2812 状态灯设置为蓝色

此前飞书入口结果:

1
2
3
4
5
6
7
sent_ok=2 sent_failed=0 expected_total=2
sensor_received=1 sensor_executed=1 expected=1
control_received=1 control_executed=1 expected=1
async_result_turns=2
feishu_send_ok=9
crashes=0
RESULT: PASS

这里的 async_result_turns=2 很关键。它说明两条请求都不是只停在“命令已发送”,而是走到了异步 OutputMessage 回注。最新一轮重点先压了串口/MQTT Mesh 层,飞书入口仍建议作为下一轮回归测试继续跑。

天气回归测试

USB0 连续执行 5 次:

1
tool_exec get_weather {"adcode":"320113","extensions":"base"}

结果全部 ESP_OKcrashes=0RESULT: PASS

关于传感器错误

压测里以前 Sensor 节点会出现:

1
2
3
AHT10 not found
DHT22=ESP_ERR_TIMEOUT
MH-Z19 UART unavailable

这些目前不算 Mesh 失败。后续 AHT20 已经接线并实测可读,USB1 telemetry 里能看到 AHT20 温湿度;DHT22、MH-Z19、SGP30 之类如果没有接线或当前配置不可用,仍然会在综合环境工具里表现为对应子项错误。判断 Mesh 是否成功,主要看 command 是否到达、是否执行、是否发布 OutputMessage,以及 Guardian 是否完成 policy/audit。

第十一章:资源占用现在怎么看

四块 S3 仍然没有把硬件资源完全吃满。更准确地说,运行时已经按 role 裁剪服务,但固件镜像还没有按角色裁剪。

最近构建的 ESPAgent.bin 大小已经明显增长。加入 capability registry、Lua runtime、动态 manifest、Guardian/Control 安全链路之后,最新 app 大约是:

1
0x191d40

最小 app 分区是 2MB,还剩约 22%。这说明 Flash 还有余量,但已经不能随便继续堆依赖。USB0 烧录慢也不是因为 app 分区“快满了”,主要是每个 role 都要重新编译/写入约 1.6MB app,再加上 SPIFFS 镜像校验。

四个角色的资源侧重点现在是:

节点 当前资源重点 后续真正吃资源的方向
Coordinator LLM、Feishu、WebSocket、MQTT、TLS、session、skills、subagent、policy retry 更完整任务拆解、上下文压缩、运行时 role profile
Sensor I2C/UART/GPIO 采样、MQTT telemetry、AHT20、EWMA/cache、阈值事件 更多传感器、异常检测、局部自治策略
Control WS2812/GPIO/Servo、MQTT command receiver、command queue、互锁、actuator state 真实继电器/风扇/水泵、硬件 interlock 实测、动作回滚
Guardian policy decision、risk_score、audit、alerts、StateBoard、watchdog、approval queue 生产级 ACL/TLS、密钥轮换、隐私脱敏、长期异常评分

这里我现在不急着把 RAM/PSRAM/Flash 全部压满。MCU 上做 Agent,稳定性比“看起来资源吃满”重要。尤其是 TLS、cJSON、LLM response、MQTT buffer、FreeRTOS task stack 都很容易在压力下出问题。最近修复的 Feishu ACK 栈、timer service 栈、console REPL 栈就是典型例子。

所以现在更合理的策略是:

1
2
3
先把链路闭环跑稳
再把每个角色的职责加厚
最后再做按角色裁剪和资源压榨

第十二章:现在还不完美的地方

虽然当前进度已经比旧稿前进很多,但还不能说是完美的 Agent Mesh。

还存在这些边界:

1
2
3
4
5
6
7
8
9
10
Coordinator 仍然是单 ESP32-S3 上的单 agent_loop,不是多个独立 LLM Agent 进程
subagent 还是同步等待的临时 FreeRTOS task
复杂跨节点任务还没有完整 DAG 调度器
trace_*.jsonl、trace_index、task_tree 已有,但还缺长期归档和跨节点查询 API
Guardian 已有 approval queue、risk_score、watchdog StateBoard,但还缺生产级隐私脱敏和长期异常评分
Control 已有 command queue、TTL、去重、互锁、actuator state,但真实继电器/风扇/水泵和硬件 interlock 还要实测
P4 已经能连接和订阅 MQTT,但屏幕 timeline 的长期实时刷新还要继续压测
Android 端目前还是交接方案,尚未完成代码实现
角色 profile 仍是 build-time,OTA 场景下最好迁移到 NVS
生产级 broker ACL/TLS、密钥轮换 SOP、决策消息认证还没有部署

现在项目真正有 Agent 特征的地方在于:

1
2
3
4
5
6
7
自然语言进入 Feishu
LLM 或确定性规则选择工具
跨节点命令先经过 Guardian 裁决
Sensor/Control 执行真实硬件任务
结果以 OutputMessage 结构化返回
Coordinator 把结果回注给 LLM 再总结
P4/Android 订阅 timeline 展示推理和通信过程

这条链路才是 ESPAgent 的核心价值。

结尾

当前 ESPAgent 已经从单板 Agent 推进到四块 ESP32-S3 的角色协作:Coordinator 负责理解和调度,Sensor 负责感知,Control 负责执行,Guardian 负责权限和审计。ESP32-P4+C6 和 Android 则作为显示终端,把 AI 的推理过程、MQTT 通信过程、节点状态和最终结果展示出来。

我觉得这一步很关键。因为硬件 Agent 最怕的不是“功能少”,而是“看起来执行了,但不知道到底发生了什么”。现在有了 policy_checkpolicy_decisionOutputMessagetimelineStateBoard、capability profile、受控 Lua runtime,以及连续两轮四板 Mesh 压力测试,系统至少开始具备可追踪、可回归、可扩展的闭环。

接下来要做的不是继续盲目堆工具,而是继续把这条闭环做厚:P4/Android 的 trace 展示、真实继电器/风扇/水泵的安全联动、Guardian 的隐私治理和生产级 broker 安全、Sensor 的更多异常事件、以及 OTA 的安全编排。

做到这些之后,ESPAgent 才会从“能控制硬件的 MCU Agent”,继续往“能感知、能判断、能协作、能证明自己做了什么的边缘 Agent Mesh”推进。