核心逻辑一句话:大模型流式输出文本 → 魔珐星云参数流驱动 → 端侧实时渲染 3D 数字人,全程低延迟、可打断、动作表情跟对话同步。
不用高配 GPU,普通电脑 / 开发板就能跑,10 分钟快速搭完,直接看效果!
一、AI 只会打字?缺的是「具身表达层」
现在大模型再强,交互也停留在冷冰冰的对话框:
-
❌ 只有文字,没表情没动作,没温度
-
❌ 延迟高,对话卡顿,不能打断
-
❌ 没法落地大屏、终端等实体场景
本质问题:Agent 只有「大脑」,没有「身体」,魔珐星云刚好补上这层短板。
二、核心破局:参数流替代视频流,低配也能实时动
传统数字人传 1080P 视频,吃带宽又吃算力;魔珐星云走参数流架构:
-
✅ 只传口型 / 表情 / 骨骼参数(仅几百 KB)
-
✅ 端侧 WebGL 实时渲染,集成显卡 / 百元开发板流畅跑
-
✅ 流式并发驱动,1 秒内响应,支持随时打断
一句话:云端算逻辑,端侧画 3D,大模型边输出,数字人边说边动。
三、10 行代码上屏:从注册到数字人显示
理论讲够了,动手。这一节,我带你40 分钟内把一个 3D 数字人放进你自己的 HTML 页面。
3.1 控制台:创建你的第一个驱动应用
打开 xingyun3d.com,注册开发者账号 → 进入控制台 → 应用管理 → 创建驱动应用。
创建流程按引导走:
-
形象:目前有几十款预置形象(从业务人员到游戏感风格)
-
场景:背景画面(淡彩渐变、办公室、演播厅等)
-
音色:超过 30 款,覆盖主播、客服、教师、博主,甚至有韩语和俄语音色
-
表演:动作风格(活泼 / 稳重 / 专业)
我这次选的配置是"智能导购员 Demo":
-
形象:和合
-
音色:温柔小欣(青年对话 / 温柔暖心)
-
场景:淡彩渐变
-
动作:活泼
-
表情:开心、严肃、疑惑
创建成功后点 "App 密钥" 按钮,拿到两串关键字符串:
-
App ID -
App Secret
妥善保存,等会儿代码里要用。
3.2 最小可运行 HTML(真·10 行主逻辑)
新建一个 index.html,贴如下代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>我的第一个具身 Agent</title>
<style>
#avatar-container { width: 800px; height: 450px; position: relative; }
#sdk { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="avatar-container"><div id="sdk"></div></div>
<!-- 星云 JS SDK -->
<script src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar@latest.js"></script>
<script>
const sdk = new XmovAvatar({
containerId: "#sdk",
appId: "your_appid_here", // 替换成你的 App ID
appSecret: "your_appsecret_here", // 替换成你的 App Secret
gatewayServer: "https://nebula-agent.xingyun3d.com/user/v1/ttsa/session",
onMessage: (m) => console.log("SDK:", m),
});
sdk.init({
onDownloadProgress: (p) => console.log("资源下载:", p + "%"),
});
</script>
</body>
</html>
真的就是这些。浏览器本地打开(或挂一个静态服务器,因为 SDK 要求 localhost 或 https 域名)——等进度条跑完,数字人就在画面里了。
3.3 三个容易踩的坑
第一次跑建议先避开这几个:
坑 1:容器比例必须和控制台选的应用类型一致 横屏应用用 16:9(如 800×450),竖屏应用用 9:16(如 450×800)。比例不对的话数字人画面会变形或跑偏。
坑 2:不能用 IP 访问 SDK 内部用了 WebGL / WebCodecs / WebWorker 一些只在 localhost 或 https 下才开放的 API。开发时用 localhost:xxxx,部署时上 HTTPS,用 IP 直连会静默失败。
坑 3:init() 返回 Promise,别忘了 .catch 资源下载、鉴权失败都会让 Promise 被 reject。加上 .catch(error => console.error(error)),排障速度快 10 倍。
3.4 让它开口说话(加 5 行代码)
上一段只是把数字人"挂"出来,现在让它说话。在 sdk.init() 之后加:
// 资源下载完成后,调用 speak
sdk.init({
onDownloadProgress: (p) => console.log("资源下载:", p + "%"),
}).then(() => {
sdk.speak(
"欢迎来到我的具身 Agent 实验,让我们开始吧。",
true, // is_start
true // is_end
);
});
speak() 的签名是 (ssml, is_start, is_end)——这三个参数看起来朴素,但下一章你会看到,它们就是为 LLM 流式输出设计的。
到这一步,你已经完成了。
下一章,我们让它对接大模型。
四、让 Agent 开口说话:对接 LLM 流式输出
这一章是全文技术密度最高的一段。如果你跳过前面都可以,这段一定要仔细看——它决定了你的 Agent 会不会像个"真人"。
4.1 聊聊这个 speak 接口:解决流式交互“顿挫感”的解药
先看签名:
sdk.speak(ssml: string, is_start: boolean, is_end: boolean): void
三个参数看起来平平无奇。但你把它和 LLM 流式输出的数据形状放在一起看:
LLM Stream Chunk 1: "欢迎" → speak("欢迎", true, false)
LLM Stream Chunk 2: "来到" → speak("来到", false, false)
LLM Stream Chunk 3: "魔珐星云" → speak("魔珐星云", false, false)
...
LLM Stream Chunk N: "就是这样" → speak("就是这样", false, true)
一对一映射。is_start=true 告诉 SDK "开始新的一轮说话",is_end=true 告诉 SDK "这段说完了"。中间所有 chunk 只管往里喂。
这意味着什么?意味着你从 OpenAI SSE、Anthropic Stream、任何一个 LLM 流式接口拿到的 delta,可以几乎零逻辑转换直接丢给 SDK。LLM 边生成,数字人边说——不等 LLM 输完整段。
4.2 工程最佳实践:按标点切片
直接按 chunk 喂会有个小问题:LLM 的 token 切分和语义单元不一致,有时候一个 chunk 就 1-2 个字,会让 TTS 节奏发飘。
我的最佳实践是按标点/句子切片,再喂给 SDK:
class AvatarBridge {
constructor(sdk) {
this.sdk = sdk;
this.buffer = "";
this.isFirst = true;
}
// 接收 LLM 流式 chunk
feed(textDelta) {
this.buffer += textDelta;
// 匹配完整句子(中英文标点)
const match = this.buffer.match(/^[^。!?;.!?;]*[。!?;.!?;]/);
if (match) {
const sentence = match[0];
this.sdk.speak(sentence, this.isFirst, false);
this.isFirst = false;
this.buffer = this.buffer.slice(sentence.length);
}
}
// LLM 流式结束
end() {
const finalText = this.buffer.trim();
if (finalText) {
this.sdk.speak(finalText, this.isFirst, true);
} else {
this.sdk.speak("", false, true);
}
this.buffer = "";
this.isFirst = true;
}
}
40 行不到,就把"LLM 流式 → 具身表达"这座桥搭好了。
4.3 三套大模型对接示例
对接 Anthropic Claude(推荐,代码最简洁)
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
async function talkWithClaude(userInput, bridge) {
const stream = await client.messages.stream({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: userInput }],
});
for await (const event of stream) {
if (event.type === "content_block_delta" &&
event.delta?.type === "text_delta") {
bridge.feed(event.delta.text);
}
}
bridge.end();
}
对接 OpenAI GPT
import OpenAI from "openai";
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function talkWithGPT(userInput, bridge) {
const stream = await client.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: userInput }],
stream: true,
});
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content;
if (text) bridge.feed(text);
}
bridge.end();
}
对接国产大模型(以 DeepSeek 为例,通义/豆包同理)
async function talkWithDeepSeek(userInput, bridge) {
const resp = await fetch("https://api.deepseek.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.DEEPSEEK_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "deepseek-chat",
messages: [{ role: "user", content: userInput }],
stream: true,
}),
});
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const data = line.slice(6);
if (data === "[DONE]") continue;
const delta = JSON.parse(data).choices[0]?.delta?.content;
if (delta) bridge.feed(delta);
}
}
bridge.end();
}
三个 LLM,同一个 bridge.feed() 入口。切换模型只改一个函数,不改数字人端任何代码。
4.4 状态机:让 Agent 生命周期可见
星云的 5 个状态天生对应 Agent 的生命周期:
星云状态
Agent 生命阶段
用户感知
idle
空闲待命
数字人做微表情、等候
interactive_idle
等待过渡
可被打断、可切走
listen
接收输入
数字人侧耳倾听
think
LLM 推理中
数字人思考表情(关键!消除 Agent 沉默感)
speak
流式输出中
数字人边说边做动作
think 状态特别重要。传统数字人方案,LLM 在推理的那 1-3 秒,屏幕上就是一个"尴尬静止"——用户以为卡了。星云在这段给你一个"思考中"的动画表情,体验差距是降维的。
调用方式:
// 用户说完话,切到 think
sdk.think();
// LLM 首个 chunk 到达,bridge 自动切到 speak 状态
bridge.feed(firstChunk);
// speak 结束(bridge.end 已处理)
// 自动回到 interactive_idle
4.5 终极武器:能被打断的 Agent
真实对话里,用户会插话、改口、纠正——这是 ChatGPT 对话框体验永远赶不上真人的原因之一。
星云给了一个"排队式打断"的优雅方案,把它封装成 SpeechManager:
class SpeechManager {
constructor(sdk) {
this.sdk = sdk;
this._pendingText = null;
}
// 打断当前并插话
interruptAndSpeak(text) {
this._pendingText = text;
this.sdk.interactiveidle(); // 发打断信号
}
// 状态监听(需绑到 SDK 的 onVoiceStateChange)
handleVoiceState(res) {
const state = typeof res === "string" ? res : (res.state || res.data);
// 上一句彻底结束,执行新指令
if ((state === "end" || state === "idle") && this._pendingText) {
const nextText = this._pendingText;
this._pendingText = null;
this.sdk.speak(nextText, true, true);
}
}
}
// 绑定
const mgr = new SpeechManager(sdk);
const sdk = new XmovAvatar({
// ...
onVoiceStateChange: (res) => mgr.handleVoiceState(res),
});
// 使用:数字人说到一半,用户问了新问题
mgr.interruptAndSpeak("好,我听到了,你刚才问的是……");
这段代码是星云在 Agent 场景真正的杀手锏。它解决了一个多轮对话 Agent 最尴尬的工程问题——"指令冲突",而它的思路不是互斥锁,是一个异步的"状态监听+指令消费"。
一句话:MCP 让 Agent 有了手,speak+SpeechManager 让 Agent 有了可被打断的嘴。
五、动作指令:像调用函数一样让数字人“动起来”
到这里,数字人已经会边思考边说话、还能被打断。但它还缺一件事——动作。
光说不做,还是"脱实向虚"。让数字人在说"请看左边大屏"的时候真的指向左边,这篇文章才算对得起"具身"两个字。
5.1 KA 接口到底是什么
KA = Knowledge Action,魔珐星云的动作语义库。文档里叫 "具身驱动 KA 查询接口"。
一句话:GET /user/v1/external/lite_ka_summary 会返回当前应用可用的所有动作清单,每个动作有英文名、中文名、类型、预览图、预览动画。
这就是后面要把动作变成 LLM function tool 的金矿。
5.2 鉴权:MD5 签名五步法
星云 API 用一套相对少见的 MD5 签名方案(X-APP-ID + X-TIMESTAMP + X-TOKEN)。完整 Python 实现:
import time, json, hashlib, requests
from urllib.parse import urljoin
def sign_headers(ak, secret, method, url, data):
t = int(time.time())
data_str = json.dumps(dict(data), sort_keys=True).replace(" ", "")
ori = f"{url.lower()}{method.lower()}{data_str}{secret}{t}"
return {
"X-APP-ID": ak,
"X-TOKEN": hashlib.md5(ori.encode("utf-8")).hexdigest(),
"X-TIMESTAMP": str(t),
}
HOST = "https://nebula-agent.xingyun3d.com"
URL = "/user/v1/external/lite_ka_summary"
def fetch_ka_list(ak, secret):
headers = sign_headers(ak, secret, "GET", URL, {})
r = requests.get(urljoin(HOST, URL), headers=headers)
return r.json()["data"]
关键细节:
-
JSON 必须 sort_keys + 去空格 — 不然签名对不上
-
时间戳 60 秒内有效 — 客户端和服务端时钟偏差要注意
-
url 全部小写 — query string 也要小写
5.3 动作清单长这样
[ { "name": "M_CN03_show03__PointingSelf", "cn_name": "指向自己", "ka_type": "body_action", "render_image_oss": "https://.../preview.png", "render_movie_oss": "https://.../preview.mp4" }, { "name": "M_CN03_show03__Welcome", "cn_name": "欢迎", "ka_type": "semantic_intent", ... }, ...]
使用时只取 name 最后一段(如 PointingSelf),塞到 SSML 里:
<speak>
好的,请看
<ue4event>
<type>ka</type>
<data><action_semantic>PointingSelf</action_semantic></data>
</ue4event>
左边大屏幕
</speak>
数字人会先做"指向自己"动作,然后说话同时手势配合。
5.4 把 KA 列表转成 LLM function tools
核心来了。我们要让 LLM 在生成回复时,自动从动作库里挑一个合适的动作搭配文本。这里我用 Anthropic 的 Tool Use 演示(Claude 的 tool 定义和 OpenAI 的 function calling 几乎一样):
from anthropic import Anthropic
client = Anthropic()
ka_list = fetch_ka_list(AK, SECRET)
# 动态生成 tool 定义
action_enum = [ka["name"].split("__")[-1] for ka in ka_list]
action_desc = "\n".join(
f"- {ka['name'].split('__')[-1]}: {ka['cn_name']}" for ka in ka_list
)
tools = [{
"name": "respond_with_action",
"description": "用一个具身动作 + 一段口头回复来响应用户。",
"input_schema": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": action_enum,
"description": f"从以下动作中选择最合适的:\n{action_desc}"
},
"dialogue": {
"type": "string",
"description": "数字人要说的话"
}
},
"required": ["action", "dialogue"]
}
}]
def agent_respond(user_input):
resp = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
tools=tools,
tool_choice={"type": "tool", "name": "respond_with_action"},
messages=[{"role": "user", "content": user_input}],
)
# 提取 LLM 选择的动作和台词
for block in resp.content:
if block.type == "tool_use" and block.name == "respond_with_action":
return block.input["action"], block.input["dialogue"]
现在用户说一句话,LLM 就会返回一个结构化的 {action, dialogue}。
5.5 组装 SSML,送进数字人
def build_ssml(action, dialogue):
return f"""<speak>
<ue4event>
<type>ka</type>
<data><action_semantic>{action}</action_semantic></data>
</ue4event>
{dialogue}
</speak>"""
# 主循环
user_input = "你好,给我介绍一下这款产品"
action, dialogue = agent_respond(user_input)
ssml = build_ssml(action, dialogue)
# 通过 WebSocket/SSE 把 ssml 推给前端
# 前端调用:sdk.speak(ssml, true, true)
跑起来的效果是这样的:用户问"给我介绍下这款产品"——LLM 选择 PointingRight 动作 + 说"来,看这里,我给您介绍一下",数字人手势一指、语调一扬、内容一接,完整的具身表达。
5.6 为什么这是"类 MCP"的设计?
MCP 的核心哲学是:把 Agent 能调用的一切(API、数据库、文件系统、第三方服务)抽象成统一的 tools 协议。LLM 看到一个工具目录,自己决定什么时候调、怎么调。
我们这节做的事本质上是一样的——把星云的动作库暴露成 LLM 的 tools,让 LLM 决定"说什么的时候做什么动作"。
如果进一步把这个能力封装成 MCP Server,那任何支持 MCP 的 Agent(Claude Desktop、Cursor、VS Code、自建 LangGraph Agent)都能天然具身化——它们原本只会打字,接入后马上多一个会说会做的 3D 表达端。
这是个巨大的想象空间。具身输出,也可以是一个标准的 MCP 工具类型。
一句话:星云 KA 接口 + MCP = 具身输出即服务。
六、Widget 系统:把 Agent 的工具输出可视化
到这里还差最后一块拼图——Agent 调完工具,结果怎么给用户看?
举个场景:用户问"查一下明天上海的天气",Agent 调 MCP 的 weather 工具,拿到一张 JSON 数据。如果只是让数字人念一遍,体验是 60 分;如果数字人边说边在画面里弹出一张天气卡片,体验是 95 分。
星云的 Widget 系统就是解决这个的。
6.1 Widget 事件一览
SDK 内置支持的 widget 事件:
事件
作用
数据字段
subtitle_on / subtitle_off
字幕
{ text }
widget_pic
显示图片
{ image } (注意字段是 image 不是 url)
widget_video
播放视频
{ src, action: "play"/"stop" }
widget_slideshow
PPT 展示
{ url, page }
widget_text
文本卡片
{ text }
这些事件从服务端通过参数流里的 event 通道下发——记得上面说的 ttsa 四路参数流吗?Widget 就是 event 通道的具体应用。
6.2 接管字幕样式 / 展示天气卡片
用 proxyWidget 局部接管事件,既保留 SDK 默认的视频/图片渲染,又能自定义字幕样式:
const sdk = new XmovAvatar({
containerId: "#sdk",
appId: "...",
appSecret: "...",
gatewayServer: "...",
proxyWidget: {
// 自定义字幕:替换 SDK 默认的黑底白字
subtitle_on: (data) => {
const el = document.getElementById("subtitle-text");
el.textContent = data.text;
el.classList.add("show");
},
subtitle_off: () => {
document.getElementById("subtitle-text").classList.remove("show");
},
// 业务层自定义:天气卡片、工具结果卡片
widget_pic: (data) => {
showToolResultCard(data.image); // 渲染 Agent 工具调用结果
},
},
onStateChange: (s) => console.log("数字人状态:", s),
});
避坑提醒:proxyWidget 是"局部覆盖"——没配的事件走 SDK 默认行为。但如果配了 onWidgetEvent 回调(哪怕只打一行日志),SDK 会认为你要完全接管 UI,默认背景、默认字幕会全失效。看到背景突然消失,先检查是不是意外配了 onWidgetEvent。
6.3 端到端:Agent 返回工具结果 → Widget 展示
我在 Demo 里把这条链路跑通了:
用户问:"帮我查下这款产品的参数"
→ Agent 调用产品数据库工具(MCP)
→ 拿到 {图片 URL, 参数描述}
→ Agent 生成 SSML,里面嵌入 widget_pic 事件和文本
→ 数字人先做"展示"手势 → 图片弹出 → 同步解说
SSML 片段长这样(服务端 TTSA 会把 image_url 转为 widget_pic 事件):
<speak>
<ue4event>
<type>ka</type>
<data><action_semantic>PresentSomething</action_semantic></data>
</ue4event>
来,您看这款产品,参数我都给您展示出来了。
</speak>
一段 SSML,一次请求,触发:动作 + 语音 + 图片展示三通道同步。
七、真实体验、延时数据、场景落地
7.1 我的实测延时
把工作台调试面板打开,发送一条 Welcome KA 意图的 SSML:
-
speak 指令首帧耗时:1314 ms
-
状态切换耗时:715 ms
-
连接稳定:3.5 分钟无断线
对比参考:
-
MuseTalk 类开源方案首包:2-5 秒
-
扩散模型类(EMO/Vasa):分钟级
-
商用 API 数字人(某些主流厂商):2-4 秒
星云的 1.3 秒,目前在我实测过的方案里是领先的。这个数字还没算上可以通过 onNetworkInfo 回调拿到的实时 rtt,可以做网络感知动态降级。
7.2 别嫌弃老设备:存量屏升级才是大生意
文档里我特别注意到两件事:
一、JS SDK 全面覆盖主流终端
-
iOS:16.6 - 18.6 全测(含 iPhone、iPad)
-
Android:小米 HyperOS、Oppo、Vivo
-
PC:macOS 15.6、Win10(Dell / 华为 / 华硕)
这意味着几乎所有装了现代浏览器的设备,都能直接跑。
二、Android SDK 适配国产嵌入式 SoC
-
RK3588 → 1080P 可用
-
RK3566 → 720P 可用
这是一个极重要的信号。RK3588 / RK3566 是教培一体机、连锁门店大屏、政务终端、工业显示屏的核心 SoC——全国几千万台存量屏幕的硬件底座。星云这一层往下铺,目标很明确:让存量屏不换硬件,通过升级"安装一个 SDK"变成 AI 服务终端。
7.3 场景速写:四条可立即落地的商业路径
按"存量屏 + 星云 SDK"这个公式推演,我能看到的立刻能跑通的场景:
① 教培一体机 → 24h AI 辅导老师 线下教培机构有大量教师机、班级一体机,原本只能播课件。接入星云,晚间自习无老师值班时段,一键切换到"AI 陪伴老师",答疑、讲题、打气,全部具身交互。
② 零售门店大屏 → 3D 智能导购 传统门店大屏只能循环播广告。星云接入后变成会主动搭话、按需介绍、边说边展示的虚拟导购。对接后端的 CRM / 商品库,每个用户看到的介绍都是个性化的。
③ 政务服务终端 → 具身接待员 政务大厅的自助查询机常年面对不会用的老年人。具身数字人可以主动询问需求、引导办事流程、用方言交互——降低数字鸿沟的最直接姿势。
④ 机器人上肢/头显表达 这个更远,但方向清晰。人形机器人、服务机器人的"上肢表达"层,完全可以复用星云的参数流协议——机器人的胸口屏、面板屏挂一个具身数字人,机器人的"脸"就是数字人。
7.4 成本细节:被我忽略但实际很重要的设计
调研时发现两个"看似小但实则非常省钱"的设计:
-
离线模式(offlineMode):长时间无人互动时进入,不消耗积分,画面上依然显示一个循环的待机动画。商用部署 24 小时开机,这个设计省下的成本相当可观。
-
基础音色 vs Pro 音色:30+ 音色里"基础"档免费,"Pro"档消耗更多积分但音质更好。开发调试阶段全程用基础档,上线后再切 Pro,开发期积分可以省一个数量级。
八、总结:给 AI 装身体,交互直接升级
不用复杂配置、不用高配设备,10 分钟就能让大模型拥有 3D 身体:
-
✅ 文字变语音 + 表情 + 手势,交互有温度
-
✅ 低延迟可打断,贴合真实对话
-
✅ 适配大屏、终端、机器人等多场景
大模型拼完智商,该拼「表达力」了!
互动聊聊
你给大模型做过具身交互吗?用的数字人方案踩过哪些坑?评论区聊聊你的开发经历~
魔珐星云体验入口:xingyun3d.com/?utm_campai…