RK3588(Rock 5T)硬件 Agent 搭建实录:从环境到跑通第一个闭环

文章目录

    • [1. 目标与总体架构](#1. 目标与总体架构)
    • [2. 环境搭建与问题实录](#2. 环境搭建与问题实录)
      • [2.1 这一阶段要干什么](#2.1 这一阶段要干什么)
      • [2.2 遇到的问题(如实记录)](#2.2 遇到的问题(如实记录))
      • [2.3 认知沉淀:venv 到底是什么](#2.3 认知沉淀:venv 到底是什么)
      • [2.4 项目分层设计(和固件分层对照)](#2.4 项目分层设计(和固件分层对照))
      • [2.5 环境信息(本机实测)](#2.5 环境信息(本机实测))
    • [3. MiniMax M3 接入](#3. MiniMax M3 接入)
      • [3.1 为什么 openai 的 SDK 能调 MiniMax](#3.1 为什么 openai 的 SDK 能调 MiniMax)
      • [3.2 Function Calling 报文格式(重点,逐帧剖析)](#3.2 Function Calling 报文格式(重点,逐帧剖析))
    • [4. 连通性自检](#4. 连通性自检)
      • [4.1 自检脚本的设计思想](#4.1 自检脚本的设计思想)
      • [4.2 操作步骤](#4.2 操作步骤)
      • [4.3 问题排查表(按现象查)](#4.3 问题排查表(按现象查))
      • [4.4 实测运行记录(2026-08-30)](#4.4 实测运行记录(2026-08-30))
    • [5. 工具层设计:把 Home Assistant 变成 Agent 的手脚](#5. 工具层设计:把 Home Assistant 变成 Agent 的手脚)
      • [5.1 设计原则:暴露"总线",不为每类设备写驱动](#5.1 设计原则:暴露"总线",不为每类设备写驱动)
      • [5.2 HA 的寻址模型(CAN 视角)](#5.2 HA 的寻址模型(CAN 视角))
      • [5.3 两个工程细节](#5.3 两个工程细节)
      • [5.4 sysinfo:Agent 感知"自己的身体"](#5.4 sysinfo:Agent 感知"自己的身体")
      • [5.5 验收清单](#5.5 验收清单)
    • [6. Agent Loop 手写:不到 100 行的主循环](#6. Agent Loop 手写:不到 100 行的主循环)
      • [6.1 全景](#6.1 全景)
      • [6.2 一次任务里 messages 的生长过程(实例)](#6.2 一次任务里 messages 的生长过程(实例))
      • [6.3 四个刻意的设计](#6.3 四个刻意的设计)
      • [6.4 流程可观测性](#6.4 流程可观测性)
    • [7. 第一次运行实录(2026-08-30)](#7. 第一次运行实录(2026-08-30))
    • [8. 遗留思考与下一步](#8. 遗留思考与下一步)

1. 目标与总体架构

一句话:让跑在板子上的 Agent 完成 感知 → 决策 → 执行 → 反馈 闭环。

组件 本项目实现
感知层 HA 设备状态 / USB 摄像头 / 板卡温度内存 get_states / camera_snapshot / get_system_status
消息层 Mosquitto(1883,已有) 后续事件驱动接入
决策层 MiniMax M3 + 手写 Agent Loop agent/loop.py + agent/llm.py
执行层 HA 服务调用 call_service(控一切已接入设备)
反馈层 终端轨迹 / 后续 SQLite 与通知 当前阶段仅终端打印

2. 环境搭建与问题实录

2.1 这一阶段要干什么

先把"骨骼"立起来:项目目录、Python 环境、和 MiniMax M3 打通、把 Home Assistant 变成 Agent 可调用的工具。目标是跑通一次完整闭环,暂时不追求智能。

2.2 遇到的问题(如实记录)

问题 1:系统 Python 是"裸"的

text 复制代码
$ python3 -c "import requests"
ModuleNotFoundError: No module named 'requests'

板子出厂的 Debian 12 精简干净,什么第三方包都没有。第一反应 pip install,但 Debian 12 会拦:

text 复制代码
error: externally-managed-environment

我的理解:这是 PEP 668 机制。发行版担心 pip 装的包覆盖 apt 管理的系统 Python 包(apt 自身的工具就依赖 python3-xxx),把系统搞坏。两条路:

  1. pip install --break-system-packages------粗暴,污染系统,pass;
  2. venv 虚拟环境------标准做法,选这个。

问题 2:venv 创建也失败

text 复制代码
$ python3 -m venv .venv
The virtual environment was not created successfully because ensurepip is not
available. ... You may need to install python3.11-venv

Debian 把 venv 支持拆成了单独的包(最小化安装的哲学,和 buildroot 只编必要的东西一个思路)。解决:

bash 复制代码
sudo apt install python3.11-venv

2.3 认知沉淀:venv 到底是什么

看完目录结构就明白了:.venv/bin/python 是指向系统解释器的软链接.venv/lib/python3.11/site-packages/ 是一块独立的包安装区pip install 只写这块区域。

类比 :和交叉编译的 sysroot 一回事------独立的头文件/库搜索空间,随便折腾,不污染主机根文件系统。删掉 .venv/ 目录 = 整个环境消失,零残留。以后每个项目一个 venv,就像每个项目一套 sysroot。

2.4 项目分层设计(和固件分层对照)

text 复制代码
hardware-agent/
├── main.py                  # 入口:selfcheck / run / chat 三个命令
├── .env                     # 配置(key、地址,不进 git)
├── agent/
│   ├── config.py            # 配置加载      ≈ 引脚配置表/板级头文件
│   ├── llm.py               # LLM 通信      ≈ 协议栈(对 MiniMax 的"串口驱动")
│   ├── loop.py              # Agent 主循环  ≈ main loop,本项目的灵魂
│   └── tools/
│       ├── registry.py      # 工具注册表    ≈ 驱动框架(ops 结构体)
│       ├── ha.py            # HA REST 工具  ≈ 一个具体的"外设驱动"
│       └── sysinfo.py       # 板卡自检      ≈ 传感器驱动(读板子健康)
├── docs/                    # 学习文档
└── snapshots/               # 摄像头快照输出(运行时生成)

分层的意义和固件一样:换模型只动 .env,换执行器只动 tools/,主循环 loop.py 从第一版起就不该大改。

2.5 环境信息(本机实测)

板卡 Radxa ROCK 5T,RK3588(4×A76+4×A55),16GB RAM
NPU 6 TOPS,驱动 v0.9.8,rknpu2 服务运行中
系统 Debian 12 bookworm,Python 3.11.2
已有服务 HA 8123 端口(host 网络)/ Mosquitto 1883 / Z2M / Node-RED(Docker)
依赖 openai 3.6.0 / requests / python-dotenv(装在 .venv)

3. MiniMax M3 接入

3.1 为什么 openai 的 SDK 能调 MiniMax

第一反应是怀疑:pip install openai 装的是 OpenAI 家的 SDK,凭什么能调别家模型?看代码(agent/llm.py)后明白了------SDK 里 base_url 是可配的。

OpenAI 的 Chat Completions HTTP 协议(POST /chat/completions,请求/响应 JSON 结构)已经成了行业事实标准,MiniMax 等各家都兼容它。所以对接任何一家,只需要三个参数:

text 复制代码
base_url  ------ 打给哪个服务(国内站 https://api.minimaxi.com/v1 / 国际站 https://api.minimax.io/v1)
api_key   ------ 身份凭证
model     ------ 指定模型名(以拿到的 API 文档为准,在 .env 里改)

类比:这和嵌入式里的协议复用一模一样------MQTT 客户端不关心 broker 是 Mosquitto 还是 EMQX;AT 指令集不关心模组是移远还是合宙。**只要报文格式约定一致,传输层换个对端,上层应用零改动。**手写的 Agent Loop 就是那个上层应用。

3.2 Function Calling 报文格式(重点,逐帧剖析)

Agent 区别于聊天机器人的核心机制。一次完整交互是三段式

第 1 段:请求时声明工具清单(能力上报)

json 复制代码
{
  "model": "MiniMax-M3",
  "messages": [{"role": "user", "content": "把书房灯打开"}],
  "tools": [{
    "type": "function",
    "function": {
      "name": "call_service",
      "description": "调用 Home Assistant 服务以控制设备......",
      "parameters": { "type": "object", "properties": { "...": "JSON Schema" } }
    }
  }]
}

parameters 是标准 JSON Schema------这就是给 LLM 看的"寄存器手册",它只能通过这段文字理解工具能干什么、参数怎么传。

第 2 段:模型不下结论,而是"下指令"

模型不执行任何东西(它只是一台推理服务器),而是返回:

json 复制代码
{
  "role": "assistant",
  "tool_calls": [{
    "id": "call_abc123",
    "function": {
      "name": "call_service",
      "arguments": "{\"domain\":\"light\",\"service\":\"turn_on\",\"entity_id\":\"light.study\"}"
    }
  }]
}

两个细节,第一眼容易忽略:

  • argumentsJSON 字符串 ,不是 JSON 对象------本地要再 json.loads 一次;
  • 模型只给参数,不执行。执行权百分之百在本地代码手里,这条边界是安全的基石:模型永远只能"申请",不能"访问"。

第 3 段:本地执行,结果回填,再问一次模型

本地执行 call_service() 成功后,把两条消息追加进历史再发起请求:

json 复制代码
{"role": "assistant", "tool_calls": [ ...第2段原样放回... ]},
{"role": "tool", "tool_call_id": "call_abc123",
 "content": "已执行 light.turn_on -> light.study"}

坑(已经预感到会在这里摔跤) :assistant 那条 tool_calls 消息必须原样 放回历史,工具结果靠 tool_call_id 与之配对,漏放或改动会直接报 400。这类似 CAN 总线上"请求帧"和"应答帧"必须成对,缺帧就是总线错误。

模型收到工具结果后,要么继续申请调下一个工具(循环),要么输出最终答复(循环结束)。这个 while 循环就是 Agent Loop 的全部。

4. 连通性自检

4.1 自检脚本的设计思想

main.py selfcheck 按依赖顺序查三层,从物理层往上量

text 复制代码
1. 配置检查   ------ key/地址/模型名是否填了           (电源在不在)
2. LLM 连通   ------ 发一句话看模型是否回话           (串口能不能通)
3. HA 连通    ------ GET /api/ 应返回 "API running."  (外设在不在)

以后每换环境(新板、新 key、断网恢复)第一步永远跑它。把"环境问题"和"代码问题"隔离排查,和点亮新板先量电源轨一个道理。

4.2 操作步骤

bash 复制代码
cd ~/hardware-agent
cp .env.example .env
nano .env        # 填 MINIMAX_API_KEY(HA_TOKEN 可留空,下一阶段再填)
.venv/bin/python main.py selfcheck

4.3 问题排查表(按现象查)

现象 可能原因 处理
401 Unauthorized key 错/未生效 核对 key、看控制台是否需开通
404 / model not found 模型名不匹配 按手头的 API 文档改 .env 的 MINIMAX_MODEL
连接超时 base_url 域名不对(国内/国际站)、网络 核对文档域名;本机有 Clash 代理,必要时设 https_proxy
selfcheck 第 3 项 401 HA_TOKEN 未填/过期 HA 网页 → 用户 → 安全 → 长期访问令牌
报 400 tool message 相关 协议配对问题(见 3.2 的坑) 检查 assistant 消息是否原样入历史

4.4 实测运行记录(2026-08-30)

  • selfcheck 三项全部通过
  • 模型实际回复:<think>用户只要求回复"在线"两个字......</think> 在线
    ------重要发现:M3 是思考模型,推理过程以 <think> 标签内联在 content 里
    不像某些厂商放在单独的 reasoning_content 字段。最终答案呈现时需要剥离
    (已在 loop.pystrip_think() 处理)。
  • key 是 sk-cp- 前缀(不是官方 JWT 格式,疑似中转渠道),但在官方国内端点
    api.minimaxi.com/v1 + 模型名 MiniMax-M3 下直接可用,无需改 base_url。
  • HA 项留待填令牌后补测。

5. 工具层设计:把 Home Assistant 变成 Agent 的手脚

5.1 设计原则:暴露"总线",不为每类设备写驱动

第一版最容易犯的错:为每种设备写一个工具------open_light()close_curtain()set_temperature()......设备一多,工具清单爆炸,模型选择空间大,出错率跟着涨。

实际方案(agent/tools/ha.py)只注册 3 个工具

工具 对应能力 类比
get_states 读所有实体状态 总线抓包(看总线上所有节点状态)
call_service 调任意 HA 服务 总线写事务(对任意节点发命令)
camera_snapshot 抓一帧画面 视觉传感器采样

因为 HA 本身就是家里所有设备的抽象层------Zigbee 开关、USB 摄像头、温湿度传感器在 HA 里全都是统一的 entity。我们复用这层抽象,等于"已有一根现成的设备总线,Agent 只需要会说这根总线上的协议"。

5.2 HA 的寻址模型(CAN 视角)

HA 服务调用是二维寻址:

text 复制代码
POST /api/services/{domain}/{service}
body: {"entity_id": "...", ...附加参数}

例:POST /api/services/light/turn_on   body: {"entity_id": "light.study"}
  • domain.service(如 light.turn_on)------功能码
  • entity_id(如 light.study)------节点地址

和 CAN 的"ID 含功能含义 + 报文指定目标"高度同构,理解成本几乎为零。一个推论:entity_id 必须先查再操作get_states 先行),凭记忆写 entity_id 就像凭记忆写 CAN ID------迟早打错节点。这条规则写进了 Agent 的 system prompt。

5.3 两个工程细节

状态裁剪(省 token = 省带宽) :HA /api/states 全量返回几十 KB,每个实体带几十个属性字段。get_states 只保留 entity_id / state / friendly_name 三个字段再回填。**LLM 上下文是昂贵资源,类似串口带宽------只发有效字节,不转发整帧。**设备数量上去之后,这一刀会越来越重要(后面要做分页/按需过滤)。

错误回填,不抛异常(registry.py 的看门狗设计)dispatch() 对所有错误(未知工具、JSON 解析失败、参数不匹配、执行异常)统一返回错误字符串回填给模型,而不是让异常炸出主循环。为什么:模型收到"参数不匹配"的错误信息后,有能力对照工具定义自行修正重试。单次工具失败 ≠ 任务失败,把错误当作总线上的 NACK 帧处理,任务级韧性来自循环重试。当然 max_iterations(第 6 节)兜底防止无限重试烧 token。

5.4 sysinfo:Agent 感知"自己的身体"

get_system_status/sys/class/thermal/thermal_zone0/temp(RK3588 SoC 温度)和 /proc/meminfo。这是硬件 Agent 和纯软件 Agent 的第一个区别:它跑在真实硅片上,过热降频、内存吃紧都会改变它的行为。让 Agent 能自查,排查"它今天变笨了/变卡了"时才有数据可看。

5.5 验收清单

  • 不填 HA_TOKEN 时调用 ha 工具 → 应得到明确报错"HA_TOKEN 未配置"
  • 填 HA_TOKEN 后 get_states 返回家中设备列表(含 Zigbee 开关)
  • call_service 控制一个真实开关,人眼确认动作
  • camera_snapshot 生成 jpg,打开看内容正确
  • get_system_status 报出合理温度(实测 70.23°C,带 Docker 负载属正常偏高)

6. Agent Loop 手写:不到 100 行的主循环

不用任何框架(LangChain/LangGraph 之类),手写整个循环------先搞懂轮子,再决定要不要用别人的轮子。

6.1 全景

text 复制代码
messages = [system, user]          ← 任务下发
while True:
    msg = LLM(messages, tools)     ← 问决策器
    if msg.tool_calls:             ← 决策"动手"
        历史 += assistant(tool_calls)     ← 请求帧入队
        for tc in tool_calls:
            结果 = dispatch(tc)   ← 执行外设操作
            历史 += tool(结果)    ← 应答帧入队
        continue                 ← 带结果再问决策器
    else:                        ← 决策"收工"
        return msg.content       ← 最终答复

对照固件:LLM 是决策器(跑在"云端的协处理器"),tools 是外设驱动,messages 历史是共享缓冲区,loop.py 是主循环。和聊天机器人的唯一区别:LLM 的输出能触发本地函数,函数结果又能回流进上下文------闭环形成,"智能"就出现在这个闭环里。

6.2 一次任务里 messages 的生长过程(实例)

用户输入:"看看书房灯开着没,开着的话帮我关掉"

text 复制代码
[0] system     你是 ROCK 5T 上的硬件 Agent......(规则)
[1] user       看看书房灯开着没,开着的话帮我关掉
[2] assistant  tool_calls: [get_states({"domain":"light"})]      ← 第1轮决策
[3] tool       [{"entity_id":"light.study","state":"on",...}]    ← 执行结果
[4] assistant  tool_calls: [call_service(light,turn_off,light.study)] ← 第2轮
[5] tool       "已执行 light.turn_off -> light.study"
[6] assistant  "书房灯原本开着,已关闭。"                          ← 最终答复,退出

三轮请求,6 条追加消息。"智能"不是一段代码,是这几条消息往返出来的轨迹------每多一轮,模型就多看一眼真实世界的反馈。

6.3 四个刻意的设计

设计 位置 理由
temperature=0.3 llm.py 硬件控制要稳定复现,不要发散。类比:控制回路采样周期宁稳勿抖
max_iterations=10 loop.py 看门狗:防模型无限调工具死循环烧 token
assistant 消息先入历史再回填 loop.py OpenAI 协议硬性要求,tool_call_id 配对,漏了报 400(3.2 的坑)
工具错误回填不抛异常 registry.py 把失败当 NACK,模型可自纠重试(5.3)

6.4 流程可观测性

loop.py 里所有 print 用了终端颜色:灰色=请求中、青色=工具调用、黄色=工具结果、绿色=最终答复。学习期把每轮决策打印出来至关重要------Agent 出错时需要像看逻辑分析仪波形一样回放这条轨迹。后面做常驻服务时这些 print 会升级成日志文件。

7. 第一次运行实录(2026-08-30)

任务:run "查一下板子的温度和内存占用,用一句话汇报"

第一遍:抓到真 bug,同时验证了错误回填设计。

text 复制代码
[loop 1] → 工具调用 get_system_status({})
[loop 1] ← 错误:'usage' object has no attribute 'percent'
[loop 2] Agent:抱歉,get_system_status 调用失败(内部错误......)建议排查

两层教训:

  1. 我的错shutil.disk_usage() 返回的 usage 对象只有 total/used/free,percent 是 psutil 的 API------写代码时把两个库的接口记串了。修复:手动 used/total*100
  2. 设计的胜利 :工具报错没有炸掉主循环,错误字符串按 role=tool 回填后,M3 自己理解了失败并向用户解释------registry.py 的"看门狗"设计第一次运行就体现了价值。

第二遍:绿色全通。

text 复制代码
[loop 1] → get_system_status({})
         ← {"soc_temp_c": 70.23, "mem_used_pct": 35.1, "disk_used_pct": 30.3}
[loop 2] Agent:板子当前 SoC 温度 70.23°C,内存占用 35.1%,运行状态正常

模型行为观察:只给了"温度和内存"却主动多报了磁盘(工具返回了三个字段它就全用了);空参数工具传参准确;两轮收敛,无多余动作。

新增处理 :M3 的 <think> 推理块会出现在最终答复里,loop.py 增加 strip_think() 剥离(4.4 有记录)。

8. 遗留思考与下一步

遗留

  • call_service 是"万能执行器",也意味着模型可以调任何 服务(包括删自动化、改配置)。后续要加白名单:service 域级别准入 + 高危操作二次确认。
  • get_states 全量返回在设备 >50 个时会挤爆上下文,届时需要"按 domain 查 + 关键字搜索"两个变体。

下一步(阶段 B)

  1. 多轮记忆:现在是每次 run() 独立、无记忆。引入 SQLite 会话存储。
  2. MCP 化:把 tools/ 封装成 MCP server,让任何 MCP 客户端直接控制板卡硬件------体验"标准化外设总线"。
  3. 语音/QQ 入口:复用板上的 QQ bot 通道,Agent 获得远程对话入口。

相关推荐
派勤电子3 天前
工控主板串口数量上限是多少?X86与ARM不同板型原生串口数量详情
嵌入式开发·工业控制·工业自动化·工控主板·arm主板·工控主板串口·x86主板串口
BW.SU3 天前
RUI Studio--嵌入式 UI 开发新范式
单片机·ui·嵌入式开发
dozenyaoyida3 天前
无外网 Linux 服务器离线安装 VS Code Remote-SSH 指南(新版双包机制 + commit 一致)
ssh·vs code·嵌入式开发·clangd·离线安装·remote-ssh
Tronlong创龙4 天前
告别系统崩溃!ARM工控机 + OverlayFS,系统一键还原
嵌入式开发·硬件开发·工业控制·工业开发板
俊基科技4 天前
AU-48 双麦多功能语音处理模组让每一句话都清晰抵达 —— 一颗 23×20mm 小芯片,重新定义“听得清“
嵌入式开发·硬件开发·ai降噪·回声消除·拾音降噪
捷瑞电子工坊7 天前
FreeRTOS 内存管理详解:从 heap_1 到 heap_5 的选择与实践
freertos·内存管理·嵌入式开发·内存碎片·嵌入式实时操作系统·动态内存分配
Lee_jerome11 天前
从 PyTorch 权重到 RK3588 板端推理:ResNet18 二分类模型完整部署教程
pytorch·边缘计算·rk3588·模型部署·onnx·resnet18·int8量化
QXWZ_IA11 天前
【深度】打破IoT“米级”魔咒:当RTK SDK遇上具身智能与工业互联,厘米级定位如何重塑边缘计算?
物联网·自动驾驶·嵌入式开发·rtk·北斗导航
lsz15259014 天前
嵌入式开发第一步:STM32 F1系列功能介绍与快速上手
stm32·单片机·嵌入式硬件·嵌入式开发·可扩展模块