13-AI智能管家模块速记

AI 智能管家模块速记(面试必备)

用途:5 分钟讲清本项目「AI 智能管家」模块怎么做的、能干啥、关键技术

适合:面试官问「你们的 AI 智能管家怎么实现的」「怎么让 AI 控制设备」「Function Calling 怎么用」时脱口而出

核心思想:RN + WebView + H5 + 智能体云服务(调度 LLM)+ 工具调用(Function Calling)


一、一张图速记

#mermaid-svg-DusTSV3uL8JU2sQz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DusTSV3uL8JU2sQz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DusTSV3uL8JU2sQz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DusTSV3uL8JU2sQz .error-icon{fill:#552222;}#mermaid-svg-DusTSV3uL8JU2sQz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DusTSV3uL8JU2sQz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DusTSV3uL8JU2sQz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DusTSV3uL8JU2sQz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DusTSV3uL8JU2sQz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DusTSV3uL8JU2sQz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DusTSV3uL8JU2sQz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DusTSV3uL8JU2sQz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DusTSV3uL8JU2sQz .marker.cross{stroke:#333333;}#mermaid-svg-DusTSV3uL8JU2sQz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DusTSV3uL8JU2sQz p{margin:0;}#mermaid-svg-DusTSV3uL8JU2sQz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DusTSV3uL8JU2sQz .cluster-label text{fill:#333;}#mermaid-svg-DusTSV3uL8JU2sQz .cluster-label span{color:#333;}#mermaid-svg-DusTSV3uL8JU2sQz .cluster-label span p{background-color:transparent;}#mermaid-svg-DusTSV3uL8JU2sQz .label text,#mermaid-svg-DusTSV3uL8JU2sQz span{fill:#333;color:#333;}#mermaid-svg-DusTSV3uL8JU2sQz .node rect,#mermaid-svg-DusTSV3uL8JU2sQz .node circle,#mermaid-svg-DusTSV3uL8JU2sQz .node ellipse,#mermaid-svg-DusTSV3uL8JU2sQz .node polygon,#mermaid-svg-DusTSV3uL8JU2sQz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DusTSV3uL8JU2sQz .rough-node .label text,#mermaid-svg-DusTSV3uL8JU2sQz .node .label text,#mermaid-svg-DusTSV3uL8JU2sQz .image-shape .label,#mermaid-svg-DusTSV3uL8JU2sQz .icon-shape .label{text-anchor:middle;}#mermaid-svg-DusTSV3uL8JU2sQz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DusTSV3uL8JU2sQz .rough-node .label,#mermaid-svg-DusTSV3uL8JU2sQz .node .label,#mermaid-svg-DusTSV3uL8JU2sQz .image-shape .label,#mermaid-svg-DusTSV3uL8JU2sQz .icon-shape .label{text-align:center;}#mermaid-svg-DusTSV3uL8JU2sQz .node.clickable{cursor:pointer;}#mermaid-svg-DusTSV3uL8JU2sQz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DusTSV3uL8JU2sQz .arrowheadPath{fill:#333333;}#mermaid-svg-DusTSV3uL8JU2sQz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DusTSV3uL8JU2sQz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DusTSV3uL8JU2sQz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DusTSV3uL8JU2sQz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DusTSV3uL8JU2sQz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DusTSV3uL8JU2sQz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DusTSV3uL8JU2sQz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DusTSV3uL8JU2sQz .cluster text{fill:#333;}#mermaid-svg-DusTSV3uL8JU2sQz .cluster span{color:#333;}#mermaid-svg-DusTSV3uL8JU2sQz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DusTSV3uL8JU2sQz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DusTSV3uL8JU2sQz rect.text{fill:none;stroke-width:0;}#mermaid-svg-DusTSV3uL8JU2sQz .icon-shape,#mermaid-svg-DusTSV3uL8JU2sQz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DusTSV3uL8JU2sQz .icon-shape p,#mermaid-svg-DusTSV3uL8JU2sQz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DusTSV3uL8JU2sQz .icon-shape .label rect,#mermaid-svg-DusTSV3uL8JU2sQz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DusTSV3uL8JU2sQz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DusTSV3uL8JU2sQz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DusTSV3uL8JU2sQz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 🛠️ 业务后端(自家服务器)
☁️ 智能体云服务(厂商托管)
📄 H5 端(独立工程,部署在 CDN)
📱 RN 客户端(手机 App)
加载 H5
对话请求
调 LLM
返回工具调用指令
返回指令给 H5
postMessage
分发 handler
调业务 API
返回结果
sendMessage
injectJavaScript
渲染聊天界面
👆 用户

点「星灵」按钮
📲 WebView 组件

(react-native-webview)

只负责显示 H5
🔌 JSBridge

(postMessage + injectJavaScript)

在 RN 项目里
🔧 本地工具 handlers

(doorOpen / getUserAllDoor /

getToken / callPhone ...)
📄 H5 聊天页

(web/agent

Vite 独立工程)
🧠 调度服务

()

调度 LLM + 工具注册
🤖 大模型 LLM

(GPT / 通义 / 文心)

只决策不执行
🚪 业务 API

(开门/查设备/查公告)

一句话总结 :「用户点 AI 入口 → WebView 加载 H5 → H5 调智能体云服务 → 智能体云服务调 LLM 理解意图 → LLM 决定调哪些工具 → JSBridge 把工具调用桥接到 RN 客户端 → 原生 handler 调业务 API」


二、4 块构成 + 各自职责

