多模态接入
更新时间: 2026/07/14 16:10:49
多模态接入用于让 AI 硬件智能体在语音对话中理解图像内容。典型交互是用户说“看看这是什么”,智能体判断需要调用拍照能力后,由端上完成拍照或截图上传,再把图片和用户问题一起发送给云端多模态模型,最后以语音或字幕形式返回识别结果。
本文以 Android Demo 和硬件 Demo 中的拍照识图链路为例,说明多模态接入的适用场景、能力范围、接入方式、端上约束、平台配置和常见问题排查。
适用场景
| 适用场景 | 典型诉求 | 推荐接入方式 |
|---|---|---|
| App / 平板 / 带屏终端 | 用户通过摄像头让智能体识别眼前物体、商品、文本、环境 | 音视频通话 NERTC SDK + 截图上传 |
| 智能硬件摄像头 | 用户对设备说“看看这是什么”,设备拍照后由智能体解释 | 嵌入式 NERTC SDK + 端侧拍照上传 |
| 陪伴机器人 / 桌面宠物 | 结合语音、表情和摄像头实现“看见并回应” | 嵌入式 NERTC SDK + Function Call / MCP 工具 |
| 客服、导购、教育类助手 | 识别图片中的商品、作业、说明书或场景 | App 端上传图片 + 多模态 LLM 识别 |
不建议用于需要高精度工业检测、医疗诊断、安防告警等强准确性场景。此类场景需要额外接入专业视觉模型或人工复核流程。
能力范围
当前最佳实践主要覆盖 图片理解:
- 通过语音触发拍照,例如“看看这是什么”。
- 端上采集当前摄像头画面或视频流截图。
- 将图片与文本问题一起发送给智能体。
- 智能体基于图片内容生成自然语言解释。
- 通过 TTS 语音、字幕或 UI 文案返回结果。
不包含以下能力:
- 连续视频流理解。
- 实时目标检测或定位。
- OCR、商品识别、医学识别等垂类高准确能力。
- 对图片内容的确定性判断或合规审核结论。
开通服务
接入多模态前,需要确认以下能力已开通或配置完成,部分详情请参考 开通服务:
| 能力 | 说明 |
|---|---|
| 音视频通话 2.0 | 用于 App 或设备接入 RTC 房间,传输音频、视频和 AI 语音。 |
| 实时对话 AI | 用于创建 AI 任务,完成 ASR、LLM、TTS 编排。 |
| 智能体平台 | 用于配置智能体、人设、模型、工具调用能力。 |
| 多模态 LLM / 视觉理解能力 | 用于处理图片 + 文本问题。具体供应商和模型以控制台开通结果为准。 |
| 设备 License(硬件场景) | 智能硬件量产或测试时,需要按设备授权口径接入。 |
同时,需要在智能体后台完成智能体基础信息、LLM 模型、提示词、TTS 音色、权限与授权等配置。拍照识图 Tool 的配置方式与接入路线有关,详见下文对应接入方式。
生产环境中,RTC Token、AI 任务创建和关闭建议由应用服务器统一处理,避免 AppSecret 或敏感配置下发到 App 端。
接入方式
Android / App 端截图上传(Function Call 方案)
Android / App 端仅支持 Function Call 方案:需要先在 Agent 后台配置拍照识图 Tool,由 LLM 判断用户意图并触发工具调用。App 先加入 RTC 房间并创建 AI 任务,随后监听 onAiData(type="tool"),收到 camera_capture 后调用 SDK 截图上传接口。
配置拍照识图 Tool
在 网易云信智能体平台 中,进入目标智能体的角色配置页,在 Tools 配置 中增加拍照识图相关 Tool。

