魔珐星云 + DeepSeek:我给女儿搭了个会辅导作业的 3D 数字人老师
一个父亲的烦恼
我家女儿今年上小学三年级,数学成绩一直不太稳定。我和老婆工作都忙,晚上回到家经常已经八九点了,辅导作业这事儿就变得特别拧巴——孩子等着你教,你已经累得不想说话了。

试过不少办法。买过学习机,孩子用两天就拿来刷视频了;下过AI辅导App,文字聊天那种,孩子看两眼就没兴趣了;请过大学生家教,一周来两次,但平时有问题没人答。
后来我想,孩子为什么对AI辅导App没兴趣?因为那个App没有"人味儿"。一个冰冷的对话框,连个表情都没有,小孩子天然没有代入感。

如果AI能有一个3D的、会说话的、能做出表情和动作的"身体"呢?
这个念头一出来,我就停不下来了。花了一个周末的时间,我用魔珐星云具身驱动SDK + DeepSeek大模型,给女儿搭了一个具身交互智能体家庭教师。女儿第一次看到"她"的时候,眼睛都亮了——那种反应,是任何文字聊天App都给不了的。
今天把整个过程写下来,给同样有辅导焦虑的家长们一个参考。
---
技术选型的思考过程
说实话,一开始我并不是直接选的魔珐星云。我调研了好几个方案:
方案一:Unity + 对口型插件。我之前有Unity基础,理论上可以用Unity做一个3D角色,再接一个TTS做对口型。但实际操作下来发现,口型同步效果很假,动作完全是预设的,而且整个项目太重了,不适合快速验证。
方案二:2D数字人方案。市面上有一些2D数字人,用一张照片生成。效果还行但总觉得不够"真实",而且2D的局限太大,没有空间感和互动感。
方案三:魔珐星云。在一个技术群里看到有人推荐,说是端侧渲染、不需要GPU、SDK几行代码就能跑通。我半信半疑地试了一下,结果——真香。
选魔珐星云的核心原因有三个:
第一,基于KA 语义动作库实现 AI 驱动动作生成,可以根据输出语义实时生成匹配手势,讲到加法会做出比划手势,夸奖时做出竖大拇指的动作,语义和动作联动,自然度远超传统预制动画方案。
第二,端侧渲染。我家里的旧笔记本就能跑,不需要花钱租GPU服务器。对于一个个人项目来说,这点至关重要。
第三,接入足够简单。一个JS文件引入,几行配置代码,前端小白也能上手。
大模型方面选了DeepSeek,主要考虑是它的中文理解和推理能力很强,辅导数学需要一定的逻辑推理能力,DeepSeek在这块表现不错,而且API价格便宜,我主要用的是DeepSeek-V4-Pro 正式版,Agent能力提升了很多。
---
从零开始:我的搭建全过程
第一步:注册和配置
魔珐星云的注册流程比较简单,官网(https://xingyun3d.com)注册账号后进入控制台。
我随后进入具身交互应用配置页面,平台提供了非常多的人设资源,我花了不少时间挑选,能看出平台在具身交互智能体人设方面做了大量积累。
给女儿用的家庭教师,形象很重要。太严肃了孩子有距离感,太活泼了又不像老师。最后选了一个扎马尾辫、穿粉色衬衫的年轻女性形象,看起来亲切又有知性。
音色选了一个活泼的故事姐姐音色,语速调到了0.9——比正常稍快一点点,因为小孩子注意力容易分散,语速太慢她会走神。

表演风格选了"活泼亲切",动作幅度稍微大一些,这样更能吸引孩子的注意力。
第二步:设计教师角色的System Prompt
这一步我觉得是整个项目里最关键的环节。大模型的能力决定了回答质量,但System Prompt决定了大模型"以什么身份、用什么方式"回答。
我反复改了好几版,最终定稿如下:
const systemPrompt = {
role: "system",
content: `你是一位温柔有耐心的家庭教师,名叫"小知姐姐"。你正在辅导一名小学三年级的学生。
教学原则:
1. 不要直接给出答案!要用启发式提问引导孩子自己思考
2. 如果孩子做错了,先肯定TA的思考过程,再温柔地指出问题
3. 用生活中的例子来解释抽象概念,比如用分苹果解释除法
4. 语言要简单易懂,避免使用超过小学三年级水平的词汇
5. 每次回复不超过4句话,保持孩子的注意力
6. 适当使用鼓励性语言,如"你真棒"、"再想想看"
7. 如果孩子表示累了或者不想学了,要理解并适当鼓励休息
特别注意:
- 孩子可能输入错别字或表达不清楚,你要耐心理解TA的意思
- 不要一次性讲太多知识点,一次只解决一个问题
- 如果问题超出小学三年级范围,告诉孩子"这个问题有点超纲了,可以问问老师哦"`
};

这个Prompt的灵魂在于**"不直接给答案"**。我看过太多AI辅导工具都是直接甩答案,孩子抄完就完事了,根本没学到东西。小知姐姐会用提问的方式引导孩子自己推导,这才是真正的辅导。
第三步:核心代码实现
整个项目的代码结构我设计成了这样的交互流程:
孩子输入问题 → 数字人"思考" → DeepSeek生成启发式回复 → 数字人"说话"+做动作
关键代码拆解如下。
数字人初始化:
const sdk = new XmovAvatar({
containerId: "#avatar-container",
appId: "YOUR_APP_ID",
appSecret: "YOUR_APP_SECRET",
gatewayServer: "https://nebula-agent.xingyun3d.com/user/v1/ttsa/session",
onStateChange(state) {
// 状态变化时更新UI
// idle → 待机, listen → 倾听, think → 思考, speak → 说话
updateTeacherStatus(state);
}
});
await sdk.init({
onDownloadProgress(progress) {
// 显示加载进度
showProgress(progress);
}
});
这里有个细节:我在onStateChange回调里做了UI状态同步。当数字人进入think状态时,界面上会显示"小知姐姐正在思考..."的提示,让孩子知道AI正在处理她的问题,而不是卡住了。
DeepSeek对话接口:
async function askTeacher(question) {
// 数字人进入思考状态
sdk.think();
messages.push({ role: "user", content: question });
const response = await fetch("https://api.deepseek.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${DEEPSEEK_API_KEY}`
},
body: JSON.stringify({
model: "deepseek-chat",
messages: messages,
temperature: 0.8, // 稍微高一点的temperature让回答更有创意
max_tokens: 300 // 控制回复长度,避免太长孩子听不进去
})
});
const data = await response.json();
const reply = data.choices[0].message.content;
messages.push({ role: "assistant", content: reply });
return reply;
}
temperature设成了0.8,比健康顾问场景高一点。因为教育场景需要一些创意——用不同的例子来解释同一个概念,让回答不那么千篇一律。
驱动数字人说话:
async function handleUserInput(text) {
// 1. 界面显示用户消息
appendMessage('user', text);
// 2. 调用DeepSeek获取回复
const reply = await askTeacher(text);
// 3. 界面显示AI回复
appendMessage('ai', reply);
// 4. 驱动数字人说出回复
sdk.speak(reply, true, true);
}
整体逻辑就是这样的线性流程。魔珐星云SDK的API设计得很直觉,think() → speak() 就完成了从思考到说话的自然过渡。
第四步:完整HTML代码
下面是完整的可运行代码,替换App ID、App Secret和DeepSeek API Key后保存为.html文件,双击即可在浏览器中打开:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小知姐姐 · AI家庭教师</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #fff3e0 0%, #fce4ec 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
.header {
width: 100%;
background: rgba(255,255,255,0.9);
padding: 14px 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
text-align: center;
}
.header h1 {
color: #e91e63;
font-size: 22px;
}
.header span { font-size: 14px; }
.header .subtitle { color: #888; font-size: 13px; margin-top: 4px; }
.main {
display: flex;
gap: 16px;
width: 100%;
max-width: 1100px;
padding: 16px;
flex: 1;
}
.avatar-box {
flex: 1;
background: #fff;
border-radius: 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
overflow: hidden;
position: relative;
min-height: 520px;
}
#avatar-container { width: 100%; height: 100%; }
.loading-mask {
position: absolute;
inset: 0;
background: rgba(255,255,255,0.96);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
}
.loading-mask.hide { display: none; }
.spinner {
width: 50px; height: 50px;
border: 5px solid #ffe0b2;
border-top-color: #ff9800;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-tip { margin-top: 16px; color: #e91e63; font-size: 14px; font-weight: 500; }
.status-tag {
position: absolute;
top: 12px; right: 12px;
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
z-index: 5;
transition: all 0.3s;
}
.status-tag.idle { background: #e8f5e9; color: #2e7d32; }
.status-tag.think { background: #fff3e0; color: #e65100; }
.status-tag.speak { background: #e3f2fd; color: #1565c0; }
.status-tag.listen { background: #f3e5f5; color: #6a1b9a; }
.chat-box {
width: 360px;
background: #fff;
border-radius: 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.06);
display: flex;
flex-direction: column;
}
.chat-title {
padding: 14px 18px;
border-bottom: 1px solid #f0f0f0;
font-weight: 600;
color: #e91e63;
font-size: 15px;
}
.chat-list {
flex: 1;
overflow-y: auto;
padding: 14px;
max-height: 420px;
}
.msg {
margin-bottom: 10px;
max-width: 88%;
padding: 10px 14px;
border-radius: 14px;
font-size: 14px;
line-height: 1.6;
}
.msg.kid {
background: #fff8e1;
margin-left: auto;
text-align: right;
border: 1px solid #ffe082;
}
.msg.teacher {
background: #fce4ec;
border-left: 3px solid #e91e63;
}
.msg .name {
font-size: 11px;
color: #999;
margin-bottom: 3px;
}
.input-bar {
padding: 10px 14px;
border-top: 1px solid #f0f0f0;
display: flex;
gap: 8px;
}
.input-bar input {
flex: 1;
padding: 10px 14px;
border: 2px solid #ffcdd2;
border-radius: 12px;
font-size: 14px;
outline: none;
transition: border-color 0.3s;
}
.input-bar input:focus { border-color: #e91e63; }
.input-bar button {
padding: 10px 18px;
background: #e91e63;
color: #fff;
border: none;
border-radius: 12px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
}
.input-bar button:hover { background: #c2185b; transform: scale(1.05); }
.input-bar button:disabled { background: #ccc; cursor: not-allowed; transform: none; }
.topics {
padding: 8px 14px;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.topics button {
padding: 5px 12px;
background: #fff3e0;
border: 1px solid #ffcc80;
border-radius: 16px;
font-size: 12px;
color: #e65100;
cursor: pointer;
transition: all 0.2s;
}
.topics button:hover { background: #ffe0b2; }
.emoji-decor { font-size: 20px; }
</style>
</head>
<body>
<div class="header">
<h1><span class="emoji-decor">📚</span> 小知姐姐 · AI家庭教师</h1>
<div class="subtitle">魔珐星云 × DeepSeek | 陪你一起学,越学越快乐</div>
</div>
<div class="main">
<div class="avatar-box">
<div class="status-tag idle" id="status-tag">准备中...</div>
<div id="avatar-container"></div>
<div class="loading-mask" id="loading-mask">
<div class="spinner"></div>
<div class="loading-tip" id="loading-tip">小知姐姐正在准备中...</div>
</div>
</div>
<div class="chat-box">
<div class="chat-title">💬 和小知姐姐聊天</div>
<div class="chat-list" id="chat-list">
<div class="msg teacher">
<div class="name">小知姐姐</div>
嗨~我是小知姐姐!今天有什么不会的题吗?或者有什么想学的呀?一起开始吧!
</div>
</div>
<div class="topics">
<button onclick="quickAsk('24 × 5 = ?')">乘法计算</button>
<button onclick="quickAsk('什么是分数?')">什么是分数?</button>
<button onclick="quickAsk('一道应用题不会做')">应用题不会</button>
<button onclick="quickAsk('我不想做作业了')">不想写作业</button>
</div>
<div class="input-bar">
<input type="text" id="kid-input" placeholder="输入你的问题..."
onkeypress="if(event.key==='Enter') sendToTeacher()">
<button id="ask-btn" onclick="sendToTeacher()">问姐姐</button>
</div>
</div>
</div>
<script src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar@latest.js"></script>
<script>
// ========== 配置 ==========
const CONFIG = {
APP_ID: "YOUR_APP_ID",
APP_SECRET: "YOUR_APP_SECRET",
DEEPSEEK_API_KEY: "YOUR_DEEPSEEK_API_KEY",
GATEWAY: "https://nebula-agent.xingyun3d.com/user/v1/ttsa/session"
};
const SYSTEM_PROMPT = {
role: "system",
content: `你是一位温柔有耐心的家庭教师,名叫"小知姐姐"。你正在辅导一名小学三年级的学生。
教学原则:
1. 不要直接给出答案!要用启发式提问引导孩子自己思考
2. 如果孩子做错了,先肯定TA的思考过程,再温柔地指出问题
3. 用生活中的例子来解释抽象概念,比如用分苹果解释除法
4. 语言要简单易懂,避免使用超过小学三年级水平的词汇
5. 每次回复不超过4句话,保持孩子的注意力
6. 适当使用鼓励性语言,如"你真棒"、"再想想看"
7. 如果孩子表示累了或者不想学了,要理解并适当鼓励休息
特别注意:
- 孩子可能输入错别字或表达不清楚,你要耐心理解TA的意思
- 不要一次性讲太多知识点,一次只解决一个问题
- 如果问题超出小学三年级范围,告诉孩子"这个问题有点超纲了,可以问问老师哦"`
};
// ========== 状态 ==========
let sdk = null;
let chatHistory = [SYSTEM_PROMPT];
let isThinking = false;
// ========== 初始化 ==========
async function initTeacher() {
try {
sdk = new XmovAvatar({
containerId: "#avatar-container",
appId: CONFIG.APP_ID,
appSecret: CONFIG.APP_SECRET,
gatewayServer: CONFIG.GATEWAY,
onStateChange(state) {
updateStatus(state);
}
});
await sdk.init({
onDownloadProgress(p) {
document.getElementById('loading-tip').textContent =
`小知姐姐正在准备中... ${p}%`;
}
});
document.getElementById('loading-mask').classList.add('hide');
document.getElementById('status-tag').textContent = '待机中';
// 打招呼
const hi = "嗨~我是小知姐姐!今天有什么不会的题吗?或者有什么想学的呀?一起开始吧!";
appendMsg('teacher', hi);
sdk.speak(hi, true, true);
} catch(e) {
console.error('初始化失败:', e);
document.getElementById('loading-tip').textContent =
'加载失败啦,请检查配置是否正确~';
}
}
// ========== 状态更新 ==========
function updateStatus(state) {
const tag = document.getElementById('status-tag');
const map = {
'idle': { text: '待机中', cls: 'idle' },
'listen': { text: '倾听中', cls: 'listen' },
'think': { text: '思考中', cls: 'think' },
'speak': { text: '讲解中', cls: 'speak' }
};
const info = map[state] || { text: state, cls: 'idle' };
tag.textContent = info.text;
tag.className = `status-tag ${info.cls}`;
}
// ========== DeepSeek对话 ==========
async function askDeepSeek(question) {
chatHistory.push({ role: "user", content: question });
const res = await fetch("https://api.deepseek.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${CONFIG.DEEPSEEK_API_KEY}`
},
body: JSON.stringify({
model: "deepseek-chat",
messages: chatHistory,
temperature: 0.8,
max_tokens: 300
})
});
const data = await res.json();
const reply = data.choices[0].message.content;
chatHistory.push({ role: "assistant", content: reply });
return reply;
}
// ========== 发送消息 ==========
async function sendToTeacher() {
const input = document.getElementById('kid-input');
const text = input.value.trim();
if (!text || isThinking) return;
isThinking = true;
document.getElementById('ask-btn').disabled = true;
input.value = '';
appendMsg('kid', text);
sdk.think();
try {
const reply = await askDeepSeek(text);
appendMsg('teacher', reply);
sdk.speak(reply, true, true);
} catch(e) {
console.error('请求出错:', e);
appendMsg('teacher', '哎呀,小知姐姐刚才走神了,再问一次好不好?');
sdk.idle();
} finally {
isThinking = false;
document.getElementById('ask-btn').disabled = false;
}
}
// ========== 快捷提问 ==========
function quickAsk(text) {
document.getElementById('kid-input').value = text;
sendToTeacher();
}
// ========== 追加消息 ==========
function appendMsg(role, content) {
const list = document.getElementById('chat-list');
const div = document.createElement('div');
div.className = `msg ${role}`;
const name = role === 'kid' ? '我' : '小知姐姐';
div.innerHTML = `<div class="name">${name}</div>${content}`;
list.appendChild(div);
list.scrollTop = list.scrollHeight;
}
// ========== 启动 ==========
window.addEventListener('load', initTeacher);
</script>
</body>
</html>
---
开发中踩的坑
整个过程虽然顺利,但也踩了几个坑,记录一下给后来者避雷。
坑一:System Prompt太宽泛
第一版Prompt我写的是"你是一位老师",结果DeepSeek经常用大学教授的口吻回答,满屏专业术语,三年级孩子根本看不懂。后来加了"小学三年级"的明确约束,又加了"用生活中的例子解释抽象概念"这条规则,效果才好起来。
教训:System Prompt一定要明确场景和受众,越具体越好。
坑二:max_tokens设太大了
一开始我设了1000,DeepSeek经常回复一大段话。数字人念半天念不完,孩子早就不听了。后来调到300,控制在4句话以内,刚刚好。
教训:数字人说话和文字聊天不一样,太长了用户根本听不下去,一定要控制回复长度。
坑三:忘记管理对话历史
最初我每次请求都重新构造messages数组,导致小知姐姐没有"记忆"——孩子刚才问的乘法,转头问除法她就忘了前面的上下文。后来改成全局chatHistory数组,每次请求都带上完整历史,这才实现了连贯的多轮对话。
但这里也有个隐患:对话历史太长会导致Token消耗增加。我的做法是超过20轮就截断前面的历史,只保留System Prompt和最近10轮对话。
坑四:localhost限制
魔珐星云SDK在本地调试时,需要通过http://localhost或http://127.0.0.1访问。如果你直接双击打开HTML文件(file://协议),SDK可能无法正常工作。解决方案是用VS Code的Live Server插件,或者用python -m http.server起一个本地服务器。
---
女儿的反应
搭好之后我喊女儿过来看。她坐到电脑前,看到一个扎马尾辫的大姐姐出现在屏幕上,对她微笑着说"嗨~我是小知姐姐"的时候,她先是愣了一下,然后特别兴奋地喊:"爸爸她好像真的在跟我说话!"

然后她就迫不及待地开始问问题了。让我意外的是,她问的第一个问题不是数学题,而是"小知姐姐,你知道有什么好玩的吗"——小知姐姐回了一句"我们先学完今天的知识,然后我再告诉你好玩的事好不好?"把孩子引导回学习上了,这个表现比我预期的好太多。
后来她做数学作业遇到不会的题,会主动去问小知姐姐。小知姐姐不会直接告诉她答案,而是问"你觉得第一步应该先算什么呢?"——引导她自己推导。说实话,这种启发式教学的方式,比我这个急性子家长强多了。
用了两周下来,孩子的数学成绩提高了8分。当然不全是数字人的功劳,但它确实让孩子从"害怕做数学"变成了"愿意做数学",这个心态转变才是最重要的。
---
一些反思
这个项目让我对"具身交互智能"有了更深的理解。
之前我觉得3D数字人真正的智能在大模型那里。但实际用下来我发现,"身体"本身就是智能的一部分。一个会微笑、会做手势的数字人老师,和一个纯文字对话框,对孩子注意力的影响是完全不同的。女儿会盯着小知姐姐的脸看,会在小知姐姐竖大拇指的时候开心地笑——这种情感连接,是纯文字永远给不了的。
魔珐星云的端侧渲染也让我很满意。家里的旧笔记本跑起来毫无压力,不需要额外的硬件投入。对于一个家庭项目来说,这点太重要了。端到端约 500ms 响应在实际体验中确实感受明显——数字人不会出现"卡一下再开口"的停顿感,整个交互非常流畅。
如果你也是家长,或者对教育类AI感兴趣,真的建议试试。不需要编程基础,照着上面的代码改改配置就能跑。给孩子一个会说话的AI老师,也许就能改变TA对学习的态度。

技术的意义不在于多炫酷,而在于能不能让生活变得更好一点点。至少在我家,这个数字人老师做到了。
相关资源:
更多推荐




所有评论(0)