模块 在哪部署 干什么
RN 客户端 手机 App 显示界面、提供原生能力(开门、调相册、调电话)
H5 聊天页 云端(CDN) 显示聊天 UI,发对话请求
智能体云服务 云端(厂商托管,<YOUR_AGENT_DOMAIN>) 调度 LLM、管理工具注册表
大模型 LLM 云端(OpenAI/通义等) 理解用户意图,决策调哪个工具
业务后端 API 云端(自家 ECS) 开门、查设备、查公告等真实业务

RN 客户端内部 4 层

复制代码
RN 客户端(手机 App)
├── React 组件层
│   └── BottomTabBar(AI 入口「星灵」按钮)
├── WebView 容器层
│   └── pages/WebView.js(react-native-webview 组件,只负责显示 H5)
├── JSBridge 桥接层
│   ├── utils/webViewHandlers.js(消息分发)
│   └── utils/agentToolHandlers/(工具实现)
└── 本地工具层
    └── 11 个原生 handler(getToken / doorOpen / callPhone ...)
概念 是什么
WebView react-native-webview 提供的 UI 组件,只负责显示 H5,是「浏览器壳子」
JSBridge 一套通信协议 + 工具分发逻辑,是「翻译官」,让 H5 和原生互相调用
本地工具 handler 普通 JS 函数 ,有名字(doorOpen)、有参数、有返回值,内部调业务 API

三、用户视角:能干什么?

业主在 App 里点底部中间的「星灵」按钮,就能跟 AI 对话:

复制代码
业主:「帮我打开小区大门的门禁」
  ↓
AI 管家(自动识别意图):
  1. 调用 getBuildingInfo → 拿当前楼宇 ID
  2. 调用 getUserAllDoor → 拿门禁设备列表
  3. 找到匹配的门禁
  4. 调用 doorOpen → 开门
  5. 回复业主:「已为您打开一号楼大门」
场景 用户说法 AI 自动调用
开门 「帮我开一号楼大门」 getBuildingInfo + getUserAllDoor + doorOpen
查服务电话 「服务中心电话多少」 getServicePhone
联系管家 「我要找管家」 跳到 ButlerList 页面
社区公告 「最近有什么活动」 调自家公告 API
设备控制(规划中) 「把客厅灯关了」 调 MQTT 控制设备(项目暂未接入)

四、6 步实现

Step 1:AI 入口(TabBar 中间按钮)

文件 :src/components/BottomTabBar/index.js

javascript 复制代码
import {AGENT_URL} from '@env';  // 从 .env 读 URL

const handleAgentEntry = () => {
  navigation.navigate('WebView', {
    uri: AGENT_URL,     // 跳到 H5 聊天页
    header: false,
  });
};

Step 2:WebView 容器(加载 H5)

文件 :src/pages/WebView.js

javascript 复制代码
import {WebView} from 'react-native-webview';

<WebView
  source={{uri: route.params.uri}}
  onMessage={this.handleWebViewMessage}  // H5 → 原生 的消息入口
  injectedJavaScript={...}                // 注入安全区域等
  onShouldStartLoadWithRequest={...}      // 拦截外链
/>

Step 3:JSBridge 桥接(核心)

双向通信机制(10 步,每步对应一个动作):
业务后端 原生 handler webViewHandlers.js WebView H5 (web/agent) 业务后端 原生 handler webViewHandlers.js WebView H5 (web/agent) #mermaid-svg-eBT3JjYp1TY9o0W2{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eBT3JjYp1TY9o0W2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eBT3JjYp1TY9o0W2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eBT3JjYp1TY9o0W2 .error-icon{fill:#552222;}#mermaid-svg-eBT3JjYp1TY9o0W2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eBT3JjYp1TY9o0W2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eBT3JjYp1TY9o0W2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eBT3JjYp1TY9o0W2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eBT3JjYp1TY9o0W2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eBT3JjYp1TY9o0W2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eBT3JjYp1TY9o0W2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eBT3JjYp1TY9o0W2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eBT3JjYp1TY9o0W2 .marker.cross{stroke:#333333;}#mermaid-svg-eBT3JjYp1TY9o0W2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eBT3JjYp1TY9o0W2 p{margin:0;}#mermaid-svg-eBT3JjYp1TY9o0W2 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eBT3JjYp1TY9o0W2 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-eBT3JjYp1TY9o0W2 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-eBT3JjYp1TY9o0W2 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-eBT3JjYp1TY9o0W2 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-eBT3JjYp1TY9o0W2 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-eBT3JjYp1TY9o0W2 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-eBT3JjYp1TY9o0W2 .sequenceNumber{fill:white;}#mermaid-svg-eBT3JjYp1TY9o0W2 #sequencenumber{fill:#333;}#mermaid-svg-eBT3JjYp1TY9o0W2 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-eBT3JjYp1TY9o0W2 .messageText{fill:#333;stroke:none;}#mermaid-svg-eBT3JjYp1TY9o0W2 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eBT3JjYp1TY9o0W2 .labelText,#mermaid-svg-eBT3JjYp1TY9o0W2 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-eBT3JjYp1TY9o0W2 .loopText,#mermaid-svg-eBT3JjYp1TY9o0W2 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-eBT3JjYp1TY9o0W2 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-eBT3JjYp1TY9o0W2 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-eBT3JjYp1TY9o0W2 .noteText,#mermaid-svg-eBT3JjYp1TY9o0W2 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-eBT3JjYp1TY9o0W2 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eBT3JjYp1TY9o0W2 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eBT3JjYp1TY9o0W2 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-eBT3JjYp1TY9o0W2 .actorPopupMenu{position:absolute;}#mermaid-svg-eBT3JjYp1TY9o0W2 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-eBT3JjYp1TY9o0W2 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-eBT3JjYp1TY9o0W2 .actor-man circle,#mermaid-svg-eBT3JjYp1TY9o0W2 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-eBT3JjYp1TY9o0W2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} H5 → 原生 的入口 WebView 的 onMessage 回调 查 handlers 表,找到对应方法 注意是 ctx.sendMessage不是 webViewRef.sendMessageToWebView ① postMessage({method, params, requestId})1② onMessage 事件触发2③ handleWebViewMessage(method, params, requestId) 分发3④ 调用对应 handler4⑤ 调业务 API5⑥ 返回数据6⑦ 调用 ctx.sendMessage(method, data, requestId)7⑧ 调用 webViewRef.injectJavaScript 注入 JS8⑨ dispatchEvent nativeMessage 事件9⑩ 拿到数据,渲染到聊天界面10

