iotsaas/docs/AI实现.md
2995840376 0ce191c448 chore: 初始化 iotsaas 版本库基线
纳入范围:
- backend/  Spring Boot 多模块后端(iot-common/device/rule/data/alarm/auth)
- frontend/admin/  Vue3 + Element Plus 管理后台
- Android/   涂鸦原生 APP (com.xhai.iot)
- backend/sql/  Flyway 风格迁移脚本(V2~V21)
- docs/       对接文档与设计文档

排除范围:
- 构建产物: node_modules/ target/ build/ dist/ .gradle/
- 签名密钥: *.jks *.keystore (严禁入库)
- 本机配置: Android/local.properties
- 运行日志: logs/ *.log
- AI 协作本地数据 .codebuddy/ 与第三方 demo tuya-demo/

.gitattributes 统一以 LF 存储, Windows 批处理保留 CRLF
2026-10-27 12:00:41 +08:00

260 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

用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<HomeDevice> getDevices(Long memberId);
CommandResult controlDevice(Long memberId, String deviceId, List<Command> 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:新增 <queries> 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 执行层改为调用统一编排接口。