AI陪伴机器人设备控制指令-speak-gesture-light-wake-sleep

10-设备控制指令-speak-gesture-light-wake-sleep

黒漂技术佬 · AI 伙伴(AI-Partner)系列 · 提醒设备与 MQTT 联动篇

一、让机器人"动起来"的最后一公里

前面几篇把上行讲透了:设备报状态、报心跳、报跌倒,云端收得明明白白。但陪伴机器人的灵魂在下行——用户说"给我讲个笑话",机器人得真的开口说;到点了该提醒吃药,它得挥手加播报。这一篇讲 AI 伙伴(AI-Partner)的指令下行全链路:从大模型决定调用工具,到指令飞进 MQTT 主题,到端侧执行。

二、入口:DeviceTool 的两个 @Tool

下行指令的入口不在 Controller(虽然也有 /api/devices/control 接口),更值得注意的是 Agent 工具。用户跟机器人聊天,大模型判断"这句话需要控制设备",就会调用 DeviceTool 里的方法:

// 项目源码:agent/tools/DeviceTool.java
@Component
@RequiredArgsConstructor
public class DeviceTool {

    private final DeviceService deviceService;

    @Tool("查询该用户绑定的陪伴机器人设备及其在线状态、电量。返回设备列表")
    public String getDeviceStatus(@ToolMemoryId Long userId) {
        return deviceService.listStatusText(userId);
    }

    @Tool("向指定陪伴机器人设备下发动作:speak(播报文本)、gesture(动作如 wave/nod/happy)、light(灯光开/关)、wake、sleep。返回下发结果")
    public String controlDevice(String deviceCode,
                                String action,
                                String param,
                                @ToolMemoryId Long userId) {
        return deviceService.control(userId, deviceCode, action, param);
    }
}

注意 controlDevice@Tool 描述原文:它把五种 action 的取值和含义直接写给了大模型——

action含义param 示例
speak播报文本"该吃药啦"
gesture肢体动作wave / nod / happy
light灯光开关"on" / "off"
wake唤醒
sleep休眠

这段描述就是大模型的"说明书":它决定调 controlDevice 时,会照着描述填 action=speakparam=讲个笑话的内容工具描述写得越具体,模型填参数越靠谱——这不是注释,是提示词工程的一部分。@ToolMemoryId Long userId 保证工具内拿到的永远是当前对话用户的 ID,多用户隔离不靠模型自觉,靠框架注入。

三、DeviceService.control:校验、组装、发布

工具方法一行转发到 DeviceService.control,这才是干活的:

// 项目源码:service/DeviceService.java
/** 下发设备控制指令(MQTT server/xxx/cmd) */
public String control(Long userId, String deviceCode, String action, String param) {
    Device device = deviceRepository.findByDeviceCode(deviceCode)
            .orElseThrow(() -> new BusinessException("设备不存在:" + deviceCode));
    if (device.getUserId() != null && !device.getUserId().equals(userId)) {
        throw new BusinessException("无权控制该设备");
    }
    Map<String, Object> cmd = new HashMap<>();
    cmd.put("action", action);
    cmd.put("param", param == null ? "" : param);
    cmd.put("ts", System.currentTimeMillis());
    mqttClientManager.publish(TopicConstants.commandTopic(deviceCode), cmd);
    return "已下发动作 " + action + " 到设备 " + deviceCode
            + (param == null || param.isBlank() ? "" : "(" + param + ")");
}

三步拆解:

  1. 校验:设备必须存在;如果设备已绑定用户,则当前 userId 必须是绑定的那个——防止 A 用户控制 B 用户的机器人。注意 userId == null(未绑定/待认领设备)时直接放行,这是当前实现的一个宽松点。
  2. 组装:payload 三件套 {action, param, ts}param 为 null 时填空串(避免端侧判空),ts 是毫秒时间戳(端侧可以做时效校验、去重排序)。
  3. 发布:目标主题由 TopicConstants.commandTopic(deviceCode) 算出,即 server/{deviceCode}/cmd——上一篇讲过的精确下行主题,只有这台设备自己订阅。