H5 → 原生(步骤 ①,H5 主动发起):

javascript 复制代码
window.ReactNativeWebView.postMessage(JSON.stringify({
  method: 'doorOpen',
  params: {deviceSerial, buildingId, employeeNo},
  requestId: 'req-123',
}));

原生 → H5(步骤 ⑦→⑧→⑨,handler 完成后回传):

javascript 复制代码
// 步骤 ⑦:在 handler 里调用 ctx.sendMessage
// doorOpenHandler.js
const result = await doorOpen({deviceSerial, buildingId, employeeNo});
ctx.sendMessage('doorOpen', result, requestId);
//      ↑ 这里的 ctx.sendMessage 实际就是 WebView.js 里的 sendMessageToWebView

// 步骤 ⑧:sendMessageToWebView 内部调 webViewRef.injectJavaScript
sendMessageToWebView = (method, data, requestId) => {
  const jsCode = `
    window.dispatchEvent(new CustomEvent('nativeMessage', {
      detail: { method: '${method}', requestId: '${requestId}', data: ${JSON.stringify(data)} }
    }));
    true;
  `;
  this.webViewRef.current?.injectJavaScript(jsCode);
};

// 步骤 ⑨:H5 监听 nativeMessage 事件
window.addEventListener('nativeMessage', e => {
  const {method, requestId, data} = e.detail;
  // method = 'doorOpen',data = 开门结果
});

步骤对应方法速查表

步骤 谁 调什么 代码位置
① H5 window.ReactNativeWebView.postMessage H5 端 SDK
② WebView onMessage 回调(自动触发) WebView.js prop
③ webViewHandlers.js handleWebViewMessage 函数 utils/webViewHandlers.js
④ webViewHandlers.js handlers[method](按 method 查表) 同上
⑤ 原生 handler 调业务 API(fetch) utils/agentToolHandlers/doorHandlers.js
⑥ 业务后端 返回数据 ---
⑦ 原生 handler ctx.sendMessage(method, data, requestId) doorHandlers.js
⑧ webViewHandlers.js → WebView.js ctx.sendMessage → webViewRef.injectJavaScript WebView.js
⑨ H5 window.dispatchEvent('nativeMessage')(注入的 JS 触发) H5 监听
⑩ H5 拿到数据,渲染聊天界面 H5 端 SDK

你提的关键问题解答

「getToken/getUserInfo 是步骤 ④ 还是步骤 ⑦?」

答:它们既是步骤 ④ 的方法名,又是步骤 ⑦ 调用的方法名。 具体看代码:

javascript 复制代码
// === 步骤 ④:handler 表里的方法名 ===
const handlers = {
  getToken: async (params, requestId, ctx) => {
    // 内部直接调 ctx.sendMessage(步骤 ⑦)
    const token = await getToken();
    ctx.sendMessage('getToken', token, requestId);
    //                ↑ 这里调用了 sendMessage(步骤 ⑦)
  },
  // ...
};

getToken 这个名字 既是:

  • handler 的方法名 (步骤 ④ 查 handlers['getToken'])
  • ctx.sendMessage 的第一个参数(步骤 ⑦ 'getToken' 作为返回值 method)

两者必须同名,因为:

  • 步骤 ④:handlers['getToken'] 表示「H5 想调 getToken 这个能力」
  • 步骤 ⑦:sendMessage('getToken', ...) 表示「回传的数据 method 是 getToken,H5 通过 method 名字区分是哪个请求的响应」

简单说:handler 名 = sendMessage 名 = method 名,三者一致。

「H5 给 RN 通信定义的方法是咋样的?」

约定俗成的方法名格式:

javascript 复制代码
// 步骤 ①:H5 主动发起
{
  method: '方法名',      // 用驼峰命名
  params: {...},         // 参数对象
  requestId: '唯一ID'    // 关联响应
}

// 步骤 ⑦:原生响应
ctx.sendMessage('方法名', result, requestId);
//                  ↑ 同一个方法名

项目里的方法名规范:

类型 命名规则 例子
拉数据(查) getXxx getToken / getUserInfo / getAgentContext / getServicePhone
查列表 queryXxx / getUserXxx queryBuildings / getUserAllDoor
执行动作 verbXxx doorOpen / verifyDoorHealth / callPhone
跳转 goXxx goBack
「RN 返回消息给 H5 是怎样的方法命名?」

命名 = handler 名(一一对应):

复制代码
H5 发起:method: 'doorOpen'
RN 响应:ctx.sendMessage('doorOpen', result, requestId)

为什么必须同名?因为 H5 端 SDK 是这样匹配响应的(伪代码):