以 App 端截图上传为例,可以配置 Tool 名称为 camera_capture,并在 Tool 描述中说明触发条件,例如:
text用户如果说类似“看看这是什么”,执行这个 tool。
配置完成后,智能体在识别到“看看这是什么”“帮我看一下”“识别一下这个”等意图时,会通过 onAiData(type="tool") 下发工具调用。App 端收到 camera_capture 后,再调用 takeSnapShotAndRequestLLM 完成截图上传。
工具名称需要与端上代码保持一致。Android Demo 中监听的是 camera_capture。实际项目应统一工具名、入参和返回结构,避免智能体下发的工具名与端上实现不一致。
链路如下:
text用户说“看看这是什么”
-> ASR 识别用户语音
-> LLM 判断需要调用拍照工具
-> App 收到 onAiData(type="tool")
-> 解析 toolCalls 中的 camera_capture
-> 调用 takeSnapShotAndRequestLLM(text, uid, streamType)
-> SDK 截取本地视频画面并上传
-> 多模态模型根据图片和问题生成回答
-> TTS 返回语音,App 播放并展示字幕
关键配合点:
| 环节 | App / SDK 配合 | 说明 |
|---|---|---|
| 入会 | 初始化 NERTC SDK,开启本地音频和视频,加入 RTC 房间 | 本地视频必须可用,否则无法截图。 |
| 创建 AI 任务 | 调用客户端 API POST https://rtc-ai.yunxinapi.com/ai/task/create,配置 ASR、LLM、TTS 和 Agent |
需要使用 RTC Token 鉴权。参考 开启实时对话。 |
| 开启字幕 | 调用 ASR Caption 能力 | 用于展示用户问题和 AI 回复。 |
| 工具调用 | 监听 onAiData(type, data) |
当 type="tool" 且工具名为 camera_capture 时触发截图。 |
| 截图上传 | 调用 takeSnapShotAndRequestLLM(text, 0, 1) |
uid=0 表示本地用户,streamType=1 表示走小流。 |
建议开启双流:大流用于本地预览,小流用于截图上传。这样可以保证预览清晰,同时降低上传图片大小和网络耗时。
如需完整 Android / App 端 Demo,可以联系网易云信技术支持获取。
Android 示例代码
以下示例展示 App 如何解析 onAiData 中的工具调用,并在收到 camera_capture 后调用截图上传接口。
1. 开启本地视频和双流
在入会前初始化 SDK,并开启本地视频。建议开启双流,小流用于截图上传。
javaprivate void enableVideoForSnapshot() {
// 伪代码:初始化 NERTC SDK 并加入 RTC 房间。
// NERtcEx.getInstance().init(context, appKey, callback, options);
// NERtcEx.getInstance().joinChannel(token, channelName, uid);
// 开启双流:大流用于本地预览,小流用于截图上传。
NERtcEx.getInstance().enableDualStreamMode(true);
// 开启本地音视频,确保后续可以截取本地视频画面。
NERtcEx.getInstance().enableLocalAudio(true);
NERtcEx.getInstance().enableLocalVideo(true);
}
2. 解析 onAiData 工具调用
当智能体判断需要拍照识图时,会通过 onAiData 下发工具调用数据。App 需要判断 type 是否为 tool,并解析 toolCalls 中的工具名称。
javaprivate static final String TOOL_CAMERA_CAPTURE = "camera_capture";
@Override
public void onAiData(String type, String data) {
if (!"tool".equals(type) || data == null) {
return;
}
try {
JSONObject json = new JSONObject(data);
String text = json.optString("request", "看看这是什么。");
JSONArray toolCalls = json.optJSONArray("toolCalls");
if (toolCalls == null) {
return;
}
for (int i = 0; i < toolCalls.length(); i++) {
JSONObject toolCall = toolCalls.optJSONObject(i);
if (toolCall == null) {
continue;
}
JSONObject function = toolCall.optJSONObject("function");
if (function != null && TOOL_CAMERA_CAPTURE.equals(function.optString("name"))) {
doSnapShotAndRequestLLM(text);
return;
}
}
} catch (JSONException e) {
Log.e("AICapacity", "parse onAiData failed", e);
}
}
onAiData 中的工具调用数据示例:
json{
"request": "看看这是什么。",
"toolCalls": [
{
"type": "function",
"function": {
"name": "camera_capture",
"arguments": "{}"
}
}
]
}
3. 调用截图上传接口
收到 camera_capture 后,调用 takeSnapShotAndRequestLLM 截取本地视频画面,并将图片和文本问题一起发送给智能体。
javaprivate void doSnapShotAndRequestLLM(String text) {
String question = (text == null || text.trim().isEmpty()) ? "看看这是什么。" : text;
// 伪代码:调用前请先确认已入会、本地视频已开启、AI 任务已创建成功。
// if (!hasJoinedChannel || !localVideoEnabled || !aiTaskCreated) return;
// uid 传 0 表示截取本地用户画面。
// streamType 传 1 表示使用小流截图,降低图片大小和上传耗时。
int result = NERtcEx.getInstance().takeSnapShotAndRequestLLM(question, 0, 1);
if (result != NERtcConstants.ErrorCode.OK) {
Log.e("AICapacity", "takeSnapShotAndRequestLLM failed, result=" + result);
}
}
接口参数说明:
| 参数 | 示例值 | 说明 |
|---|---|---|
text |
看看这是什么。 |
随图片一起发送给智能体的问题。 |
uid |
0 |
截图目标用户。传 0 表示本地用户。 |
streamType |
1 |
截图流类型。建议传 1 使用小流。 |
调用前请确认:
- App 已加入 RTC 房间。
- 本地视频已开启。
- AI 任务已创建成功。
- 智能体已配置并能下发
camera_capture工具调用。
智能硬件端拍照上传(Function Call / 端侧 MCP 方案)
智能硬件端支持两条技术路线:
- Function Call 方案:与 App 端类似,在 Agent 后台配置拍照识图 Tool,由 LLM 判断用户意图并触发工具调用,设备收到工具调用后拍照上传。
- 端侧 MCP 方案:参考当前硬件 Demo,在设备侧注册
self.photo_explain、self.camera.take_photo等 MCP 工具,由设备侧工具处理拍照、编码和发送图片。
当前硬件 Demo 采用端侧 MCP 方案实现拍照识图。可参考 GitHub 上的 ESP32 示例工程:nertc-iot-demo/esp32/nertc_demo。
链路如下:
text用户说“看看这是什么”
-> 设备进入语音对话链路
-> 智能体触发拍照识图工具
-> 设备调用 PhotoExplain
-> 摄像头 Capture 当前画面
-> 将 JPEG 转为 Base64 或通过视觉 URL 上传
-> 调用 SendLlmImage 发送图片和问题
-> 多模态模型生成解释
-> 设备播放 TTS 回复
硬件 Demo 中存在两类实现:
| 实现方式 | 说明 | 适用情况 |
|---|---|---|
PhotoExplain + SendLlmImage |
端侧手动触发拍照后,将 JPEG 转为 Base64,并通过 NERTC 协议发送图片和问题 | 适合嵌入式 NERTC 接入链路。 |
| 端侧 MCP 方式 | 在设备侧注册 self.photo_explain 或 self.camera.take_photo 等工具 |
适合参考当前硬件 Demo、由设备侧承载工具实现的场景。 |
设备需要实现摄像头初始化、拍照、图片编码、内存释放和异常处理。ESP32-S3 等设备通常需要 PSRAM 支持,图片分辨率和质量需要根据内存、网络和识别效果权衡。
智能硬件示例代码
以下示例展示智能硬件如何从“看看这是什么”触发拍照识图。示例基于硬件 Demo 的实现方式,重点展示触发、拍照、编码和发送图片的关键逻辑。
1. 触发拍照识图
如果设备上有独立摄像头按键,可以在按键回调中调用拍照识图方法。若由智能体识别“看看这是什么”等意图,也可以复用同一个方法。
cppvoid RequestPhotoExplain(const std::string& question) {
// 伪代码:调用前请先确认设备已联网、已建立实时对话链路、摄像头可用。
// if (!deviceReady || !cameraReady) return;
Application::GetInstance().PhotoExplain(question, "我来看一下");
}
2. 注册端侧 MCP 拍照工具
如果采用端侧 MCP 方案,可以在设备侧注册工具。例如,当用户说“看看这是什么”时,智能体调用 self.photo_explain,设备收到工具调用后执行拍照识图。
cppvoid RegisterPhotoExplainTool() {
AddTool("self.photo_explain",
1,
PropertyList({
Property("question", kPropertyTypeString),
Property("pre_answer", kPropertyTypeString)
}),
[](const PropertyList& properties) -> ReturnValue {
std::string question = properties["question"].value<std::string>();
std::string preAnswer = properties["pre_answer"].value<std::string>();
if (question.empty()) {
question = "看看这是什么。";
}
Application::GetInstance().PhotoExplain(question, preAnswer);
return "{\"success\":true}";
});
}
工具入参建议:
| 参数 | 说明 |
|---|---|
question |
用户关于图片的问题,例如“看看这是什么”。 |
pre_answer |
可选。拍照前先播报的提示语,例如“我来看一下”。 |
3. 拍照、编码并发送图片
PhotoExplain 中完成摄像头拍照、获取 JPEG、Base64 编码,并通过 SendLlmImage 把图片和问题发送给智能体。以下代码保留核心逻辑,摄像头初始化、任务调度和异常提示可按设备工程实现补充。
cppvoid Application::PhotoExplain(const std::string& question,
const std::string& preAnswer) {
// 伪代码:如需先播报“我来看一下”,可在这里调用 TTS。
// if (!preAnswer.empty()) protocol_->SendTTSText(preAnswer, 2, false);
Camera* camera = Board::GetInstance().GetCamera();
if (camera == nullptr || !camera->Capture()) {
ESP_LOGE("PhotoExplain", "camera capture failed");
return;
}
JpegChunk jpeg = {nullptr, 0};
if (!camera->GetCapturedJpeg(jpeg.data, jpeg.len) || jpeg.data == nullptr || jpeg.len == 0) {
ESP_LOGE("PhotoExplain", "invalid JPEG data");
if (jpeg.data != nullptr) {
heap_caps_free(jpeg.data);
}
return;
}
const char* prefix = "data:image/jpeg;base64,";
size_t prefixLen = strlen(prefix);
size_t outputLen = ((jpeg.len + 2) / 3) * 4 + prefixLen + 1;
unsigned char* image = static_cast<unsigned char*>(malloc(outputLen));
if (image == nullptr) {
heap_caps_free(jpeg.data);
return;
}
memset(image, 0, outputLen);
memcpy(image, prefix, prefixLen);
size_t encodedLen = 0;
int result = mbedtls_base64_encode(
image + prefixLen,
outputLen - prefixLen,
&encodedLen,
jpeg.data,
jpeg.len
);
if (result == 0) {
// 第 1 个参数:图片内容,格式为 data:image/jpeg;base64,...
// 第 2 个参数:图片内容长度。
// 第 4 个参数:随图片一起发送给 LLM 的问题。
// 第 5 个参数:0 表示 Base64 图片内容。
protocol_->SendLlmImage(
reinterpret_cast<const char*>(image),
prefixLen + encodedLen,
0,
question,
0
);
}
free(image);
heap_caps_free(jpeg.data);
}
4. 使用图片 URL 方式发送
如果图片已上传到自有对象存储,也可以直接向 LLM 发送图片 URL。此方式适合图片由业务服务器统一存储、审核或加签的场景。
cppvoid Application::SendImageUrlToLLM(const std::string& imageUrl,
const std::string& question) {
// 最后一个参数传 1,表示图片来源为网络图片 URL。
protocol_->SendLlmImage(imageUrl.c_str(), imageUrl.size(), 0, question, 1);
}
其中最后一个参数用于区分图片来源类型,0 表示 Base64 图片内容,1 表示网络图片 URL。具体取值以当前 SDK / 协议版本为准。
5. 调用前检查项
- 设备已完成联网,并已建立实时对话链路。
- 摄像头已初始化,
Board::GetInstance().GetCamera()能返回有效对象。 - 设备状态允许拍照识图,例如不在 Wi-Fi 配网、启动中或关闭对话状态。
- 设备有足够内存完成 JPEG 缓冲和 Base64 编码,建议使用带 PSRAM 的硬件。
- 图片尺寸和 JPEG 质量经过验证,不会导致上传过慢或内存不足。
- 工具名、工具入参和智能体平台中的工具配置保持一致。
端上约束
App 端
- 需要摄像头和麦克风权限,并在隐私协议中说明图片上传用途。
- 调用截图上传前,用户必须已入会、本地视频已开启、AI 任务已创建。
- 建议开启双流,截图上传走小流,避免高清图导致上传慢或卡顿。
- 截图上传失败时,需要给用户明确提示,例如“我没看到画面,可以再试一次吗”。
- AppSecret 不应内置在 App 端,生产环境应由服务端签发 RTC Token。
硬件端
- 设备必须带摄像头,并完成摄像头驱动、方向、镜像、曝光等适配。
- 建议支持 PSRAM,否则 Base64 编码和 JPEG 缓冲可能失败。
- 图片分辨率和 JPEG 质量不宜过高,需兼顾识别效果、内存占用和上传耗时。
- 需要处理拍照失败、编码失败、网络失败、模型无响应等异常。
- 若设备无屏,需要通过灯效、提示音或 TTS 告知用户“正在看”或“识别失败”。
常见问题排查
| 问题 | 可能原因 | 排查建议 |
|---|---|---|
| 用户说“看看这是什么”后没有拍照 | 智能体未触发工具调用,或工具名不匹配 | 检查 onAiData / MCP 日志,确认是否下发 camera_capture 或约定工具名。 |
| App 截图上传失败 | 未入会、本地视频未开启、AI 任务未创建 | 检查入会状态、视频状态、task/create 响应和 SDK 返回码。 |
| 图片上传慢或卡顿 | 图片分辨率过高,走了大流截图 | 开启双流,截图上传使用小流;降低图片质量。 |
| 硬件拍照失败 | 摄像头未初始化、帧缓冲为空、设备状态不允许拍照 | 检查摄像头初始化日志、Capture 返回值和设备状态。 |
| 硬件编码失败或重启 | 内存不足,Base64/JPEG 缓冲过大 | 降低分辨率和 JPEG 质量,确认 PSRAM 可用,释放临时内存。 |
| AI 回复“我看不到图片” | 图片未成功发送到 LLM,或模型不支持视觉输入 | 检查 SendLlmImage 调用、上传响应、模型是否支持多模态。 |
| 识别结果不准确 | 图片模糊、光线不足、问题不明确 | 引导用户对准目标、保持稳定,问题中补充识别目标。 |
| 线上无法创建 AI 任务 | 鉴权错误或能力未开通 | 检查 RTC Token、AppKey、Cname、Uid、实时对话 AI 和多模态能力开通状态。 |
上线建议
- 明确告知用户摄像头和图片上传用途,获得必要授权。
- 对“看看这是什么”等触发词设置自然提示,但不要强依赖固定口令。
- 工具调用失败时准备兜底话术,例如“我刚刚没拍到,可以再让我看一次吗”。
- 建议在端侧记录
requestId、工具调用时间、SDK 返回码和图片大小,便于线上排查。 - 硬件量产前,需要在弱网、暗光、移动画面、连续触发拍照等场景下验证稳定性。