顺带一提:control 方法没有 @Transactional——它是纯读校验加 MQTT 发布,不写库,不需要事务。给这种方法加事务注解反而是噪音。

四、同一主题,两种报文

有意思的来了。发到 server/{code}/cmd 这个主题上的报文,其实有两种形态。除了上面的控制指令,提醒调度器(ReminderScheduler)到期触发时也往同一个主题发:

// 形态一:控制指令(DeviceService.control 下发)
{ "action": "speak", "param": "该吃药啦", "ts": 1760000000000 }

// 形态二:提醒播报(ReminderScheduler 下发)
{ "kind": "reminder", "title": "吃降压药", "content": "饭后服用", "type": "medication", "ts": 1760000000000 }

区别一目了然:控制指令用 action 字段开头,提醒报文用 kind 字段开头。端侧固件收到消息后,必须先判断是 kind 型还是 action 型,再走不同处理分支。这个设计能跑,但不算优雅——同一个主题承载两种协议,端侧解析逻辑被迫写 if-else。更规整的做法是统一信封:{ "kind": "cmd", "action": ... }{ "kind": "reminder", ... },或干脆拆成 server/{code}/cmdserver/{code}/reminder 两个主题。属于"能跑但建议重构"的债。

五、完整链路:模型决定 → 组装 → 端侧执行

把从用户一句话到机器人动起来串成全景图:

用户:"宝贝,挥挥手"
   │
   ▼
ChatService.chat(userId, message)
   │  系统消息 = 人设 + 记忆 + 回复要求(PersonaProvider)
   ▼
大模型推理(LangChain4j Agent)
   │  看到工具描述:"向指定陪伴机器人设备下发动作:speak/gesture/light/wake/sleep"
   │  决定:调用 controlDevice,填参 deviceCode=?, action="gesture", param="wave"
   ▼
DeviceTool.controlDevice(deviceCode, action, param, userId)
   │  @ToolMemoryId 注入当前用户
   ▼
DeviceService.control
   │  ① 查设备 ② 校验归属 ③ 组装 {action:"gesture", param:"wave", ts}
   ▼
MqttClientManager.publish("server/A001/cmd", cmd)
   │  JSON 序列化 → MQTT 发布(QoS1)
   ▼
Broker 转发 → 设备 A001(它只订阅自己的 server/A001/cmd)
   │
   ▼
端侧解析 → 执行:舵机挥手 / TTS 播报 / 灯光亮起

链路上每一环都有明确的职责:模型管"要不要控制、控制什么",Service 管"能不能控制、怎么封装",MQTT 管"送到哪",端侧管"怎么执行"。任何一环失败,链条就断——所以下一步的可靠性问题必须摆上台面。

六、action 无枚举:隐患与收口方案

DeviceTool 描述里写了五种 action,但 DeviceService.control 没有任何校验——action 是任意字符串透传。模型填了 "sing""jump" 甚至 "rm_rf",都会原样组装进 payload 发给设备。隐患有三:

  1. 拼写漂移:模型可能填 "Speak""SPEAK",端侧不认识就静默忽略,用户体验是"机器人不理我"。
  2. 越权语义param 同样自由透传,若端侧某 action 会解释 param 为文件路径或音量数值,恶意/异常值没有服务端拦截层。
  3. 端云协议失控:没有"合法 action 清单"这个 single source of truth,端侧实现与模型描述极易失配。

收口方案(改动很小,收益很大):

// 示意:action 白名单收口(非项目源码)
private static final Set<String> ALLOWED_ACTIONS =
        Set.of("speak", "gesture", "light", "wake", "sleep");

if (!ALLOWED_ACTIONS.contains(action)) {
    throw new BusinessException("不支持的动作:" + action);
}

