手把手教你用魔珐星云搭一个会说话的3D AI老师,DeepSeek做大脑,Node.js保安全
手把手教你用魔珐星云搭一个会说话的3D AI老师,DeepSeek做大脑,Node.js保安全
当所有Agent都在卷"脑子",我开始思考——如果AI有一副身体,交互会变成什么样,
## 当Agent拥有了"身体"
最近半年,AI圈最热闹的事是各家都在做Agent——能自己思考、自己调用工具、自己干活的AI。市面上也出了不少相关产品,但用了一圈下来,我总觉得差点意思。

差点什么?差一个"人味儿",总是冷冰冰的一个对话框,让人没有安全感,总之很官方。
你看现在大部分Agent的交互方式:你在对话框里打字,它回你一段文字,最多配张图。整个过程像在跟一个客服窗口聊天——它能回答你的问题,但你感受不到对面坐着一个"人"。

而我们人类真实的沟通是什么样的?是面对面的——你会看到对方的表情、听到他说话的语气、注意他讲到重点时的手势。这些"非语言信息"其实占了沟通量的60%以上。
这就是为什么数字人这两年突然又火起来了。从企业展厅里的虚拟讲解员,到直播间里的AI主播,再到银行大堂的智能客服——几乎所有"有屏"的地方,都开始出现数字人的身影。原因不复杂:当AI能"长出一张脸、能开口说话、能做表情动作"的时候,它跟用户之间的距离就一下子被拉近了。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
所以当我接触到魔珐星云这个平台的时候,第一反应是——这个方向对了。它给AI Agent提供了一副3D的"身体",不仅能思考,还能像真人一样自然地表达和交互。这篇文章就是我这次实战的记录:踩过哪些坑、怎么搭起来、跑起来什么效果,以及一些个人感受。希望能给同样在探索Agent交互形态的同行一点参考,希望也能和大家多多交流。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
一个很优秀的点:有很多形象可以我们自己选择。
## 一、为什么是数字人?
说白了,文字Agent的天花板不是算法,是"人"。人天然更喜欢跟"人"打交道,而不是跟"对话框"打交道。
数字人把 AI 拟人化之后,至少带来三个变化:
- 注意力更集中:有人盯着你说话,不容易走神
- 信息密度更高:表情、手势、语气都是信息通道
- 情感连接更强:用户会更愿意信任一个"看着像人"的 AI
这就是为什么 2025 年开始,几乎所有带屏的产品(手机助手、智能音箱、车机、柜台大屏)都在加数字人能力。
## 二、实战:搭建一个多场景具身AI老师
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
### 2.1 场景设计
做了一个"多场景具身AI老师":左侧 3D 数字人,右侧场景卡片,可切换数学/物理/英语/编程/语文 5 个学科,每个学科有 4 个快捷问题,一键对话。
需求:
1. 场景化:用户进来就能选,不用裸聊天框
2. 快捷交互:每场景预置 4 个典型问题
3. 多角色扮演:后端动态切换 system prompt
4. 密钥安全:appId/appSecret/LLM_KEY 全部存服务端 `.env`
5. 流式输出:DeepSeek 一边生成,数字人一边说
### 2.2 架构设计
┌─────────── Node.js 代理层 (:8080) ────────────┐
│ │
│ .env 文件 → dotenv 加载密钥 │
│ ├─ GET / → 字符串替换渲染 HTML │
│ ├─ POST /api/chat → DeepSeek SSE 代理转发 │
│ └─ GET /api/health → 配置校验 │
│ │
└──────┬──────────────────────────┬───────────┘
│ 模板注入 appId/appSecret │ 转发 LLM 请求
▼ ▼
┌────────────── 前端 ───────────┐ ┌─── 外部服务 ───┐
│ 加载星云SDK (<script> CDN) │ │ 数字人服务 │
│ 渲染场景卡片 (5个学科) │ │ TTS+动画参数流 │
│ SDK直连云服务 (WebSocket) │ ├─────────────────┤
│ fetch('/api/chat') 走代理 │ │ DeepSeek API │
│ 状态机驱动 (speak/listen等) │ │ 流式推理 (SSE) │
└───────────────────────────────┘ └─────────────────┘
### 2.3 项目结构
xingyun-demo/
├── .env # ← 所有密钥存这里
├── .env.example # 配置模板
├── server.js # Node.js 代理服务器
├── templates/
│ └── index.html # 前端页面(占位符模板)
├── package.json
└── node_modules/
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在这里的我将示例代码提供给大家,大家也可以根据官方Skill以及Trea.ai去生成。
Gitee代码地址:https://gitee.com/xiaotietou/xingyun-08-01.git,大家可以去下载立马就能体验哦,代码都是完整的。
最近官方刚发布了小程序,大家可以在 微信->搜索小程序-> 魔珐星云 就可以预览咯。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
### 2.4 极简Demo代码
> 这是 Node.js 代理架构的 Demo。`appId`、`appSecret`、LLM API Key 全部存在 `.env`,由服务端注入到 HTML,前端源码零明文。
这里大家可一定要注意,千万不要明文暴露密钥,带来更大的隐患
#### 第一步:`server.js` — Node.js 代理层
const express = require('express');
const cors = require('cors');
const path = require('path');
const fs = require('fs');
const https = require('https');
require('dotenv').config();
const XINGYUN_APP_ID = process.env.XINGYUN_APP_ID || '';
const XINGYUN_APP_SECRET = process.env.XINGYUN_APP_SECRET || '';
const LLM_API_KEY = process.env.LLM_API_KEY || '';
const LLM_API_BASE = process.env.LLM_API_BASE || 'https://api.deepseek.com/v1/chat/completions';
const LLM_MODEL = process.env.LLM_MODEL || 'deepseek-chat';
const PORT = parseInt(process.env.PORT || '8080', 10);
function checkConfig() {
const missing = [];
if (!XINGYUN_APP_ID || XINGYUN_APP_ID.startsWith('your_')) missing.push('XINGYUN_APP_ID');
if (!XINGYUN_APP_SECRET || XINGYUN_APP_SECRET.startsWith('your_')) missing.push('XINGYUN_APP_SECRET');
if (!LLM_API_KEY || LLM_API_KEY.startsWith('your_')) missing.push('LLM_API_KEY');
return missing;
}
const app = express();
app.use(cors());
app.use(express.json());
// 首页:模板注入凭证
app.get('/', (req, res) => {
let html = fs.readFileSync(path.join(__dirname, 'templates', 'index.html'), 'utf-8');
const missing = checkConfig();
const llmOk = !missing.includes('LLM_API_KEY');
html = html.replace(/__APP_ID__/g, XINGYUN_APP_ID);
html = html.replace(/__APP_SECRET__/g, XINGYUN_APP_SECRET);
html = html.replace(/__GATEWAY_SERVER__/g, process.env.XINGYUN_GATEWAY_SERVER || '');
html = html.replace(/__LLM_CONFIGURED__/g, llmOk ? 'true' : 'false');
html = html.replace('__MISSING_CONFIG_JSON__', JSON.stringify(missing));
res.type('html').send(html);
});
// 5 个学科的 system prompt
const ROLE_PROMPTS = {
math: '你是一位亲切耐心的中学数学老师。简洁易懂,回答200字以内。',
physics: '你是一位风趣的物理老师。用生活例子解释,回答200字以内。',
english: '你是一位英语外教。中英双语教学,回答200字以内。',
coding: '你是一位编程导师。通俗讲解代码逻辑,回答200字以内。',
literature: '你是一位语文老师。旁征博引讲解古诗词,回答200字以内。',
default: '你是一位AI助手。回答200字以内。',
};
// LLM 代理:SSE 流式透传
app.post('/api/chat', (req, res) => {
if (!LLM_API_KEY || LLM_API_KEY.startsWith('your_')) {
return res.status(503).json({ error: 'LLM_API_KEY 未配置' });
}
const { message, role = 'default' } = req.body || {};
if (!message) return res.status(400).json({ error: 'message 为必填' });
const messages = [
{ role: 'system', content: ROLE_PROMPTS[role] || ROLE_PROMPTS.default },
{ role: 'user', content: message },
];
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' });
const url = new URL(LLM_API_BASE);
const proxyReq = https.request({
hostname: url.hostname, port: 443, path: url.pathname, method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${LLM_API_KEY}` },
timeout: 60000,
}, proxyRes => {
proxyRes.on('data', chunk => res.write(chunk));
proxyRes.on('end', () => res.end());
proxyRes.on('error', err => { res.write(`data: ${JSON.stringify({error: err.message})}\n\n`); res.end(); });
});
proxyReq.on('error', err => { res.write(`data: ${JSON.stringify({error: err.message})}\n\n`); res.end(); });
proxyReq.write(JSON.stringify({ model: LLM_MODEL, messages, stream: true }));
proxyReq.end();
});
app.listen(PORT, () => console.log(`http://localhost:${PORT}`));
#### 第二步:`templates/index.html` — 关键片段
<!-- 凭证由服务端注入 -->
<script>
var APP_ID = "__APP_ID__";
var APP_SECRET = "__APP_SECRET__";
var LLM_CONFIGURED = __LLM_CONFIGURED__;
</script>
<script src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar@latest.js"></script>
创建数字人:
sdk = new XmovAvatar({
containerId: '#avatar-container',
appId: APP_ID, appSecret: APP_SECRET, gatewayServer: GATEWAY_SVR,
onStateChange: s => console.log('[SDK]', s),
onVoiceStateChange: s => { if (s === 'end') sdk.interactiveidle(); },
onMessage: m => { if (m.code >= 40000) addMessage('system', 'SDK错误: ' + m.message); }
});
await sdk.init({ onDownloadProgress: p => setStatus('loading', '加载 ' + p + '%') });
await sdk.idle();

LLM 调用走代理 + 数字人说话:
我们这里配置的是DeepSeek-V4-Pro,大家也可以对接其他优秀的AI公司接口。
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: question, role: currentRole })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let full = '', buf = '', isFirst = true;
const msgDiv = addMessage('assistant', '');
while (true) {
const { done, value } = await reader.read();
if (done) break;
for (const line of decoder.decode(value, { stream: true }).split('\n')) {
if (!line.startsWith('data:')) continue;
const data = line.slice(5).trim();
if (data === '[DONE]') continue;
try {
const c = JSON.parse(data).choices?.[0]?.delta?.content;
if (!c) continue;
full += c; buf += c; msgDiv.textContent = full;
// 首个 token 或累积 30 字/遇到标点就驱动说话
if (isFirst || buf.length >= 30 || /[。!?;\n]/.test(buf)) {
await sdk.speak(buf, isFirst, false);
isFirst = false; buf = '';
}
} catch (e) {}
}
}
if (buf) await sdk.speak(buf, false, true);

#### 第三步:`.env`
XINGYUN_APP_ID=你的真实AppID
XINGYUN_APP_SECRET=你的真实AppSecret
LLM_API_KEY=sk-你的真实Key
LLM_API_BASE=https://api.deepseek.com/v1/chat/completions
LLM_MODEL=deepseek-chat
PORT=8080
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
这里我们单独拆分的文件去集中管理密钥,
针对于XINGYUN_APP_ID,XINGYUN_APP_SECRET我们去https://xingyun3d.com/ 官网,创建应用


流程很简单,如果大家觉得体验很好,前期测试可以点击福利中心先使用我的邀请码进行1000积分领取,足够大家进行测试。
邀请码:JMWAGFUI8Z

#### 启动
npm install
npx nodemon server.js
打开 http://localhost:8080,选场景 → 连接数字人 → 对话。nodemon 会监听 templates/.env/server.js,改任何文件自动重启。
### 4.5 代码解析:9个关键设计点
#### (1)状态机驱动的交互节奏
星云 SDK 核心是 5 状态流转:
idle(待机) → listen(倾听) → think(思考) → speak(说话) → interactiveidle(互动待机)
不是简单切动画,是 SDK 内部统一管理的连贯行为。idle 自然呼吸、listen 前倾、think 皱眉、speak 配合手势。onVoiceStateChange('end') 时调用 interactiveidle() 让数字人回到互动待机态,准备下一轮。
#### (2)Node.js 代理架构:密钥安全的核心
之前的文章版本把 appId/appSecret/LLM Key 全硬编码在前端 JS 中——右键查看源代码就能拿走。
新架构:
- 密钥分层:`.env` 存机密,`.env.example` 提交 Git,`.env` 加入 `.gitignore`
- 服务端注入:服务端读 `.env` → `APP_ID` 字符串替换 → 返回真实 HTML
- LLM 代理:前端不直连 DeepSeek,调用 `/api/chat`,Node.js 转发时附加 `Authorization: Bearer {LLM_API_KEY}`
- 配置校验:`checkConfig()` 启动时检查,未配置页面顶部显示黄色警告
> 安全边界:星云 SDK 是客户端 JS 库,`appId`/`appSecret` 仍会在浏览器内存中存在。生产环境建议服务端生成临时 Session Token(需星云配合),或把数字人渲染放在受控设备上。
#### (3)多场景角色切换
后端 ROLE_PROMPTS 映射表 + 前端 currentRole 状态:

// 后端:5 个学科 → 5 套人设
const ROLE_PROMPTS = { math: '...', physics: '...', english: '...', ... };
// 前端:调用时把 role 一起传过去
fetch('/api/chat', { body: JSON.stringify({ message, role: currentRole }) });

新增场景只需加一行 prompt + 4 个问题,零侵入。
#### (4)流式驱动:LLM 和数字人的节奏同步
链路:DeepSeek → SSE → Node.js /api/chat → SSE 透传 → 浏览器 fetch → 分块 sdk.speak()
Node.js 代理对 SSE 零修改透传,不增加延迟。前端策略:
-
第一个 token 到达 →
speak(text, true, false)立即开始驱动 -
后续累积 30 字或遇到
。!?;\n标点 →speak(chunk, false, false)追加 -
LLM 生成完 →
speak('', false, true)标记结束
"说话"和"生成"并行,用户几乎无感。
#### (5)生命周期管理:别忘了 destroy()
页面关闭不调 sdk.destroy(),WebSocket 不释放,连续刷新几次就触发并发限制(错误码 10005)。beforeunload 里必须调用:
window.addEventListener('beforeunload', () => {
if (sdk) { try { sdk.destroy(); } catch (e) {} }
});
#### (6)UI 状态机:`setStatus()` 与 `addMessage()`
把"数字人连接状态"和"对话气泡"做成统一的小函数,避免 DOM 操作散落各处:
function setStatus(state, text) {
$('status-dot').className = 'status-dot ' + state; // online/loading/error
$('status-text').textContent = text;
}
function addMessage(role, text) {
const div = document.createElement('div');
div.className = 'message ' + role; // user/assistant/system
div.textContent = text;
$('messages').appendChild(div);
$('messages').scrollTop = $('messages').scrollHeight;
return div;
}
这样在 connect()、disconnect()、sendMessage() 任何地方都能用一行代码更新 UI,不容易出错。
#### (7)错误码分级处理
星云 SDK 错误码分段很清晰:10000-19999 是用户态(密钥错、参数错),40000+ 是 SDK 内部错误。`onMessage` 回调里我做了简单分级:
onMessage: m => {
if (m.code >= 40000) addMessage('system', 'SDK错误: ' + m.message);
// 10000 段错误一般是密钥或网关问题,让用户去查 .env
}
不要把所有错误都静默吞掉——用户看到"连接失败"却不知道为啥,体验直接崩。
#### (8)场景卡片的动态渲染
切换场景时,快捷问题列表要跟着变。用一个 map 维护所有场景的问题,切换时清空容器重新渲染:
function switchRole(role) {
currentRole = role;
// 更新左侧标签的 active 态
document.querySelectorAll('.scene-tag').forEach(t => {
t.classList.toggle('active', t.dataset.role === role);
});
renderQuickQuestions(); // 重新渲染快捷问题
}
function renderQuickQuestions() {
const box = $('quick-questions');
box.innerHTML = '';
(QUICK_QUESTIONS[currentRole] || []).forEach(q => {
const b = document.createElement('button');
b.className = 'quick-btn';
b.textContent = q;
b.onclick = () => { $('user-input').value = q; sendMessage(); };
box.appendChild(b);
});
}
这样"切换学科=改数据",不用动 HTML 结构。
#### (9)nodemon 自动重启 + 路径配置
开发期用 npx nodemon --watch templates --watch .env --watch server.js server.js,改任何文件自动重启,不用手动 Ctrl+C。
坑点提醒:H 盘如果有写入权限问题(Windows 系统盘保护),建议把项目复制到 C 盘临时目录运行,源码仍放在 H 盘维护。
### 4.6 从单场景到多场景的产品化思路
早期 Demo 只有"数学老师",对话框空空如也。改成多场景后体验完全不同:
- 打开页面 → 5 个学科卡片(数学/物理/英语/编程/语文)整齐排列

- 点"数学老师" → 快捷问题变成「勾股定理证明」「一元二次方程」等
- 点"连接数字人" → 数字人加载后待机,呼吸/眨眼自然循环
- 点任意快捷问题 → 数字人开始说话,DeepSeek 流式文字逐字显示,口型表情实时跟随
- 切换场景 → 数字人保持连接,system prompt 立即切换,再次提问人设就变了(选英语会用中英双语回答)
"开箱即用"的设计是把 Demo 变成可交付产品的关键。
## 三、SDK 与落地要点
### SDK 集成
<script src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar@latest.js"></script>
| API | 用途 |
|---|---|
new XmovAvatar(config) | 创建实例 |
sdk.init(options) | 初始化(下载资源 + 建 WebSocket) |
sdk.speak(ssml, isStart, isEnd) | 驱动说话,支持流式和 SSML 动作 |
sdk.listen() / sdk.think() / sdk.idle() | 状态切换 |
sdk.interactiveidle() | 互动待机(用于打断) |
sdk.destroy() | 释放资源,必须在 beforeunload 调用 |
### 落地要点
1. 协议要求:本地 `localhost:8080` 自动满足 SDK 要求,生产环境需 HTTPS
2. 密钥安全:`.env` 存机密,`.env.example` 提交 Git,`.env` 加入 `.gitignore`
3. 开发效率:`nodemon` 监听 `templates/.env/server.js`,改任何文件自动重启
4. 场景扩展:在 `ROLE_PROMPTS` 加一行 + `QUICK_QUESTIONS` 加 4 个问题即可
## 四、总结
参数流+端侧渲染的架构是范式转换,不是优化。当数字人能在 500ms 内实时回应、表情自然变化、手势跟内容配合,"具身交互智能"和"文字聊天"的体验鸿沟就出现了——这是交互形态的质变。
星云 SDK 对开发者很友好:核心 API 七八个,状态机语义清晰,配合 AI Coding 工具和官方 Skill 文件,从零到可运行 Demo 可以压缩到 30 分钟,大家去使用我的GiteeAI教育示例,运行就可以预览,会更节省大家的调研时间。
教育只是其中一个场景。金融客服、医疗咨询、文旅讲解、智能座舱——任何一块屏幕,都可能因为具身交互智能数字人而升级为一个 AI 具身交互智能体。这不是遥远的未来,是现在就能落地的能力
最后特别想提一下国产化闭环:魔珐星云 + DeepSeek/Qwen,从 LLM 推理到语音合成到 3D 具身表达,全部国产技术栈走通,在各种场景下意义重大。
用一个词概括我的整体体验:具身****交互智能。AI 不只是"能回答问题",更能"像一个真实的存在一样和你对话"。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
相关资源:
更多推荐





所有评论(0)