从纯文本到具身交互智能:魔珐星云+国产大模型搭建拥有 3D 具身的展厅Agent
一、交互范式重构:大模型如何从“理解世界”走向“进入现场”
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=
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)