再进一步可以按 action 校验 param 形态(speak 必须有非空文本、light 只认 on/off),并让 DeviceTool 的描述与这份白名单保持同步——描述即协议,协议即校验。

七、指令幂等:重发同一条会怎样

MQTT QoS 1 意味着"至少一次",重复是常态而非异常。设想:网络抖动,Paho 重发了 {action:"speak", param:"该吃药啦", ts:1760000000000}——机器人会播报两遍。对 speak 这种幂等友好的动作(说两遍顶多重听一遍),问题不大;但对 "light" 开关这种非幂等动作,重复投递可能导致"开-关-开"抖动。

这就是 payload 里 ts 字段真正该发挥的作用——端侧做去重

// 示意:端侧基于 ts 的指令去重(非项目源码)
private long lastTs = 0;

void onCommand(String json) {
    long ts = parseTs(json);
    if (ts <= lastTs) { return; }   // 老指令/重复指令,丢弃
    lastTs = ts;
    execute(json);
}

配套的还有回执设计——当前实现完全没有回执:云端发布完就返回"已下发动作 speak 到设备 A001",注意这句话的真实含义只是"我把消息丢给了 Broker",设备收没收到、执行成功与否,云端一概不知。改进方向是端侧收到指令后向上行主题发一条 {event:"cmd_ack", action:"speak", ts:...},云端凭回执更新指令状态(sent/acked/failed)。有了回执,"未连接时静默丢弃"和"设备离线假装成功"这两个问题才有被观测到的可能。

八、设备不在线时的降级策略

最后直面那个组合问题:用户让机器人说句话,但机器人恰好离线(断网/没电/心跳超时误判),会发生什么?

当前实现的真实行为链:control 校验的是数据库状态位,不看真实连接(也看不了)→ publish 时如果云端到 Broker 的连接断着,log.warn 丢弃,接口仍返回成功文案;如果连接是好的,消息发到 Broker,但设备不在线,QoS1 消息在 cleanSession=true 下不会被保留,设备重连后也收不到。两种情况用户感知都一样:机器人没反应,但系统认为一切正常。

分层降级建议(按产品优先级排序):

  1. 下发前判在线:校验环节加"心跳时间差"判定(上一篇的心跳巡检思路),离线直接返回"机器人目前不在线",让用户知情——这已经比静默失败强得多。
  2. 时效性评估:提醒类消息离线丢失后,重连时判断"已过期"就不再补发,宁可错过不可错乱;重要提醒(如吃药)改走多通道兜底(短信/家属 App 推送)。
  3. 回执+重试:指令未收到回执时按指数退避重试 N 次,仍失败则告警工单。
  4. 队列缓冲:对"唤醒后可执行"的指令(如 sleep 期间的留言),Broker 侧或云端保留到设备上线。

九、小结

要点现状建议
工具入口controlDevice 五种 action 写进 @Tool 描述描述即协议,保持与白名单同步
校验设备存在 + 归属校验,action 透传action 白名单 + param 形态校验
报文{action,param,ts}{kind:"reminder",...} 两种形态共用一个主题统一信封或拆主题
幂等QoS1 可能重复,端侧未去重端侧按 ts 去重
回执增加 cmd_ack 上行
离线降级静默丢失,用户无感知前置在线判定 + 多通道兜底

指令下行是陪伴机器人从"能聊"到"能动"的关键一步。协议收敛、幂等去重、在线降级这三件事做了,链路才算从 Demo 走向可靠。

合规提醒:本篇的指令链路直接作用于物理实体(语音、灯光、动作),面向老人与儿童时需格外谨慎——speak 的 param 是大模型生成的自由文本,端侧应保留内容安全过滤;lightgesture 等动作不应包含惊吓性内容。儿童使用场景下,指令下发应有监护人可查的日志留痕,遵守未成年人个人信息保护与内容安全相关要求。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值