javascript 复制代码
// H5 端 SDK 内部(简化)
const pendingRequests = new Map();  // requestId -> {resolve, reject, method}

// H5 发请求时
window.ReactNativeWebView.postMessage({method: 'doorOpen', requestId: 'req-123'});
pendingRequests.set('req-123', {method: 'doorOpen', resolve, reject});

// H5 收到响应时(nativeMessage 事件)
window.addEventListener('nativeMessage', e => {
  const {method, requestId, data} = e.detail;
  const pending = pendingRequests.get(requestId);
  // 用 method 校验是不是同一个请求的响应(防误匹配)
  if (pending && pending.method === method) {
    pending.resolve(data);
    pendingRequests.delete(requestId);
  }
});

Step 4:工具处理器(按 method 分发)

文件 :src/utils/webViewHandlers.js

javascript 复制代码
const handlers = {
  ...doorHandlers,
  goBack: (params, requestId, ctx) => ctx.navigation.goBack(),
  getToken: async (params, requestId, ctx) => {
    const token = await getToken();
    ctx.sendMessage('getToken', token, requestId);
  },
  getUserInfo: async (params, requestId, ctx) => {
    ctx.sendMessage('getUserInfo', ctx.user.userInfo, requestId);
  },
  getAgentContext: async (params, requestId, ctx) => {
    ctx.sendMessage('getAgentContext', buildAgentContext(...), requestId);
  },
};

export async function handleWebViewMessage(method, params, requestId, context) {
  const handler = handlers[method];
  if (!handler) return console.warn('Unknown:', method);
  await handler(params, requestId, context);
}

当前已实现的工具(11 个):

工具名 作用 内部调什么
getCloudTalkUserInfo 拿云对讲用户信息 api.getCloudTalkUserInfo()
queryBuildings 拿楼栋列表 api.queryBuildingsNew()
getUserAllDoor 拿门禁列表 api.getUserAllDoorNew()
verifyDoorHealth 验证开门权限 api.verifyDoorHealth()
doorOpen 执行开门 api.doorOpen()
getBuildingInfo 拿当前楼宇 直接从 redux 取
getBindingStatus 拿房屋绑定状态 直接从 redux 取
getToken 拿登录 token storage.getToken()
getUserInfo 拿用户信息 直接从 redux 取
getAgentContext 拿 agent 上下文 从 redux 拼
getServicePhone 拿服务中心电话 直接从 redux 取
callPhone 拨打电话 Linking.openURL('tel:...')

🎯 完整案例:「帮我开一号楼大门」

这是 AI 智能管家最经典的场景,包含 3 个 handler 调用 + 4 次 H5↔RN 通信。

全流程时序图

业务后端 原生 handlers webViewHandlers WebView 大模型 智能体云服务 H5 聊天页 用户 业务后端 原生 handlers webViewHandlers WebView 大模型 智能体云服务 H5 聊天页 用户 #mermaid-svg-9dl3pM1RM4rw0tpw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9dl3pM1RM4rw0tpw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9dl3pM1RM4rw0tpw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9dl3pM1RM4rw0tpw .error-icon{fill:#552222;}#mermaid-svg-9dl3pM1RM4rw0tpw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9dl3pM1RM4rw0tpw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9dl3pM1RM4rw0tpw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9dl3pM1RM4rw0tpw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9dl3pM1RM4rw0tpw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9dl3pM1RM4rw0tpw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9dl3pM1RM4rw0tpw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9dl3pM1RM4rw0tpw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9dl3pM1RM4rw0tpw .marker.cross{stroke:#333333;}#mermaid-svg-9dl3pM1RM4rw0tpw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9dl3pM1RM4rw0tpw p{margin:0;}#mermaid-svg-9dl3pM1RM4rw0tpw .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9dl3pM1RM4rw0tpw text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-9dl3pM1RM4rw0tpw .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9dl3pM1RM4rw0tpw .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-9dl3pM1RM4rw0tpw .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-9dl3pM1RM4rw0tpw .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-9dl3pM1RM4rw0tpw #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-9dl3pM1RM4rw0tpw .sequenceNumber{fill:white;}#mermaid-svg-9dl3pM1RM4rw0tpw #sequencenumber{fill:#333;}#mermaid-svg-9dl3pM1RM4rw0tpw #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-9dl3pM1RM4rw0tpw .messageText{fill:#333;stroke:none;}#mermaid-svg-9dl3pM1RM4rw0tpw .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9dl3pM1RM4rw0tpw .labelText,#mermaid-svg-9dl3pM1RM4rw0tpw .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-9dl3pM1RM4rw0tpw .loopText,#mermaid-svg-9dl3pM1RM4rw0tpw .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-9dl3pM1RM4rw0tpw .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-9dl3pM1RM4rw0tpw .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-9dl3pM1RM4rw0tpw .noteText,#mermaid-svg-9dl3pM1RM4rw0tpw .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-9dl3pM1RM4rw0tpw .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9dl3pM1RM4rw0tpw .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9dl3pM1RM4rw0tpw .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-9dl3pM1RM4rw0tpw .actorPopupMenu{position:absolute;}#mermaid-svg-9dl3pM1RM4rw0tpw .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-9dl3pM1RM4rw0tpw .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-9dl3pM1RM4rw0tpw .actor-man circle,#mermaid-svg-9dl3pM1RM4rw0tpw line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-9dl3pM1RM4rw0tpw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} === 第 1 轮:拿楼宇信息 === 从 redux 直接读不调 API === 第 2 轮:拿门禁列表 === === 第 3 轮:执行开门 === 「帮我开一号楼大门」1对话请求2理解意图3method=getBuildingInfo4返回工具调用5① postMessage({method:getBuildingInfo, requestId:req-001})6② onMessage7③ 查 handlers.getBuildingInfo8④ ctx.sendMessage(getBuildingInfo, {buildingId:1}, req-001)9⑤ webViewRef.injectJavaScript10⑥ dispatchEvent nativeMessage11拿到 buildingId=112自动追问(buildingId=1)13理解14method=getUserAllDoor, params={buildingId:1}15返回工具调用16① postMessage({method:getUserAllDoor, params:{buildingId:1}, requestId:req-002})17② onMessage18③ 查 handlers.getUserAllDoor19调 api.getUserAllDoorNew(buildingId, employeeNo)20返回门禁列表21④ ctx.sendMessage(getUserAllDoor, {serial:abc, name:一号楼大门}, req-002)22⑤ webViewRef.injectJavaScript23⑥ dispatchEvent nativeMessage24拿到门禁列表25自动追问(deviceSerial=abc)26理解27method=doorOpen, params={deviceSerial:abc, ...}28返回工具调用29① postMessage({method:doorOpen, params:{deviceSerial:abc, ...}, requestId:req-003})30② onMessage31③ 查 handlers.doorOpen32调 api.doorOpen({deviceSerial, buildingId, ...})33返回 {success:true}34④ ctx.sendMessage(doorOpen, {success:true}, req-003)35⑤ webViewRef.injectJavaScript36⑥ dispatchEvent nativeMessage37显示「已为您打开一号楼大门」38

