一、交互范式重构:大模型如何从“理解世界”走向“进入现场”
1.1 从文本问答到具身在场:AI 交互边界的重新定义
以 DeepSeek 为代表的大模型,已经赋予 AI 完善的语言理解、逻辑推理、文本生成能力,各类纯文本 Agent 也成为主流工具。但这类 AI 只拥有 “思考大脑”,缺少完整具身交互智能,落地展厅、门店等线下终端时,仅靠文字聊天框无法实现真人化现场沟通。
在办公和编程场景中,用户可以耐心阅读长文本,也可以随时复制、修改和反复追问;而在展厅环境中,参观者通常站在屏幕前,希望直接提出问题,并迅速判断系统是否已经接收、是否正在处理以及何时开始回答。若整个过程仍依赖键盘输入和大段文字阅读,大模型虽然接入了展厅系统,却还没有真正融入展厅的交互环境。
真实场景中的交流不只有信息内容,还包括声音、节奏、姿态和即时反馈。面对真人讲解员时,我们能够从眼神、表情和动作中判断对方是否正在倾听,也可以在讲解过程中随时插话,调整交流方向。文本 AI 通常只能呈现最终答案,用户很难感知其等待、处理和表达过程。
因此,AI 正在经历一次从文本交互到具身交互智能的变化。具身交互智能不是简单给 AI 贴虚拟皮肤,也不止实体人形机器人,是一套完整人机交互体系。网页中的 3D 数字人、展厅大屏上的虚拟讲解员、移动应用中的数字助手以及人形机器人,都可以成为 AI 的身体载体。
在我看来,具身交互智能是指:
让 AI 拥有可被看见的身体、可被感知的状态、可自然表达的语音表情动作,以及可实时响应、可随时打断的交互能力。
这意味着,AI 的回答不再只是屏幕上的一段文字,而是一个完整的表达过程:用户提出问题后,智能体能够呈现交互状态,通过语音和动作组织答案,并允许用户中途打断或继续追问。
从这个角度看,大模型负责 AI 认知推理,Agent 负责任务调度执行;魔珐星云作为全域具身交互智能开放平台,补齐 AI 缺失的实体载体、多模态表达、实时双向交互整套能力。它绝非简单数字人渲染工具,也不是 Agent 外层视觉装饰,依托自研参数流、AI 端侧解算,为所有大模型、Agent 提供标准化具身交互底层能力。
换句话说,大模型解决 AI 的“大脑”,魔珐星云补齐 AI 的“身体、表达和交互”。只有当认知能力与表达载体真正连接起来,AI 才可能从一个隐藏在聊天框中的文本工具,转变为能够进入真实服务场景的智能体。
为了验证这种交互方式,本文选择展厅讲解作为具体场景,搭建一个由 DeepSeek 与魔珐星云 SDK 共同驱动的 3D 讲解 Agent。
二、从能力拼接到体验断层:传统数字人方案的系统性瓶颈
2.1 LLM、TTS 与渲染链路之间的协同断点
从功能层面看,构建一个“能回答、会说话、有形象”的数字人并不复杂:大模型负责理解问题并生成文本,TTS 将文本转换为语音,数字人渲染模块再完成口型、表情和动作呈现。多个模块依次连接,似乎已经具备完整的交互能力。
但真实体验并不取决于单个模块是否可用,而取决于整条链路能否协同运行。当大模型、语音合成和数字人渲染彼此独立时,开发者仍需自行处理文本切分、语音队列、口型同步、动作衔接、状态切换与异常恢复。模型已经输出内容,语音却仍在等待;声音已经播放,人物动作尚未跟进;用户发起新问题后,旧回答仍在继续。这些看似细微的不同步,最终都会转化为用户能够直接感知的等待感和割裂感。
因此,具身交互的技术门槛并不是简单地把 LLM、TTS 和 3D 形象串联起来,而是让认知、声音、动作和交互状态围绕同一轮对话形成统一节奏。
2.2 从预制播报到实时交互的能力分界
传统数字人更擅长处理预先确定的内容,例如宣传视频、固定讲解词或循环播放脚本。这类场景以内容展示为核心,语音、口型和动作可以提前制作,用户也不需要在播放过程中改变交流方向。
展厅讲解 Agent 面对的则是动态交互。参观者的问题无法提前预设,回答需要由大模型实时生成,数字人的语音、表情和动作也必须随内容同步变化。更重要的是,参观者可能在讲解过程中继续追问、补充条件,甚至直接插话,系统需要迅速结束旧任务并进入下一轮交流。
这正是“能播报”与“能交互”的根本区别。前者关注内容是否能够被完整呈现,后者关注智能体能否理解现场变化、及时组织表达,并保持多轮对话的连续性。数字人的核心价值也由此从“像人展示内容”,转向“代表 AI 与人自然交流”。
2.3 参数流与端侧技术重构实时交互链路
面对实时交互需求,继续依赖多套独立系统叠加,不仅会拉长处理链路,也会增加云端渲染、网络传输和终端部署的复杂度。尤其在展厅大屏、门店终端和公共服务设备中,数字人既要保持稳定呈现,也要及时回应用户的提问与打断。
魔珐星云采用自研参数流架构,并结合 AI 端渲和端侧解算技术,将具身表达所需的渲染与解算能力下沉至终端。与持续传输完整视频画面的方式不同,参数流侧重传递数字人的驱动信息,再由终端完成实时呈现,从而打通内容生成、语音表达、动作驱动和画面渲染之间的链路。
在这一体系中,大模型继续承担理解与生成,魔珐星云则将文本内容转化为语音、口型、表情和身体动作,并通过 SDK 接入网页、应用、大屏、机器人及 AR/VR 等终端形态。官方资料给出的端到端约 500ms 响应、实时打断、多终端兼容等能力,也正是面向具身交互场景而设计。
由此可见,数字人从展示工具走向具身智能体,关键不在于增加多少独立功能,而在于是否建立了一条真正端到端、可实时响应、可持续交互的表达链路。
三、重构具身交互链路:DeepSeek 与魔珐星云的协同架构
3.1 从认知生成到具身呈现:展厅讲解 Agent 的分层设计
搭建展厅讲解 Agent 时,并不是简单地把 DeepSeek 的回答交给一个数字人朗读,而是将系统拆分为认知生成、对话编排和具身交互三个层次。
参观者提出问题后,DeepSeek 负责理解问题并生成符合展厅讲解语境的回答;应用层负责维护对话上下文、接收流式内容、划分可播报语句,并处理新旧任务的切换;魔珐星云 SDK 则负责加载和驱动 3D 数字人,将文本内容转化为语音、口型、表情与动作,最终呈现在网页或展厅屏幕上。
三层架构的价值在于职责清晰。更换大模型不会改变数字人的呈现方式,调整角色形象和音色也不需要重写对话逻辑。当后续需要加入展品知识库、路线查询或业务接口时,只需在 Agent 编排层扩展对应能力。
3.2 从文本回答到多模态表达:星云 SDK 承担的关键环节
DeepSeek 返回的结果本质上仍是一段文本。要将这段内容变成用户能够直接感知的讲解过程,还需要处理声音生成、口型同步、人物动作、角色状态以及 3D 画面呈现。
魔珐星云官方将具身驱动描述为将 AI 表达由文本升级为 3D 多模态。其 JavaScript SDK 提供实时 3D 数字人渲染与驱动、语音合成与口型同步、多状态行为控制,以及字幕、图片、视频等 Widget 事件能力。开发者还可以通过状态、语音和网络回调,掌握数字人的运行过程。
在本文的展厅 Demo 中,星云 SDK 主要承担四项工作:首先在网页中初始化加载 3D 具身交互智能数字人;其次接收 DeepSeek 生成的讲解文本;同步驱动数字人完成语音、口型和动作表达;最后实时反馈交互状态,支撑双向对话。
由此,DeepSeek 与魔珐星云并不是简单的两个接口串联,而是分别负责“讲什么”和“如何讲”。前者生成讲解内容,后者让内容获得形象、声音和动作。
3.3 从信息展示到智能服务:展厅屏幕的交互升级
传统展厅屏幕承担的主要是内容展示:按照预设顺序播放图片、视频和讲解词。它可以持续、稳定地传递信息,却很难根据参观者的兴趣改变讲解内容。当用户对某件展品产生疑问时,屏幕展示的仍然是既定内容,真正的交流还需要依赖现场工作人员。
接入大模型后,屏幕开始具备理解问题和动态组织内容的能力,但一段生成在界面上的文字,还不足以替代现场讲解。展厅环境强调直观、即时和连续的体验,参观者需要看到讲解者、听到讲解内容,并通过角色的口型、表情和动作感知交流过程。
DeepSeek 与魔珐星云的组合,正好对应了这两个层面的需求:DeepSeek 根据参观者的问题生成讲解内容,魔珐星云则将文字回答转化为数字人的语音、口型、表情和动作。原本只能循环播放内容的屏幕,由此获得了一位能够围绕现场问题进行动态表达的 3D 讲解员。
此时,展厅数字人的价值也不再只是“形象更生动”,而是成为展览内容与参观者之间的交互入口。例如,参观者可以继续追问展品背景、补充问题或改变讲解方向;系统则围绕当前对话重新组织内容,并通过数字人完成下一轮表达。屏幕由单向的信息出口,转变为能够接收问题、生成回答并持续交流的服务终端。
四、基于魔珐星云 JS SDK 的展厅讲解 Agent 实战
前面几章讨论的是为什么展厅 Agent 需要具身表达层,以及 DeepSeek 和魔珐星云分别负责什么。从这一章开始,不再停留在架构图上,直接进入接入过程。
这次使用的技术栈是 Vue 3、TypeScript 和 Vite,DeepSeek-V4-Pro 作为讲解内容生成模型,魔珐星云 JavaScript SDK 负责数字人的渲染与表达。
下面按照你原稿中“后台配置—SDK 初始化—流式驱动—动作与 Widget—中断状态机”的实战顺序重写,业务统一为DeepSeek-V4-Pro 展厅讲解 Agent。原稿里的注册流程和开发踩坑得以保留,但删去了与商场服务台相关的内容。
4.1 在魔珐星云后台创建具身交互智能体应用
写代码之前,需要先在魔珐星云平台中把数字讲解员配置出来。角色形象、音色和表演风格都属于应用配置,SDK 初始化时只需要提供对应应用的 App ID 和 App Secret。
Step 1:注册账号
打开专属链接 https://xingyun3d.com/?utm_campaign=jixinghuiKoc228&utm_source=jixinghuiKoc&utm_medium=&utm_term=&utm_content= ,点右上角"登录注册"。
Step 2:进入体验中心 → 具身驱动
登录之后第一件事不是急着创建应用,先去体验中心 → 具身驱动那一页玩一玩。
这个页面是星云的"试驾区",你能直接在浏览器里和内置数字人对话——我看到的有"睡前陪伴"(萌萌的水獭形象,海滩主题)、"元气段子手"、"AI 男友"等几个内置形象。直接点底部任意一个,然后右下角"开始聊天",就能和它说话。
Step 3:点击"创建具身应用"
体验完之后,右上角"创建具身应用"按钮就是我们的目的地。点进去会让你做几个关键选择:
选择形象
这是最影响最终用户体验的一步。星云的形象库分几个风格档位:超写实、美型、卡通、二次元。服务台场景的形象选型逻辑:
- 不要选超写实——商场环境光线复杂,超写实形象在弱光或强光下容易出现"恐怖谷"。除非你的部署场景光线极度可控(比如展厅射灯),否则避开
- 美型/卡通是首选——既有人形识别度,又有"我是数字人"的合理预期,不会让顾客产生"对方是不是真人"的错位感
- 二次元谨慎选——除非是动漫主题商场或者目标客群是 Z 世代,普通商场用二次元会显得不正式
我最终选了一个偏成熟的女性卡通形象,30 岁左右,职业装感——既亲切又有"服务员"该有的专业感。
选择音色
音色库我快速试听了一遍,大致分:亲切女声 / 专业女声 / 活泼少女音 / 中年男声 / 商务男声。
服务台场景我选的是亲切但不娃娃音的女声——太活泼会显得不专业,太成熟会显得有距离感。这一步建议你听一遍每一个音色再选,文字描述很难还原音色的实际气质。
选择表演风格
这个我理解是数字人的"基础节奏"——平稳/活泼/专业。服务台选"专业",讲解类场景可以选"活泼"。
Step 4:进入"应用管理",拿到 appId / appSecret
应用创建完之后,左侧菜单"应用管理"页面里能看到刚才创建的应用,点进去 → 查看密钥,就能拿到appId和appSecret。这两个就是后面所有 SDK 代码里要用的凭证。
到这一步,准备工作就完成了。下面进入代码部分。
4.2 接入 JavaScript SDK,让数字讲解员开口
前端工程使用 Vue 3、TypeScript 和 Vite。项目结构不需要设计得过于复杂,只要把数字人驱动、模型请求和流式播报分开即可:
deepseek-exhibition-agent/ ├─ src/ │ ├─ components/ │ │ ├─ AvatarPanel.vue │ │ └─ ExhibitCard.vue │ ├─ composables/ │ │ └─ useStreamSpeak.ts │ ├─ App.vue │ └─ main.ts ├─ server/ │ ├─ index.ts │ └─ data/ │ └─ exhibits.json └─ index.html我使用 ChatGPT 辅助拆分了组件和 TypeScript 类型,但给它设置了一个明确约束:不得根据常见数字人 SDK 的经验自行补充接口,所有方法都必须来自魔珐星云官方文档。
引入 SDK
在index.html中直接引入 JavaScript SDK:
<!doctype html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>DeepSeek 展厅讲解 Agent</title> </head> <body> <div id="app"></div> <script src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar@latest.js"></script> <script type="module" src="/src/main.ts"></script> </body> </html>然后将 SDK 封装为独立的AvatarPanel.vue:
<template> <div class="avatar-wrapper"> <div id="xmov-avatar" class="avatar-container"></div> <div v-if="!ready" class="loading-mask"> <p>数字讲解员资源加载中:{{ progress }}%</p> <progress :value="progress" max="100"></progress> </div> </div> </template> <script setup lang="ts"> import { onBeforeUnmount, onMounted, ref } from "vue"; declare const XmovAvatar: any; const emit = defineEmits<{ ready: []; voiceState: [status: string]; widget: [type: string, data: Record<string, unknown>]; }>(); const ready = ref(false); const progress = ref(0); let avatar: any = null; onMounted(async () => { avatar = new XmovAvatar({ containerId: "#xmov-avatar", appId: import.meta.env.VITE_XMOV_APP_ID, appSecret: import.meta.env.VITE_XMOV_APP_SECRET, gatewayServer: "https://nebula-agent.xingyun3d.com/user/v1/ttsa/session", hardwareAcceleration: "prefer-hardware", enableLogger: true, onMessage(message: { code?: number; message?: string }) { console.log("[Xmov message]", message); if (message.code && message.code >= 10000) { console.error("[Xmov error]", message.message); } }, onStateChange(state: string) { console.log("[Xmov state]", state); }, onVoiceStateChange(status: string) { console.log("[Xmov voice]", status); emit("voiceState", status); }, onNetworkInfo(info: { rtt: number; downlink: number; }) { console.log("[Xmov network]", info); }, proxyWidget: { widget_exhibit_card(data: Record<string, unknown>) { emit("widget", "exhibit_card", data); }, }, }); await avatar.init({ initModel: "normal", onDownloadProgress(value: number) { progress.value = value; console.log(`[Xmov loading] ${value}%`); }, }); ready.value = true; emit("ready"); }); onBeforeUnmount(() => { avatar?.destroy(); avatar = null; }); function speak( content: string, isStart: boolean, isEnd: boolean, ): void { if (!avatar) { throw new Error("数字讲解员尚未初始化"); } avatar.speak(content, isStart, isEnd); } function interactiveIdle(): void { avatar?.interactiveidle(); } function idle(): void { avatar?.idle(); } defineExpose({ speak, interactiveIdle, idle, }); </script> <style scoped> .avatar-wrapper { position: relative; width: 540px; height: 960px; overflow: hidden; background: #101522; } .avatar-container { width: 100%; height: 100%; } .loading-mask { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; color: #fff; background: rgba(8, 14, 28, 0.88); } </style>官方 SDK 通过XmovAvatar创建实例,初始化时需要提供容器、App ID、App Secret 和服务地址。init()负责建立连接并加载资源,speak()负责驱动数字人播报,interactiveidle()可以将数字人切换回交互待机状态,页面卸载前则应调用destroy()清理资源。
首次加载不是“黑屏”
第一次启动页面时,数字人不会立即出现。SDK 需要先加载当前应用对应的角色、表情和动作资源,如果界面没有任何提示,容器很容易看起来像一块黑屏。
因此,我将onDownloadProgress直接绑定到了页面进度条上。官方文档说明,该回调范围为 0—100;如果首次所需资源加载失败,进度不会正常到达 100。
另一个容易踩到的问题是访问环境。SDK 使用的部分浏览器能力只支持localhost或 HTTPS。本地开发可以直接使用localhost,但通过普通 HTTP 局域网地址访问时,相关能力可能无法正常工作。([魔珐星云][1])
数字人初始化完成后,先使用固定欢迎语验证主链路:
avatarRef.value?.speak( "欢迎来到人工智能主题展区," + "我是本次展览的数字讲解员。" + "您可以向我了解展品原理、技术背景和参观路线。", true, true, );speak()的三个参数分别是播报内容、是否为本轮流式内容的开始,以及是否为本轮流式内容的结束。一次性提交完整文本时,后两个参数都设置为true。
点击测试按钮后,数字讲解员开始播放欢迎语,口型和身体动作随语音一起呈现。到这里,数字人这一侧已经跑通,接下来需要把固定文本替换为 DeepSeek-V4-Pro 的实时回答。
4.3 接入 DeepSeek-V4-Pro,实现流式具身讲解
服务端主要做三件事:接收参观者问题、加载相关展品资料、调用 DeepSeek-V4-Pro 并把流式内容转发给前端。
本次先使用一份本地exhibits.json保存展品资料:
[ { "id": "robot-arm-01", "name": "六轴工业机器人", "zone": "智能制造展区", "summary": "通过六个旋转关节完成多方向运动,可用于装配、搬运、焊接和精密操作。", "keywords": ["机械臂", "六轴", "机器人", "智能制造"] }, { "id": "edge-chip-01", "name": "边缘计算芯片", "zone": "人工智能展区", "summary": "在终端本地处理部分人工智能任务,降低数据频繁往返云端的需求。", "keywords": ["芯片", "边缘计算", "端侧AI"] } ]服务端根据问题筛选相关展品,将资料与问题一起交给模型。DeepSeek 官方 API 当前支持deepseek-v4-pro,并通过stream: true以 SSE 形式返回内容增量;本文关闭思考模式,只将最终讲解文本传给数字人。
// server/index.ts import "dotenv/config"; import express from "express"; import { readFile } from "node:fs/promises"; const app = express(); app.use(express.json()); const exhibits = JSON.parse( await readFile( new URL("./data/exhibits.json", import.meta.url), "utf-8", ), ); const SYSTEM_PROMPT = ` 你是科技展厅的数字讲解员。 回答要求: 1. 只能根据提供的展品资料回答; 2. 不编造资料中不存在的信息; 3. 使用自然、清晰、适合口头讲解的中文; 4. 单次回答控制在100字以内; 5. 不使用Markdown标题、列表和表格; 6. 讲到具体展品时,可以插入对应的展品卡片指令。 `; app.post("/api/chat", async (req, res) => { const question = String(req.body?.question ?? "").trim(); if (!question) { return res.status(400).json({ message: "问题不能为空", }); } const matched = exhibits.filter((item: any) => item.keywords.some((keyword: string) => question.includes(keyword), ), ); const context = (matched.length ? matched : exhibits) .slice(0, 5) .map( (item: any) => `ID:${item.id} 展品:${item.name} 展区:${item.zone} 资料:${item.summary}`, ) .join("\n\n"); const upstream = await fetch( "https://api.deepseek.com/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.DEEPSEEK_API_KEY}`, }, body: JSON.stringify({ model: "deepseek-v4-pro", thinking: { type: "disabled", }, stream: true, messages: [ { role: "system", content: SYSTEM_PROMPT, }, { role: "user", content: `展品资料:\n${context}\n\n` + `参观者问题:${question}`, }, ], }), }, ); if (!upstream.ok || !upstream.body) { const detail = await upstream.text(); return res.status(502).json({ message: `DeepSeek 请求失败:${detail}`, }); } res.setHeader("Content-Type", "text/event-stream"); res.setHeader("Cache-Control", "no-cache"); res.setHeader("Connection", "keep-alive"); const reader = upstream.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true, }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; const payload = line.slice(6).trim(); if (payload === "[DONE]") { res.write( `data: ${JSON.stringify({ done: true, })}\n\n`, ); res.end(); return; } const chunk = JSON.parse(payload); const token = chunk.choices?.[0]?.delta?.content ?? ""; if (token) { res.write( `data: ${JSON.stringify({ token, })}\n\n`, ); } } } res.end(); }); app.listen(3001, () => { console.log("Agent server: http://localhost:3001"); });前端收到的不是一整段回答,而是一串持续到达的文本增量。这里不能每收到一个 Token 就立即调用speak():中文可能被切得过碎,SSML 标签也可能在中间断开,数字人甚至会把不完整的标签当作普通文本读出来。
我的处理方式是先把 Token 写入缓冲区,在完整句子或完整标签边界处再提交给 SDK:
// src/composables/useStreamSpeak.ts import type { Ref } from "vue"; interface AvatarInstance { speak: ( content: string, isStart: boolean, isEnd: boolean, ) => void; interactiveIdle: () => void; } export function useStreamSpeak( avatarRef: Ref<AvatarInstance | null>, ) { let buffer = ""; let firstChunk = true; const FIRST_FLUSH_THRESHOLD = 20; function start(): void { buffer = ""; firstChunk = true; } function feed(token: string): void { buffer += token; if ( firstChunk && buffer.length < FIRST_FLUSH_THRESHOLD ) { return; } if (!isSafeBoundary(buffer)) { return; } flush(false); } function finish(): void { flush(true); } function flush(isEnd: boolean): void { if (!buffer && !isEnd) return; if (buffer) { avatarRef.value?.speak( `<speak>${buffer}</speak>`, firstChunk, isEnd, ); buffer = ""; firstChunk = false; } } function isSafeBoundary(content: string): boolean { // 当前存在尚未闭合的 XML/SSML 标签 if (/<[^>]*$/.test(content)) { return false; } return /[。!?;!?]$/.test(content); } return { start, feed, finish, }; }魔珐星云官方文档支持分段调用speak():第一段设置is_start=true,最后一段设置is_end=true,中间内容的两个标记均为false。文档也特别建议,首次流式提交前先积累一小段内容,避免数字人说话速度追上模型输出速度而产生停顿。
加入动作与展品信息卡
仅仅做到“边生成边播报”,数字人仍然可能像在机械念稿。下一步是使用 SSML 中的 KA 指令,为讲解增加动作。
例如欢迎参观者时,可以插入官方示例中的欢迎动作:
<speak> <ue4event> <type>ka_intent</type> <data> <ka_intent>Welcome</ka_intent> </data> </ue4event> 欢迎来到智能制造展区。 </speak>魔珐星云 SDK 支持在speak()的 SSML 内容中嵌入语义 KA、技能 KA 等指令。具体可用动作应通过当前应用的 KA 查询结果确认,不能凭经验随意填写动作名称。
除了动作,我还增加了一个自定义的widget_exhibit_card事件。当讲解员介绍某件展品时,模型可以在合适的位置嵌入展品 ID:
<speak> 这件展品是六轴工业机器人。 <ue4event> <type>widget_exhibit_card</type> <data> <id>robot-arm-01</id> </data> </ue4event> 它通过六个旋转关节完成多方向运动, 常用于装配、搬运和焊接。 </speak>前面初始化XmovAvatar时已经通过proxyWidget注册了该事件。父组件收到回调后,只需要根据 ID 查询本地展品数据:
function onWidget( type: string, data: Record<string, unknown>, ): void { if (type !== "exhibit_card") return; currentExhibit.value = exhibits.find( (item) => item.id === data.id, ) ?? null; }这样,数字讲解员说到对应展品时,侧边区域会同步出现展品名称、图片、展区和核心介绍。SDK 官方提供onWidgetEvent与proxyWidget两种事件代理方式,也允许业务侧实现自定义 Widget。
4.4 实现可打断的多轮讲解
流式播报跑通以后,最后一个关键问题是中断。
参观者可能在讲解尚未结束时提出新问题。此时只停止声音还不够:旧的 DeepSeek 请求仍可能继续返回内容,前端缓冲区里也可能保留尚未提交的句子。如果不统一清理,新的问题开始以后,旧回答还会重新进入播报队列。
因此,一次完整中断需要同时处理三层状态:
模型层:终止或忽略旧 DeepSeek 请求 业务层:清空文本缓冲与旧任务标记 具身层:调用 interactiveidle() 中断当前状态前端使用AbortController管理当前请求:
let activeController: AbortController | null = null; let requestVersion = 0; async function askAgent(question: string): Promise<void> { const version = ++requestVersion; activeController?.abort(); activeController = new AbortController(); avatarRef.value?.interactiveIdle(); streamSpeak.start(); const response = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ question }), signal: activeController.signal, }); if (!response.ok || !response.body) { throw new Error("讲解服务请求失败"); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let sseBuffer = ""; try { while (true) { const { done, value } = await reader.read(); if (done) break; if (version !== requestVersion) return; sseBuffer += decoder.decode(value, { stream: true, }); const events = sseBuffer.split("\n\n"); sseBuffer = events.pop() ?? ""; for (const event of events) { const line = event .split("\n") .find((item) => item.startsWith("data: "), ); if (!line) continue; const data = JSON.parse(line.slice(6)); if (data.token) { streamSpeak.feed(data.token); } if (data.done) { streamSpeak.finish(); } } } } catch (error) { if ( error instanceof DOMException && error.name === "AbortError" ) { return; } throw error; } } function interruptAndAsk( nextQuestion: string, ): void { requestVersion += 1; activeController?.abort(); avatarRef.value?.interactiveIdle(); void askAgent(nextQuestion); }interactiveidle()在官方文档中的用途之一,就是打断当前状态并返回交互待机;一次完整speak()结束以后,文档也建议先通过interactive_idle完成状态切换,再开始下一轮播报。([魔珐星云][1])
数字讲解员的状态可以简化为:
interactive_idle ↓ 用户提问 waiting_model ↓ 第一段内容到达 speaking ↓ 播报结束 interactive_idle当用户在speaking状态中再次提问时,则走另一条路径:
speaking ↓ 用户插话 interrupting ↓ 取消旧请求、清理旧缓冲 waiting_model ↓ 播放新回答 speaking为了判断数字人什么时候真正开始和结束讲话,可以监听onVoiceStateChange。官方文档说明,speak()过程中会抛出语音开始和结束事件,可用于管理当前播报状态。
完成中断逻辑后,我连续测试了三种情况:
- 正常提问并等待完整回答;
- 长回答生成过程中继续追问;
- 数字人正在播报时立即改问另一件展品。
重点观察旧请求是否停止、缓存内容是否清空、新旧回答是否发生串音,以及数字人能否重新进入正常播报状态。
到这里,展厅讲解 Agent 的完整主链路已经打通:
这套 Demo 真正解决的并不是“让数字人读出一段大模型回答”,而是验证了具身交互智能对纯文本 Agent 的升级作用。DeepSeek 负责内容推理,魔珐星云补齐拟人交互全套能力,将单向文字输出升级为有形象、有情绪、可实时对话的完整现场交互。
当数字讲解员能够根据现场问题动态组织内容、同步展示展品信息,并允许参观者随时改变交流方向时,屏幕才从一个内容播放器,真正变成了可以交流的展厅服务入口。
可以。第五章直接承担全文收束,不再继续拆流式缓冲、中断机制等实现细节;这些都留在第四章实战中完成。第五章重点总结三件事:Demo 验证了什么、魔珐星云提供了什么价值、具身交互为什么不仅适用于展厅。
五、从展厅原型到具身交互落地:开发体验与价值总结
5.1 这次接入真正验证了什么
回看整个搭建过程,这个 Demo 完成的并不只是“让 DeepSeek 驱动一个数字人说话”。
如果只是将大模型输出接入 TTS,让页面播放一段语音,其实很快就能完成。但那种方案依旧停留在文本问答的延伸形态:模型负责生成,语音负责朗读,屏幕上的形象与整个对话过程没有形成真正的协同。
这次接入魔珐星云 SDK 后,DeepSeek 生成的讲解内容能够继续转化为数字人的语音、口型、表情和身体动作;当讲解涉及具体展品时,页面还能同步展示对应的信息卡;用户在播报过程中改变问题,系统也可以停止上一轮任务并切换到新的讲解内容。
从用户视角看,整个过程不再是“提交问题后等待一段文字”,而是:
提出问题 → 数字讲解员开始组织回答 → 语音、口型和动作同步呈现 → 展品信息随讲解出现 → 用户可以继续追问或中途插话这些变化单独看都不复杂,但组合起来之后,屏幕的角色就不再只是一个负责播放内容的数字形象,而开始具备智能体的基本交互特征。
这也是这次实战最重要的结论:具身交互的价值不在于让 AI 看起来更像人,而在于让 AI 的理解、表达和反馈发生在同一条交互链路中。
DeepSeek 解决了“讲什么”,Agent 业务层负责组织展品资料、维护对话和管理任务,魔珐星云则解决“如何讲出来”。三者各自独立,又通过清晰的接口协同工作。
这种分层方式也为后续扩展留下了空间。未来无论是将本地展品数据替换为 RAG 知识库,还是接入路线查询、展项状态、票务信息和多语言能力,变化主要发生在认知层和业务层,数字人的具身表达链路不需要重新搭建。
5.2 魔珐星云提供的,不只是一个数字人形象
在接入之前,我对数字人产品的第一反应仍然是形象、音色和动作效果。但真正把 SDK 接进项目后能清晰感知,魔珐星云绝非单纯 3D 形象工具,而是标准化具身交互智能底层基础设施。整合语音合成、3D 渲染、表情肢体解算、交互状态管理全套能力,一套 SDK 即可完成完整拟人交互链路,解决各类 Agent 缺失具象表达的行业难题。
在第四章的实战中,这种整合最明显地体现在三个地方。
第一是流式表达。DeepSeek 不需要等完整回答全部生成后,再交给数字人一次性播放,而是可以按照语义片段持续提交。数字讲解员能够更早开始表达,现场交流中的等待感也随之降低。
第二是非语言表达。语音只是讲解的一部分,口型、表情和身体动作决定了这段内容是否像一次真正的交流。通过 KA 指令,模型生成的语言内容可以与动作意图一起进入表达链路,数字人不再只是站在屏幕中机械念稿。
第三是业务界面协同。Widget 事件让数字人的讲解能够与展品卡片、图片、字幕和其他页面内容同步。它不是把一个独立数字人窗口叠加到现有页面上,而是让数字人表达与业务界面形成统一节奏。
从底层技术路径看,魔珐星云强调参数流、AI 端渲和端侧解算。其核心思路不是持续向终端传输已经渲染完成的视频画面,而是传递具身表达所需的驱动信息,再由终端完成实时呈现。这种方式减少了对完整云端视频流和持续云端渲染资源的依赖,也为低延时、高并发和多终端部署提供了基础。
因此,魔珐星云不是给 Agent 套上一层数字人外观,也不是替代 DeepSeek 这样的模型。更准确地说:
大模型解决 AI 的“大脑”,Agent 负责执行与流程,魔珐星云补齐 AI 的身体、表达和实时交互。
这也是为什么本文最终采用“具身交互”而不是“数字人播报”来描述这套方案。前者关注完整的人机交流,后者更接近内容展示。
5.3 从一块展厅屏幕,走向更多真实服务终端
展厅是一个很适合验证具身交互的场景。
传统展厅屏幕通常循环播放图片、视频和固定讲解内容。即使加入搜索或问答功能,交互入口也大多还是文本框。接入 DeepSeek 和魔珐星云后,屏幕可以根据参观者的问题动态组织内容,并通过数字讲解员完成语音和动作表达。
它所带来的变化,不只是“展厅里多了一个数字人”,而是原本单向展示的屏幕开始具备接收问题、组织信息和持续交流的能力。
这套思路也不局限于展厅。
在零售门店中,它可以成为能够介绍商品、展示商品卡片并接受顾客插话的导购 Agent;在教育培训中,它可以将知识问答转化为带有语音、表情和动作的讲解过程;在文旅场景中,它可以承担景点导览和文化内容介绍;在政务、金融和酒店服务中,它也可以成为业务系统面向用户的可视化交互入口。
终端形态同样可以继续延展。本文使用网页完成接入,是为了方便开发和验证,但相同的认知与表达链路还可以进入商显大屏、自助终端、应用、人形机器人以及 AR/VR 等设备。
这说明具身交互并不是某一种硬件独占的能力。它更像是连接大模型、Agent 与真实终端的一层通用接口:上层可以替换模型和业务系统,下层可以适配不同的屏幕和设备,中间的具身表达能力保持相对稳定。
回到文章最开始的问题:当 DeepSeek 已经能够理解、推理和生成内容后,AI 为什么还需要一个身体?
经过这次接入,我的答案更加明确。
身体不是装饰,而是 AI 进入真实场景的交互界面。声音、口型、表情、动作和状态反馈,共同决定用户是否愿意把屏幕里的系统当作一个可以交流的对象。只有当这些表达能力与大模型的认知能力真正连接起来,AI 才能从聊天框中的文本工具,转变为出现在展厅、门店、学校和服务终端中的具身智能体。
DeepSeek 让数字讲解员知道应该回答什么,魔珐星云让它知道应该如何站在用户面前完成表达。
从文本问答到具身交互,改变的不只是 AI 的外观,而是人与 AI 发生交流的方式。
写在最后:大模型让 AI 学会思考,魔珐星云让 AI 进入终端
这次实践让我更加确信,AI 赛道竞争已经从文本推理比拼,转向具身交互智能落地能力比拼。大模型让 AI 学会思考,魔珐星云让 AI 进入终端,以具身的方式与人交互。
从展厅讲解出发,这套方案还可以继续延展到零售导购、教育陪练、文旅导览、政务咨询以及机器人等场景。真正有价值的,不只是让数字人“开口说话”,而是让 AI 能够进入现场、理解用户,并以更自然的方式完成持续交流。
相关开发资料可参考:
- 魔珐星云开发者文档与 SDK:https://xingyun3d.com/developers/52-183
- 魔珐星云 AI Coding 操作手册:https://rsjqcmnt5p.feishu.cn/wiki/ULNQwoiKwid2tVkTpAlcMb49nKg
- 魔珐星云官网:https://xingyun3d.com/?utm_campaign=jixinghuiKoc228&utm_source=jixinghuiKoc&utm_medium=&utm_term=&utm_content=