一、交互范式重构:大模型如何从“理解世界”走向“进入现场”

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

应用创建完之后,左侧菜单"应用管理"页面里能看到刚才创建的应用,点进去 → 查看密钥,就能拿到 appIdappSecret这两个就是后面所有 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 官方提供 onWidgetEventproxyWidget 两种事件代理方式,也允许业务侧实现自定义 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() 过程中会抛出语音开始和结束事件,可用于管理当前播报状态。

完成中断逻辑后,我连续测试了三种情况:

  1. 正常提问并等待完整回答;
  2. 长回答生成过程中继续追问;
  3. 数字人正在播报时立即改问另一件展品。

重点观察旧请求是否停止、缓存内容是否清空、新旧回答是否发生串音,以及数字人能否重新进入正常播报状态。

到这里,展厅讲解 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 能够进入现场、理解用户,并以更自然的方式完成持续交流。

相关开发资料可参考:

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