关键代码对照

① H5 发请求(步骤 ①,H5 端 SDK 调用):

javascript 复制代码
// 第一轮:拿楼宇
window.ReactNativeWebView.postMessage(JSON.stringify({
  method: 'getBuildingInfo',
  params: {},
  requestId: 'req-001'  // ← 每次请求唯一 ID
}));

// 第二轮:拿门禁列表
window.ReactNativeWebView.postMessage(JSON.stringify({
  method: 'getUserAllDoor',
  params: {buildingId: 1, employeeNo: 'xxx'},
  requestId: 'req-002'
}));

// 第三轮:开门
window.ReactNativeWebView.postMessage(JSON.stringify({
  method: 'doorOpen',
  params: {
    deviceSerial: 'abc',
    buildingId: 1,
    employeeNo: 'xxx'
  },
  requestId: 'req-003'
}));

②③④ 原生 handler 实现(步骤 ②③④,RN 项目代码):

javascript 复制代码
// src/utils/agentToolHandlers/doorHandlers.js

// handler 名 = method 名 = sendMessage 名(必须一致)
export async function getBuildingInfoHandler(params, requestId, context) {
  const {sendMessage} = context;
  // 从 redux 拿,不调 API
  const buildingId = context.building?.curId;
  sendMessage('getBuildingInfo', {buildingId}, requestId);  // ← 步骤 ④
}

export async function getUserAllDoorHandler(params, requestId, context) {
  const {sendMessage} = context;
  const {buildingId, employeeNo} = params;
  // 调业务 API
  const result = await getUserAllDoorNew(buildingId, employeeNo);
  sendMessage('getUserAllDoor', result, requestId);  // ← 步骤 ④
}

export async function doorOpenHandler(params, requestId, context) {
  const {sendMessage} = context;
  const {deviceSerial, buildingId, employeeNo} = params;
  // 调开门 API
  const result = await doorOpen({deviceSerial, buildingId, employeeNo});
  sendMessage('doorOpen', result, requestId);  // ← 步骤 ④
}

⑤⑥ H5 接收响应(H5 端 SDK 内部):

javascript 复制代码
// H5 监听 nativeMessage 事件
window.addEventListener('nativeMessage', e => {
  const {method, requestId, data} = e.detail;

  // method 区分是哪个请求的响应
  if (method === 'getBuildingInfo') {
    // 拿到 buildingId,继续下一步
  } else if (method === 'getUserAllDoor') {
    // 拿到门禁列表
  } else if (method === 'doorOpen') {
    // 开门成功,显示结果
  }
});
4 个观察点(最容易混的地方)
观察点 谁 啥时候调
method 名字一致性 H5 method = handler 名 = sendMessage 参数 全程保持一致
requestId 唯一性 每次新请求生成 H5 发起时
ctx.sendMessage 是步骤 ⑦ 原生 handler 完成后调 handler 内部
sendMessageToWebView 是步骤 ⑧ ctx.sendMessage 内部调 WebView.js 内

💡 核心口诀 :

「method 名字串起 H5→RN→H5 三步,requestId 串起一次会话,handler 名 = sendMessage 的 method 参数」

Step 5:H5 端实现(独立前端工程)

文件 :web/agent/(Vite 构建的独立 H5 工程)

javascript 复制代码
// vite.config.js 关键配置
{
  apiBaseUrl: 'https://<YOUR_AGENT_DOMAIN>/api',  // 智能体云服务地址
  appKey: '<YOUR_AGENT_APP_KEY>',
  appSecret: '<YOUR_AGENT_APP_SECRET>',
  tenantId: '<YOUR_AGENT_TENANT_ID>',
  agentId: '<YOUR_AGENT_ID>',
}

H5 端 SDK 叫 AgentChatSDK ,是 web/agent/public/sdk.js 这个文件,对外暴露 v1/agent/chat/... 这类接口。

Step 6:测试与发布

bash 复制代码
cd web/agent
npm run start:uat     # 本地调试(vite dev)
npm run build:prod    # 构建 prod 包
# 产物: web/agent/dist/prod/xingzhijia-agent/

