用AI自然语言控制设备/场景的实现方案 一、整体架构 你的需求本质上是 NLU(自然语言理解)→ 结构化指令 → 执行。核心是在现有"设备控制 + 规则引擎"能力之上,加一个 AI意图解析层。架构如下: 用户语音/文字 │ ▼ [APP / 前端] 采集指令 │ POST /api/v1/ai/command ▼ [AI 指令服务 iot-ai](新增) ├─ 1. 解析输入(语音→文本,已有则跳过) ├─ 2. 意图识别 + 槽位填充(LLM / 规则匹配) │ 识别:意图(开/关/调色温/设温度/启用场景) │ 目标(客厅开关、离家模式) ├─ 3. 实体解析(把"客厅开关"映射到真实 deviceId) │ └─ 用 Device.location + deviceName + deviceType 模糊匹配 ├─ 4. 执行映射(转成 DeviceCommand / 场景动作) ├─ 5. 执行 + 鉴权(ownerId 隔离) ▼ [现有服务] DeviceController.sendCommand / RuleEngineController.execute 二、核心设计:统一命令编排层(可挂多平台) 由于你们要兼容涂鸦/谷歌/华为/小米多平台(记忆里已规划 IPlatformService),AI 层不应直接调涂鸦,而应调用一个统一执行接口,这样才能做到"自然语言"不绑定平台。 建议在 iot-device 新增 IPlatformService 接口 + 统一编排,AI 层只认标准动作(如 {action:"turn_on", target:"device:xxx"} 或 {action:"execute_scene", scene_code:"leave_home"}),由编排层路由到对应平台的实现类: public interface IPlatformService { String platform(); // tuya / google / huawei / xiaomi / local / matter List getDevices(Long memberId); CommandResult controlDevice(Long memberId, String deviceId, List commands); // 场景可复用:每个平台实现自己的场景执行 } 三、关键步骤详解 1. 指令输入(语音) APP 端用系统语音识别(iOS SFSpeech / Android SpeechRecognizer / 或接入讯飞、腾讯云 ASR),或直接在 uniapp 里用语音插件。 语音转文本后,走同一个文本接口 POST /api/v1/ai/command,语音识别与意图解析解耦。 2. 意图识别 + 槽位填充(两种方案,建议结合) 方案A:LLM 结构化输出(推荐,能力强、好扩展) 调用大模型(GPT / 通义 / 文心,或自建)把自然语言转成 JSON: code 应用 方案B:本地规则引擎(离线、低延迟、免费) 不依赖大模型时,用"意图模板 + 关键词匹配": 动词表:开/关/打开/关闭/启动/调高/调低/设置温度到 X 度/切换… 名词表:从 Device 表动态加载(deviceName + location 组合) 例如正则:(打开|开启)的?(客厅|卧室)?\s*(开关|灯|空调) 命中后查库匹配 deviceId 建议:规则模板做兜底,LLM 做主力。LLM 无法匹配的自动降级到规则,两者都失败则返回"未找到设备/请再说一次"。 3. 实体解析(自然语言 → 真实设备ID)⭐ 核心难点 LLM 只给出"客厅开关"这种描述,必须映射到系统里的 deviceId。用 Device 表的字段匹配: location(客厅/卧室/厨房)→ 位置 deviceName + deviceType(开关/灯/空调)→ 设备类型 用户维度:必须限定 ownerId,防止串号(跨用户控制他人设备是安全问题) 匹配策略(按优先级): 精确:location + deviceName 完全命中 模糊:别名表 / 分词后包含匹配 多结果:客厅有两个灯 → 返回候选让用户确认(或按主设备默认) 执行层最后生成标准动作: { "deviceId": "xxx", "commands": [{"code":"switch_1","value":true}] } 调涂鸦就走 TuyaCloudService.sendCommand(memberId, deviceId, commands),调本地/其它平台走对应实现。 4. 场景执行("启用离家模式") 场景本质是"一组预设动作",你们已有 linkage_rule 表(RuleEntity 有 ruleName/actions),RuleEngineController.execute 支持手动执行。 推荐做法: 方案①(复用现有):把"离家模式"建成一条规则,ruleName="离家模式",actions 存一组关灯/关空调/关窗帘动作,AI 识别到 execute_scene 意图后,按场景名模糊匹配 ruleName,调 POST /api/v1/rules/{ruleId}/execute。 方案②(正式场景表):新建 scene 表(scene_name, actions JSON, ownerId),AI 命中后调场景服务执行。这个更贴合智能家居"场景"概念,扩展性更好。 四、新增接口(建议) 在 iot-device(或独立 iot-ai 微服务)新增 AI 统一入口: POST /api/v1/ai/command // 文本指令 → 执行 → 返回结果 入参: { "text": "打开客厅开关", "type": "text" } // type: text/voice 出参: { intent, matched_device/scene, executed, result, candidateList } POST /api/v1/ai/devices // 供 LLM 匹配用的设备清单(带location/name/type) 五、安全与多用户隔离 所有 AI 执行接口都要解析 Token,强制 ownerId 隔离(你们 DeviceController 已实现 member 隔离,AI 层同样套用)。 LLM 返回的动作只能操作当前用户已绑定的设备/场景,不允许传任意 deviceId。 危险动作(如"离家模式"关闭所有)可加二次确认。 六、落地顺序建议 **先做方案B(规则模板)**打通"打开客厅开关"这类固定句式 —— 验证全链路,成本低。 再加 LLM 解析替换模板 —— 支持"离家模式"、复杂参数("空调调到26度")。 补多平台抽象层 —— 接入涂鸦后的谷歌/华为/小米(配合已有的 IPlatformService 规划)。 补前端:uniapp 语音输入 + 意图结果展示(已执行的设备/场景、未匹配时的候选确认)。 需要我帮你动手落地的话,告诉我选哪一步,我可以直接实现: A. 新增 iot-ai 意图解析服务(含 LLM 对接 + 规则模板兜底 + 实体匹配 + 统一执行接口) B. 只做本地规则模板版(不依赖大模型,先跑通) C. 新增 scene 场景表 + 场景执行接口(为"离家模式"打基础) D. 统一多平台 IPlatformService 抽象(涂鸦/谷歌/华为/小米) --- 七、Phase 3 实施进展与 ai.llm 配置说明(当前已落地) 7.1 分层解析架构(已实现) 按"规则为主、大模型兜底"实现于 iot-rule 内(无需独立 iot-ai 服务): - L0 规则模板:RuleBasedIntentParser + AiLexicon(内置词库 + 位置扫描),零成本、零延迟、可离线,覆盖"打开客厅开关""启用离家模式""空调调到26度"等高频句式。 - L1 大模型兜底:LlmProviderRouter + QwenLlmProvider(通义千问,OpenAI 兼容协议),仅在 L0 未识别时调用,用于理解复杂/模糊表达。 - 降级策略:LLM 未配置/超时/报错(内部 catch)→ 静默回退 L0 结果,设备控制可用性不受影响。 - 安全:LLM 提示词明确禁止输出设备ID,仅输出语义槽位(意图/动作/房间/品类/数值);设备ID始终由 AiEntityResolver 按 ownerId 本地匹配,天然防越权与幻觉。 7.2 大模型凭证配置(两种方式,推荐后台页面) 优先级:后台「平台集成」→ 阿里云 > application.yml 的 ai.llm.* > 未配置(仅走 L0 规则模板)。 方式一(推荐,运维友好,无需改配置/重启):后台管理 → 系统管理 → 平台集成 → 切到「阿里云(通义千问)」标签页 - 填写 DashScope API Key(必填)、模型名称(建议 qwen-turbo)、API 基础地址、超时时间等,保存后打开「启用此平台」开关 - 点击「测试连接」可一次性验证 Key + 模型名 + 基础地址是否正确(内部发一条 max_tokens=1 的极小请求) - 保存后最多 30 秒自动生效(LlmConfigResolver 本地缓存 30s) - 存储位置:platform_config 表 platform_code='aliyun',字段 api_key / model_name / base_url / timeout_ms / region(V13__add_aliyun_platform_config.sql) - 接口:GET/PUT /api/v1/admin/platform-config/aliyun、PUT .../toggle、POST .../test 方式二(本地开发 / 兜底):iot-rule/src/main/resources/application.yml | 配置项 | 默认值 | 说明 | | --- | --- | --- | | ai.llm.enabled | false | 总开关,需同时填写 api-key 才生效 | | ai.llm.provider | qwen | 服务商,当前实现 qwen(OpenAI 兼容协议,便于平移其他厂商) | | ai.llm.api-key | 空 | 阿里云 DashScope API Key,形如 sk-xxxxxxxx | | ai.llm.base-url | https://dashscope.aliyuncs.com/compatible-mode/v1 | API 基础地址 | | ai.llm.model | qwen-turbo | 模型名,可选 qwen-turbo / qwen-plus / qwen-max | | ai.llm.timeout-ms | 3000 | 连接/读取超时(智能家居对延迟敏感) | | ai.llm.temperature | 0.0 | 采样温度,指令解析需稳定,建议 0 | 两种方式都未配置时只走 L0,无需任何外部依赖,零成本可离线。 代码映射: - AiLlmProperties(@ConfigurationProperties(prefix="ai.llm")):yml 配置 - LlmConfigResolver:合并「后台平台配置」与「application.yml」得到生效配置 LlmConfig,30 秒缓存,异常时静默回退 - LlmProviderRouter 按 provider 名称选实现类,厂商无关(新增厂商只需实现 LlmProvider 并注册 Bean) - QwenLlmProvider:调用方,baseUrl/超时变化时自动重建 HTTP 客户端 - iot-rule 通过 PlatformConfigMapper(只读)读取 iot-device 维护的同一张 platform_config 表 7.3 AI 指令码来源(ai_device_profile)— 本次新增自动探测 AI 控制需要设备"真实的"DP 指令码(如 switch_1 / switch_led / bright_value),三种来源按优先级: 1. manual(用户校准):PUT /api/v1/ai/devices/{deviceId}/profile,最高优先级,自动探测不会覆盖(除非 force=true)。 2. auto(状态自动探测,本次新增):iot-device 调用涂鸦 GET /v1.0/devices/{deviceId}/status,从真实上报的 DP 中识别开关/亮度/色温/温度/模式指令码后写入。比"按设备类型猜测"更准(同型号灯也可能是 switch_led 或 switch_1)。 3. type-default(类型默认):iot-rule 在无档案时按设备类型推断兜底。 iot-device 侧新增能力: - 新增 AiDeviceProfileDetectService / AiDeviceProfileMapper / AiDeviceProfile 实体 / AiDeviceProfileDTO(位于 iot-device,因为只有它能访问涂鸦云;iot-rule 侧只读)。 - 识别规则:开关码优先 switch_led > switch > switch_1 > switch_2 > ... > control > power,前缀兜底 switch/control/power/socket;亮度 bright_value(含 bright);色温 temp_value;温度 temp_set;模式 mode;开关下发值按当前值推断 true/false、on/off、open/close。 - 绑定设备后自动触发异步批量探测(TuyaController 绑定流程,best-effort,不阻塞绑定返回)。 - 新增接口: GET /api/v1/tuya/devices/{deviceId}/ai-profile 查询档案 POST /api/v1/tuya/devices/{deviceId}/ai-profile/detect 探测并保存(?force=true 可覆盖手动档案) POST /api/v1/tuya/ai-profile/detect-all 批量探测当前用户全部涂鸦设备 - 依赖涂鸦 /status,设备需在线且涂鸦账号已绑定;探测失败保留旧档案,不影响已有控制能力。 7.4 AI 控制接口(iot-rule,已实现) - POST /api/v1/ai/command:文本指令 → 执行 → 返回可直接语音播报的文案(歧义时返回 needConfirm + 候选,二次确认后自动学习为别名) - GET /api/v1/ai/devices:可被 AI 控制的设备清单 - GET /api/v1/ai/devices/{deviceId}/profile:查询指令码档案 - PUT /api/v1/ai/devices/{deviceId}/profile:手动校准指令码(source=manual) - GET /api/v1/ai/logs:AI 指令日志 仅支持 member 身份(强制 ownerId 隔离)。 7.5 验证情况 - RuleBasedIntentParserTest:12/12 用例通过(以 JUnit Platform 直接运行,规避 Windows 下 surefire fork 的 Manifest-JAR classpath 问题)。 - iot-device 模块 mvn -pl iot-device -am -DskipTests compile 编译通过。 - 环境注意:本机 Windows 下 surefire 因 Manifest-JAR 含绝对路径 / 跨盘符根导致 fork VM 启动失败;官方规避参数为 -Djdk.net.URLClassPath.disableClassPathURLCheck=true(本地/CI 跑 mvn test 时可加上)。 7.6 APP 端(原生 Android)AI 助手 — 本次完善 入口:MainActivity 的 AI 悬浮按钮(btn_ai_float)**单击进入助手页 / 长按直接语音控制**(长按携带 EXTRA_AUTO_VOICE,进页后自动唤起一次识别);HomeFragment 的 AI 助手按钮(btn_ai_home)→ AiAssistantActivity。 接入方式:Retrofit(ApiService / ApiClient)调用 iot-rule 的 AI 接口,必须用 ApiClient.getSceneApiService()(AI 接口在 9083,不是 9081)。 ⚠️ 语音输入的设备兼容性(实测阻塞): 系统 SpeechRecognizer 依赖设备内置 android.speech.RecognitionService。测试机(ColorOS,PKT110,com.google.android.gms 被禁用且未预装 Google 语音服务)实测: - pm query-services -a android.speech.RecognitionService → No services found - am start -a android.speech.action.RECOGNIZE_SPEECH → unable to resolve Intent 因此 isRecognitionAvailable() 返回 false;国内无 GMS 的 ROM 普遍如此。 已实现兜底 fallbackToKeyboard():检测到无识别服务时 Toast 说明原因,并自动聚焦输入框、拉起软键盘,保证长按后不会"没反应"。 语音方案选型:已接入讯飞「语音听写(流式版)」WebAPI(不引入 SDK/so,纯代码 + wss 长连接,避免下载二进制 SDK)。 识别优先级(APP 端运行时自动选择,无需发版):后台讯飞(已启用且凭证齐全)> 系统 SpeechRecognizer(可用时)> 键盘兜底。 7.7 讯飞语音听写 · 后台平台集成(本次,凭证不再放 APP 端) 架构:APISecret 只存服务端。APP 长按麦克风 → GET /api/v1/ai/speech/auth(member 身份)领取带签名的临时 wss 地址 → 直连 iat-api.xfyun.cn 流式识别。凭证不下发客户端,签名时效受讯飞约束(约 5 分钟)。 后台配置(运维在管理页维护,无需改代码/重启): - 前端「系统管理 → 平台集成」新增第三个平台「讯飞(语音听写)」(frontend/admin/src/views/System/PlatformConfig.vue) - 字段映射(复用 platform_config 现有列):access_id=APPID,api_key=APIKey,access_secret=APISecret - sql/V14__add_xfyun_platform_config.sql 已在本地库执行(id=3,默认停用,填 key 后启用) - 「测试连接」会真实发起一次 WebSocket 握手验证凭证(凭证错时讯飞在握手阶段返回 401),比仅校验非空可靠 - 分层解析(与 LLM 配置一致):platform_config(xfyun, enabled=1 且三项齐全) > application.yml 的 ai.xfyun.*(兜底)> 不可用;30 秒缓存自动生效 后端新增/改动: - iot-common/util/XfyunAuthUtil.java:HMAC-SHA256 鉴权 URL 生成(服务端共用) - iot-rule/ai/speech/XfyunSpeechProperties / XfyunSpeechConfig / XfyunSpeechConfigResolver:配置解析(仿 LlmConfigResolver) - iot-rule/controller/AiSpeechController.java:GET /api/v1/ai/speech/auth → { enabled, appId, url };未启用时 enabled=false,APP 据此回退 - iot-device PlatformConfigService.testConnection:新增 xfyun 分支(WebSocket 握手验证) - iot-rule application.yml:新增 ai.xfyun 兜底配置段(默认 enabled=false) APP 端改动(凭证相关代码全部移除): - 删除 XfyunConfig / XfyunAuthUtil(不再硬编码凭证、不再本地签名) - 新增 model/SpeechAuth.java + ApiService.getSpeechAuth():先领签名地址再识别 - speech/XfyunIatClient.java:start(appId, wssUrl) 接收后端签发的参数;录音/分帧/wpgs 动态修正/自动发送逻辑不变 - ui/AiAssistantActivity.java:startListening() 先调鉴权接口 → enabled ? 讯飞 : startSystemListening()(系统识别/键盘回退);聆听中再点麦克风可提前结束 接入步骤(运维):console.xfyun.cn 创建应用 → 开通「语音听写(流式版)」→ 平台集成页填 APPID/APIKey/APISecret → 测试连接 → 启用。最多 30 秒生效。 注意:讯飞要求服务器时间与标准时间偏差小于 5 分钟(签名 date),时间不准会鉴权失败(10161)。 验证: - 后端 mvn compile / Android assembleDebug / 前端 vue-tsc 全绿 - iot-rule(9083)/iot-device(9081) 已重启加载新接口;GET /api/v1/ai/speech/auth 无 token → 401(鉴权生效) - platform_config 表已有 xfyun 记录(enabled=0 待填 key) - 待办:手机插回后安装新 APK 端到端回归;后台填入真实讯飞凭证后验证完整识别链路 新增/改动文件: - model/AiCommandResult.java:对应 AiCommandResponse,含内部类 Candidate(targetType/targetId/targetName/score) - model/AiDeviceInfo.java:对应 AiDeviceView - data/ApiService.java:新增 aiCommand(POST v1/ai/command)、getAiDevices(GET v1/ai/devices)、getAiLogs(GET v1/ai/logs) - ui/AiAssistantActivity.java:由"本地模拟回复"改为真实链路 - res/layout/item_ai_msg_ai.xml:新增 tv_meta(来源·耗时)与 sv_candidates/ll_candidates(歧义候选) - res/layout/activity_ai_assistant.xml:新增 tv_status(真实在线状态)、btn_history(指令记录) - AndroidManifest.xml:新增 android.speech.RecognitionService(Android 11+ 语音识别包可见性) 关键行为: - 发指令 → 先插入「思考中」气泡 → 结果回来后就地替换,避免列表跳动;同时置位 sending 防止并发重复下发。 - 解析来源标注:气泡下方展示「由规则模板理解 · 10ms」/「由大模型理解 · 1240ms」,让用户可感知是 L0 还是 L1 生效。 - 歧义二次确认:needConfirm=true 时渲染候选 chip;点选后以「原始说法 + targetType + targetId + learn=true」重新下发,服务端顺势学习别名,下次同样说法可直接命中。 7.3 聚合指令支持("打开所有灯 / 关闭全部开关") - 背景:用户反馈"打开所有灯"未理解,"打开所有开关"只命中了一个名为"2位开关"的设备;智能级别需支持"所有/全部/全屋"批量控制。 - 后端改动: - 新增 AiTargetScope(SINGLE / ALL)和 AiIntent.targetScope,规则模板识别"所有/全部/一切/全屋/通通/统统/一律"后把 targetScope 置为 ALL。 - RuleBasedIntentParser.buildDeviceHint 去掉聚合词,避免"所有开关"被当成单个设备名。 - AiEntityResolver.matchAll(intent, ownerId):按房间 + 品类筛选全部可控设备,返回 targetType=device_group,targetId 为逗号分隔的 deviceId 串。 - AiControlServiceImpl 新增 doExecuteDeviceGroup:拆开 deviceId 串逐一下发,单个失败不影响其他设备,汇总为"好的,已打开「3个灯」"。 - matched_id 只存 device_group:count(表字段仅 64 字符),matched_name 存"3个灯" / "客厅2个灯"。 - 前端/APP:胶囊里增加「打开所有灯 / 关闭所有开关」等聚合快捷入口。 7.4 快捷建议:在线、常用设备优先 - 背景:用户反馈底端胶囊应展示在线、常用设备,而不是默认前 3 个设备(截图里出现了 DMC162-vdevo1 这类网关/调试设备)。 - 后端:AiDeviceView 增加 status 字段;listControllableDevices 排序:在线(online)优先 → 常用类型(light/switch/socket/air_conditioner/fan/curtain/sensor)优先 → 名称短优先。 - Android:AiDeviceInfo 增加 status/useCount/lastUsedTime;renderSuggestions 按同样规则排序;状态栏从「在线 · 可控设备 N 台」改为「在线 X 台 · 可控设备 N 台」;同时为灯/开关/空调追加聚合建议 chip。 7.5 验证(真实 member 身份端到端) - 后端 mvn compile / Android assembleDebug / 前端 npm run build 全绿;RuleBasedIntentParserTest 新增 3 个聚合指令用例。 - iot-rule(9083) / iot-device(9081) 已重启加载新接口。 - 待办:测试手机插回后安装新 APK,实测「打开所有灯」「关闭全部开关」批量执行与快捷建议排序。 八、后续可选项 - uniapp 端尚未接入 AI 助手(当前仅原生 Android 已打通,见 7.6)。 - 已知缺陷:member 注册未传 phone 时会写入空串,与 uk_member_phone 唯一索引冲突导致第二个无手机号用户注册失败(本次验证时复现),建议改为不写该列或存 NULL。 - L1 增加多厂商实现(文心 / DeepSeek / 自建)与灰度、降级观测指标。 - ai_device_profile 增加前端可视化校验入口(当前可用涂鸦 GET /status 对照)。 - 多平台 IPlatformService 抽象落地后,AI 执行层改为调用统一编排接口。