部署到 CDN 后,AGENT_URL 配置 .env.production 指向正式地址即可。


五、Function Calling 全流程

核心认知 :LLM 是「决策者」,不真开门;真开门是 RN 客户端的 handler 调业务 API 做的。
真实大门 业务后端 原生 handler JSBridge WebView (RN) 大模型 LLM 智能体云服务 H5 聊天页 用户 真实大门 业务后端 原生 handler JSBridge WebView (RN) 大模型 LLM 智能体云服务 H5 聊天页 用户 #mermaid-svg-pJxgl0wp3zKSj5gt{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pJxgl0wp3zKSj5gt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pJxgl0wp3zKSj5gt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pJxgl0wp3zKSj5gt .error-icon{fill:#552222;}#mermaid-svg-pJxgl0wp3zKSj5gt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pJxgl0wp3zKSj5gt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pJxgl0wp3zKSj5gt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pJxgl0wp3zKSj5gt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pJxgl0wp3zKSj5gt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pJxgl0wp3zKSj5gt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pJxgl0wp3zKSj5gt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pJxgl0wp3zKSj5gt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pJxgl0wp3zKSj5gt .marker.cross{stroke:#333333;}#mermaid-svg-pJxgl0wp3zKSj5gt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pJxgl0wp3zKSj5gt p{margin:0;}#mermaid-svg-pJxgl0wp3zKSj5gt .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-pJxgl0wp3zKSj5gt text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-pJxgl0wp3zKSj5gt .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-pJxgl0wp3zKSj5gt .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-pJxgl0wp3zKSj5gt .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-pJxgl0wp3zKSj5gt .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-pJxgl0wp3zKSj5gt #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-pJxgl0wp3zKSj5gt .sequenceNumber{fill:white;}#mermaid-svg-pJxgl0wp3zKSj5gt #sequencenumber{fill:#333;}#mermaid-svg-pJxgl0wp3zKSj5gt #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-pJxgl0wp3zKSj5gt .messageText{fill:#333;stroke:none;}#mermaid-svg-pJxgl0wp3zKSj5gt .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-pJxgl0wp3zKSj5gt .labelText,#mermaid-svg-pJxgl0wp3zKSj5gt .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-pJxgl0wp3zKSj5gt .loopText,#mermaid-svg-pJxgl0wp3zKSj5gt .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-pJxgl0wp3zKSj5gt .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-pJxgl0wp3zKSj5gt .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-pJxgl0wp3zKSj5gt .noteText,#mermaid-svg-pJxgl0wp3zKSj5gt .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-pJxgl0wp3zKSj5gt .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-pJxgl0wp3zKSj5gt .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-pJxgl0wp3zKSj5gt .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-pJxgl0wp3zKSj5gt .actorPopupMenu{position:absolute;}#mermaid-svg-pJxgl0wp3zKSj5gt .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-pJxgl0wp3zKSj5gt .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-pJxgl0wp3zKSj5gt .actor-man circle,#mermaid-svg-pJxgl0wp3zKSj5gt line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-pJxgl0wp3zKSj5gt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} method=doorOpen params deviceSerial 1.说:开一号楼大门12.发对话请求23.理解意图34.返回调工具指令45.返回工具调用指令56.调 window.postMessage67.onMessage 触发 handleWebViewMessage78.分发到 doorOpenHandler89.调开门 API (fetch)910.发开门指令1011.门开了1112.返回成功1213.调用 ctx.sendMessage1314.调用 webViewRef.injectJavaScript1415.注入 JS 派发 nativeMessage 事件1516.显示「已为您打开一号楼大门」16

三个核心边界:

边界 在哪 干啥
⛔ LLM 不执行 步骤 3-4(红框) LLM 只返回「要调什么工具」,不真开门
✅ 工具真执行 步骤 8-12(黄框) RN 客户端的 handler 调 API 开门
🚪 物理世界 步骤 10-11(绿框) 门是真的开了

关键认知:3 个边界节点

#mermaid-svg-tSwMUorhQAmA7hBm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-tSwMUorhQAmA7hBm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tSwMUorhQAmA7hBm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tSwMUorhQAmA7hBm .error-icon{fill:#552222;}#mermaid-svg-tSwMUorhQAmA7hBm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tSwMUorhQAmA7hBm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tSwMUorhQAmA7hBm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tSwMUorhQAmA7hBm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tSwMUorhQAmA7hBm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tSwMUorhQAmA7hBm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tSwMUorhQAmA7hBm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tSwMUorhQAmA7hBm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tSwMUorhQAmA7hBm .marker.cross{stroke:#333333;}#mermaid-svg-tSwMUorhQAmA7hBm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tSwMUorhQAmA7hBm p{margin:0;}#mermaid-svg-tSwMUorhQAmA7hBm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tSwMUorhQAmA7hBm .cluster-label text{fill:#333;}#mermaid-svg-tSwMUorhQAmA7hBm .cluster-label span{color:#333;}#mermaid-svg-tSwMUorhQAmA7hBm .cluster-label span p{background-color:transparent;}#mermaid-svg-tSwMUorhQAmA7hBm .label text,#mermaid-svg-tSwMUorhQAmA7hBm span{fill:#333;color:#333;}#mermaid-svg-tSwMUorhQAmA7hBm .node rect,#mermaid-svg-tSwMUorhQAmA7hBm .node circle,#mermaid-svg-tSwMUorhQAmA7hBm .node ellipse,#mermaid-svg-tSwMUorhQAmA7hBm .node polygon,#mermaid-svg-tSwMUorhQAmA7hBm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tSwMUorhQAmA7hBm .rough-node .label text,#mermaid-svg-tSwMUorhQAmA7hBm .node .label text,#mermaid-svg-tSwMUorhQAmA7hBm .image-shape .label,#mermaid-svg-tSwMUorhQAmA7hBm .icon-shape .label{text-anchor:middle;}#mermaid-svg-tSwMUorhQAmA7hBm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tSwMUorhQAmA7hBm .rough-node .label,#mermaid-svg-tSwMUorhQAmA7hBm .node .label,#mermaid-svg-tSwMUorhQAmA7hBm .image-shape .label,#mermaid-svg-tSwMUorhQAmA7hBm .icon-shape .label{text-align:center;}#mermaid-svg-tSwMUorhQAmA7hBm .node.clickable{cursor:pointer;}#mermaid-svg-tSwMUorhQAmA7hBm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tSwMUorhQAmA7hBm .arrowheadPath{fill:#333333;}#mermaid-svg-tSwMUorhQAmA7hBm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tSwMUorhQAmA7hBm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tSwMUorhQAmA7hBm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tSwMUorhQAmA7hBm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tSwMUorhQAmA7hBm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tSwMUorhQAmA7hBm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tSwMUorhQAmA7hBm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tSwMUorhQAmA7hBm .cluster text{fill:#333;}#mermaid-svg-tSwMUorhQAmA7hBm .cluster span{color:#333;}#mermaid-svg-tSwMUorhQAmA7hBm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-tSwMUorhQAmA7hBm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tSwMUorhQAmA7hBm rect.text{fill:none;stroke-width:0;}#mermaid-svg-tSwMUorhQAmA7hBm .icon-shape,#mermaid-svg-tSwMUorhQAmA7hBm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tSwMUorhQAmA7hBm .icon-shape p,#mermaid-svg-tSwMUorhQAmA7hBm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tSwMUorhQAmA7hBm .icon-shape .label rect,#mermaid-svg-tSwMUorhQAmA7hBm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tSwMUorhQAmA7hBm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tSwMUorhQAmA7hBm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tSwMUorhQAmA7hBm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-tSwMUorhQAmA7hBm .llm>*{fill:#fce4ec!important;stroke:#c2185b!important;stroke-width:3px!important;color:#000!important;}#mermaid-svg-tSwMUorhQAmA7hBm .llm span{fill:#fce4ec!important;stroke:#c2185b!important;stroke-width:3px!important;color:#000!important;}#mermaid-svg-tSwMUorhQAmA7hBm .llm tspan{fill:#000!important;}#mermaid-svg-tSwMUorhQAmA7hBm .tool>*{fill:#fff9c4!important;stroke:#f57c00!important;stroke-width:3px!important;color:#000!important;}#mermaid-svg-tSwMUorhQAmA7hBm .tool span{fill:#fff9c4!important;stroke:#f57c00!important;stroke-width:3px!important;color:#000!important;}#mermaid-svg-tSwMUorhQAmA7hBm .tool tspan{fill:#000!important;}#mermaid-svg-tSwMUorhQAmA7hBm .door>*{fill:#e8f5e9!important;stroke:#2e7d32!important;stroke-width:3px!important;color:#000!important;}#mermaid-svg-tSwMUorhQAmA7hBm .door span{fill:#e8f5e9!important;stroke:#2e7d32!important;stroke-width:3px!important;color:#000!important;}#mermaid-svg-tSwMUorhQAmA7hBm .door tspan{fill:#000!important;} 返回指令
发指令
🤖 LLM

⛔ 只决策
🔧 原生 handler

✅ 真执行
🚪 物理大门

🌍 真实发生

💡 一句话总结 :

「用户说话 → H5 发请求 → 云服务调 LLM 理解 → LLM 决策调 doorOpen → 云服务返回指令 → H5 postMessage → 原生 handler 调 API → 真开门 → sendMessage 回传结果 → H5 显示」


六、5 个关键技术

1. WebView + JSBridge

通信方向 技术 项目代码
H5 → 原生 window.ReactNativeWebView.postMessage WebView.js onMessage
原生 → H5 injectJavaScript + CustomEvent WebView.js sendMessageToWebView

2. Function Calling(AI 工具调用)

  • LLM 只决策 (要调哪个工具),不真执行
  • 工具实际执行由原生 handler 完成
  • 这样安全可控(LLM 不能乱调 API)

3. 环境变量配置

javascript 复制代码
// .env.production
AGENT_URL=https://agent-prod.newbest.com.cn/xingzhijia-agent/

// 组件里用
import {AGENT_URL} from '@env';

4 个配置文件 :.env.local / .env.dev / .env.uat / .env.prod

4. 协议设计(请求 ID + 异步响应)

javascript 复制代码
// H5 发起请求
window.ReactNativeWebView.postMessage({
  method: 'doorOpen',
  params: {...},
  requestId: 'req-123',  // 异步请求 ID
});

// 原生响应
sendMessage('doorOpen', {success: true}, 'req-123');

为什么需要 requestId? 异步调用,H5 可能同时发多个请求,用 requestId 区分响应对应哪个请求。

5. 5 层安全防护

层 防护
1. 域名白名单 currentUri.includes('xingzhijia-agent')
2. 外链拦截 onShouldStartLoadWithRequest
3. 环境变量 凭证不写死,靠 .env
4. 平台凭证 H5 端 AppKey/AppSecret 是平台级
5. 事件隔离 用 CustomEvent 不污染全局

七、与传统聊天机器人对比

维度 传统 AI 客服 本项目 AI 智能管家
架构 单页面 Web/App RN + WebView + H5 + LLM
意图理解 仅文本回复 Function Calling 调原生能力
业务能力 只能回答问题 能开门、调相册、查房屋
更新方式 发版才能更新 H5 端可热更新(LLM 提示词也能热更新)
数据隔离 后端控制 用户上下文 + 房屋上下文实时传入

八、项目实战亮点(面试加分)

亮点 1:JSBridge 双向通信

复制代码
H5 ←→ postMessage / injectJavaScript ←→ 原生

亮点 2:Function Calling 真实落地

复制代码
不是 demo 级 AI,是真的能开门、调相册、看用户房屋

亮点 3:H5 独立工程可热更新

复制代码
AI 模型更新、提示词优化、新增工具都不用发版

亮点 4:环境变量 + 凭证分层

复制代码
AGENT_URL 走环境变量,平台凭证走 H5 config,业务凭证走后端

亮点 5:requestId 异步协议设计

复制代码
H5 同时发多个工具调用也不会乱序

九、面试 5 分钟模板(直接背)

「我们项目的 AI 智能管家用 RN + WebView + H5 实现:

  1. 入口:底部 TabBar 中间「星灵」按钮,H5 URL 走环境变量;
  2. 架构 :H5 端调智能体云服务(<YOUR_AGENT_DOMAIN>),云服务调度 LLM + 工具;
  3. 核心机制 :JSBridge 双向通信(H5→原生用 postMessage,原生→H5 用 injectJavaScript + CustomEvent);
  4. 工具调用:原生端实现了 11 个 handler(开门、查房屋、调电话等),LLM 通过 Function Calling 自动调用;
  5. 安全:域名白名单 + 外链拦截 + 平台凭证走 H5 config 分离。

亮点 :AI 不只是聊天机器人,能真实控制业务 (开门、调相册)。H5 独立工程可热更新,AI 模型迭代不用发版。」


十、口诀速记(脱口而出)

口诀 1:整体架构

复制代码
RN App 嵌入 H5,H5 调云服务,云服务调 LLM,LLM 调原生,原生调 API

口诀 2:双向通信

复制代码
H5 → 原生用 postMessage,原生 → H5 用 injectJavaScript + CustomEvent

口诀 3:Function Calling 三步走

复制代码
LLM 理解意图 → 选工具 → 工具执行(安全可控)

口诀 4:requestId 协议

复制代码
异步调用必须有 ID,不然多个请求会乱

口诀 5:H5 独立工程价值

复制代码
AI 模型可热更,提示词可迭代,新工具可加,不用发版

口诀 6:5 层安全

复制代码
域名白名单 + 外链拦截 + 环境变量 + 平台凭证 + 事件隔离

十一、面试常见 5 问速答

Q1:JSBridge 是什么?

A :JSBridge 是 JavaScript 与原生代码之间的通信桥梁。在 RN WebView 里:

  • H5 → 原生:window.ReactNativeWebView.postMessage
  • 原生 → H5:webViewRef.injectJavaScript 注入 JS 派发 CustomEvent

Q2:Function Calling 是什么?

A :让 LLM 自动选择并调用工具 的能力。流程:

  1. LLM 收到用户问题
  2. LLM 判断需要调哪个工具 + 参数
  3. 后端执行工具(拿到结果)
  4. LLM 拿到结果,组织自然语言回复

关键 :LLM 不直接执行工具 ,只是「决策者」,执行由后端/原生完成,安全可控。

Q3:为什么 AI 智能管家用 H5 而不是 RN 原生写?

A:3 个原因:

  1. AI 模型迭代快------H5 可热更新,RN 原生需要发版
  2. 提示词工程------H5 调 LLM API 更灵活
  3. 复用 Web 生态------智能体云服务 SDK 一般只有 Web 版本

Q4:requestId 是干嘛的?

A :关联异步请求和响应 。H5 可能同时发多个 postMessage,原生处理完不一定按顺序返回,用 requestId 让 H5 知道这个响应是对应哪个请求。

Q5:怎么保证 AI 智能管家的安全?

A:5 层防护:

  1. 域名白名单 (currentUri.includes('xingzhijia-agent'))
  2. 外链拦截 (onShouldStartLoadWithRequest)
  3. 环境变量(URL 不写死)
  4. 平台凭证隔离(H5 config 和业务凭证分开)
  5. 事件隔离(用 CustomEvent 不污染全局)

面试前 5 分钟 :看第一节(一图速记)+ 第九节(5 分钟模板)+ 第十节(6 句口诀),足够应付 80% 追问。

被追问细节:看第四节(6 步实现)+ 第六节(5 个关键技术)。

相关推荐
q243622 小时前
2026 企业 AI 办公工具选型指南:如何匹配业务场景完成端到端任务
大数据·人工智能
u1301302 小时前
AI 日报(2026年10月13日)
人工智能
Fly2 小时前
AI Native 产研实战:从一句需求,到一次可靠交付
人工智能
大力财经2 小时前
字节跳动 Seed 单独设立大模型安全与对齐部门
人工智能
陈天伟教授2 小时前
高级考评员
人工智能·具身智能
JavaEdge.2 小时前
Only-AI-Flow-Can-Do
人工智能
forest_092 小时前
Agent 系列07|记忆与状态设计
人工智能·机器学习·架构·prompt·agi
anyup_前端梦工厂2 小时前
uni-app 模板 Starter 一键清理 Skill 发布
前端·人工智能·uni-app