给个人知识库装了一个会说话的数字人前台
一、你有没有遇到过这种情况?
上周我在找一个半年前记的笔记——关于微服务拆分的几个要点,当时写在 Notion 里了。打开搜索,翻了七八个页面,最后在一篇会议纪要的角落找到了。
说实话,挺崩溃的。
我电脑里攒了三年多的笔记、文档、书签、PDF 论文。每次想找一个东西,不是在 Finder 里搜文件名,就是在各种笔记软件里翻目录。更难受的是,这些东西之间明明有联系——那篇论文的观点刚好能佐证上周的项目决策、两个月前记的一个灵感跟现在的工作高度相关——但你就是想不起来,更别说关联起来了。
前段时间我在想:有没有可能,我把这些乱七八糟的知识"喂"给一个 AI,然后像问同事一样直接问它?
更进一步——能不能不只返回纯文本回答,而是拥有可视化的**具身交互智能体,**像真实知识顾问一样和我面对面沟通?
答案是可以。花了一个周末,我用魔珐星云 SDK + 本地 RAG 搭了一个原型。这篇文章就记录整个过程。
二、AI 的交互形态正在变,只是大多数人还没意识到
在说技术方案之前,我想先说一个趋势判断。
过去两年,大部分人对 AI 的使用停留在"聊天框思维"——打开 ChatGPT、Kimi、豆包,打字提问,看文字回答。这个模式当然有用,但它的天花板也很明显:
你获取的是信息,不是交互。
在真实场景里,人对信息的接收远不止文字。语气、表情、肢体动作——这些构成了面对面交流中 70% 以上的信息量。当一个 AI 只能用纯文本回答你时,它本质上还是一个搜索引擎,不是你的"伙伴"。
这就是"具身交互智能"要解决的问题——让 AI 从文本框里走出来,拥有身体、表情和声音,以更像"人"的方式与你交互。
大模型解决了 AI 的"思考"问题。但要让 AI 真正融入你的桌面、你的工作流、你的日常生活,它还需要一个能被看见、能说话、能实时回应的身体。
三、魔珐星云:让大模型"长"出身体
我在做这个项目之前,其实调研了一圈市面上的方案。
市面上不少传统虚拟载体产品大多属于视频生成模式:输入文案,云端渲染生成单向播放的念稿视频。少数可以做语音驱动,但延迟高达数秒,不支持中途打断。
而本项目需要的并不是单向念稿的虚拟形象,而是一套支持实时双向对话的知识顾问交互体系:支持随时插话打断、跟随上下文调整语气、多终端设备流畅运行。
后来我找到了 (魔珐星云)
它做的事情其实很聚焦——专门补齐大模型在终端落地时缺少的"交互层"。 用一句话概括:
大模型负责"大脑",Agent 负责"调度",星云负责"身体和表达"——让 AI 以 3D 具身的形态,自然地与人交互。
更具体来说,魔珐星云将多模态感知、智能体调度、依托 LAM3D 大模型 + KA 语义动作库的AI 端侧解算 + 参数流表达引擎封装为 SDK,实现一站式接入。开发者不用单独处理语音合成、口型对齐、表情手势同步等复杂底层模块,只需要关注大模型输出业务文本内容;星云负责把文本转化成语音、表情、语义手势协同的整套多模态交互输出。
这个定位在当下很关键。因为大模型的能力已经到了一个临界点——模型够聪明了,但"怎么让人舒服地跟它交互"这个问题,模型自己解决不了。
四、动手:搭建一个个人知识库数字人顾问
好了,理论聊完,下面进入正题——我具体做了什么,以及怎么做。
4.1 整体思路
架构其实不复杂:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 知识库文档 │ → │ RAG 检索引擎 │ → │ 豆包 │
│ (本地文件) │ │ (向量搜索) │ │ 大模型生成 │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
┌──────────────┐ │
│ 魔珐星云 SDK │ ←──────────┘
│ 3D 数字人 │
│ 语音+表情+动作 │
└──────────────┘
流程:
-
提前把你的笔记、文档做向量化处理,存入本地向量数据库
-
用户提问时,先从向量库检索最相关的内容片段
-
把检索结果作为上下文,连同用户问题一起发给 DeepSeek 大模型
-
大模型生成的回答,通过星云 SDK 驱动 3D 数字人"说"出来
4.2 准备工作
要用到的工具和凭证:
| 组件 | 用途 | 获取方式 |
|---|---|---|
| 魔珐星云 SDK | 3D 数字人驱动 | 登录 xingyun3d.com → 应用中心创建应用,拿到 appId 和 appSecret |
| DeepSeek API | 大模型回答生成 | DeepSeek 开放平台申请 API Key |
| ChromaDB | 本地向量数据库 | pip install chromadb 即可 |
| text2vec | 中文文本向量化 | pip install text2vec-base-chinese,离线可用 |
| Python + Node.js | 后端 + 前端 | Python 3.10+ / Node.js 20+ |
我的本地环境:
-
系统:macOS 15(Windows 同样适用,代码跨平台)
-
编辑器:VS Code + Cursor
-
Python 做 RAG 后端,Node.js 做前端页面
4.3 第一步:搭建知识库 RAG 后端
先把散落各处的文档"喂"给向量数据库。我自己的知识库内容大概有:
-
Notion 导出的技术笔记(约 200 篇,Markdown 格式)
-
项目文档和复盘记录(约 50 个 PDF)
-
手动记录的技术文章(手动导出了 30 多篇)
第一步是把这些内容向量化:
# rag_server.py
import os
import chromadb
from chromadb.config import Settings
import hashlib
# ===== 文档加载器:支持 Markdown、PDF、TXT =====
def load_documents(doc_dir):
"""遍历目录,加载所有支持的文档"""
docs = []
for root, _, files in os.walk(doc_dir):
for f in files:
path = os.path.join(root, f)
ext = os.path.splitext(f)[1].lower()
if ext == '.md' or ext == '.txt':
with open(path, 'r', encoding='utf-8') as fp:
docs.append({
'content': fp.read(),
'source': f,
'path': path,
})
elif ext == '.pdf':
# 使用 PyPDF2 提取文本
try:
from PyPDF2 import PdfReader
reader = PdfReader(path)
text = '\n'.join([p.extract_text() or '' for p in reader.pages])
docs.append({
'content': text,
'source': f,
'path': path,
})
except Exception as e:
print(f"⚠️ 跳过 {f}: {e}")
print(f"📄 共加载 {len(docs)} 篇文档")
return docs
# ===== 文本切分:按语义段落切分 =====
def split_text(text, chunk_size=500, overlap=100):
"""简单的中文段落切分,保证每个 chunk 语义完整"""
# 优先按自然段落切分
paragraphs = text.split('\n\n')
chunks = []
current = ''
for p in paragraphs:
p = p.strip()
if not p:
continue
if len(current) + len(p) < chunk_size:
current += p + '\n'
else:
if current:
chunks.append(current.strip())
current = p + '\n'
if current:
chunks.append(current.strip())
print(f"✂️ 切分为 {len(chunks)} 个文本块")
return chunks

然后是向量化和存储:
# 接上面的代码...
from sentence_transformers import SentenceTransformer
import numpy as np
# ===== 向量化引擎 =====
# text2vec-base-chinese: 中文专用模型,本地运行,不需要联网
embedder = SentenceTransformer('shibing624/text2vec-base-chinese')
def embed_texts(texts):
"""批量向量化"""
return embedder.encode(texts, normalize_embeddings=True)
# ===== ChromaDB 初始化 =====
client = chromadb.PersistentClient(path="./knowledge_db")
collection = client.get_or_create_collection(
name="my_knowledge",
metadata={"hnsw:space": "cosine"} # 余弦相似度
)
# ===== 主流程:建库 =====
def build_knowledge_base(doc_dir):
docs = load_documents(doc_dir)
all_chunks = []
all_metadatas = []
for doc in docs:
chunks = split_text(doc['content'])
all_chunks.extend(chunks)
all_metadatas.extend([{
'source': doc['source'],
'path': doc['path'],
}] * len(chunks))
# 生成向量
print("🧠 正在向量化...")
embeddings = embed_texts(all_chunks)
# 生成唯一 ID
ids = [hashlib.md5(c.encode()).hexdigest()[:16] for c in all_chunks]
# 写入数据库
collection.add(
ids=ids,
documents=all_chunks,
metadatas=all_metadatas,
embeddings=embeddings.tolist(),
)
print(f"✅ 知识库构建完成!共 {len(all_chunks)} 条记录")
# 建库(只需要跑一次)
if __name__ == '__main__':
build_knowledge_base('./my_docs')
接下来是检索 API:
# rag_search.py — 提供 FastAPI 检索接口
from fastapi import FastAPI
from pydantic import BaseModel
import chromadb
from sentence_transformers import SentenceTransformer
import requests
app = FastAPI()
# 复用模型和数据库
embedder = SentenceTransformer('shibing624/text2vec-base-chinese')
client = chromadb.PersistentClient(path="./knowledge_db")
collection = client.get_collection("my_knowledge")
DEEPSEEK_KEY = "sk-your-key"
class QueryRequest(BaseModel):
question: str
top_k: int = 5
@app.post("/api/ask")
async def ask(req: QueryRequest):
# 1. 将用户问题向量化
q_embedding = embedder.encode(
[req.question], normalize_embeddings=True
).tolist()
# 2. 从知识库检索最相关内容
results = collection.query(
query_embeddings=q_embedding,
n_results=req.top_k,
)
# 3. 拼接检索结果作为上下文
context_parts = []
for doc, meta in zip(
results['documents'][0],
results['metadatas'][0]
):
context_parts.append(
f"[来源: {meta['source']}]\n{doc}"
)
context = '\n\n---\n\n'.join(context_parts)
# 4. 调用 DeepSeek 大模型,生成回答
system_prompt = f"""你是一个个人知识库 AI 助手。
请根据以下知识库内容回答用户问题。如果知识库中没有相关信息,请诚实告知。
回答要口语化、简洁,100-200 字以内。
知识库内容:
{context}"""
response = requests.post(
'https://api.deepseek.com/v1/chat/completions',
headers={
'Authorization': f'Bearer {DEEPSEEK_KEY}',
'Content-Type': 'application/json',
},
json={
'model': 'deepseek-chat',
'messages': [
{'role': 'system', 'content': system_prompt},
{'role': 'user', 'content': req.question},
],
'temperature': 0.5,
'max_tokens': 400,
},
)
data = response.json()
answer = data['choices'][0]['message']['content']
# 5. 返回回答 + 引用来源
sources = list(set(
m['source'] for m in results['metadatas'][0]
))
return {
'answer': answer,
'sources': sources,
}
if __name__ == '__main__':
import uvicorn
uvicorn.run(app, host='0.0.0.0', port=8765)
4.4 第二步:接入魔珐星云数字人
RAG 后端跑起来之后,下一步就是把文字回答变成"有人对你说话"的体验。
前端部分直接复用星云的 JS SDK,核心就三件事:
① 初始化数字人
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>智脑AI助手</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #0f0f0f;
color: #e0e0e0;
display: flex;
height: 100vh;
overflow: hidden;
}
.avatar-side {
flex: 1;
background: #1a1a2e;
position: relative;
}
.avatar-side #sdk-container {
width: 100%;
height: 100%;
}
.chat-side {
width: 420px;
display: flex;
flex-direction: column;
border-left: 1px solid #2a2a3e;
background: #14141f;
}
.chat-header {
padding: 20px;
border-bottom: 1px solid #2a2a3e;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-header h2 { font-size: 16px; font-weight: 600; }
.status-badge {
font-size: 12px;
padding: 4px 12px;
border-radius: 20px;
background: #1a3a1a;
color: #4ade80;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
max-width: 85%;
padding: 12px 16px;
border-radius: 12px;
font-size: 14px;
line-height: 1.7;
}
.message.user {
align-self: flex-end;
background: #2563eb;
color: white;
border-bottom-right-radius: 4px;
}
.message.assistant {
align-self: flex-start;
background: #1e1e30;
border: 1px solid #2a2a3e;
border-bottom-left-radius: 4px;
}
.message .source-tag {
display: inline-block;
font-size: 11px;
margin-top: 8px;
padding: 2px 8px;
border-radius: 4px;
background: #2a2a4e;
color: #94a3b8;
}
.chat-input-area {
padding: 16px 20px;
border-top: 1px solid #2a2a3e;
display: flex;
gap: 8px;
}
.chat-input-area textarea {
flex: 1;
background: #1a1a2e;
border: 1px solid #2a2a3e;
border-radius: 8px;
color: #e0e0e0;
padding: 10px 14px;
font-size: 14px;
resize: none;
outline: none;
font-family: inherit;
}
.chat-input-area textarea:focus {
border-color: #2563eb;
}
.chat-input-area button {
padding: 10px 20px;
background: #2563eb;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
}
.chat-input-area button:hover { background: #1d4ed8; }
.chat-input-area button:disabled {
background: #374151;
cursor: not-allowed;
}
</style>
</head>
<body>
<div class="avatar-side">
<div id="sdk-container"></div>
</div>
<div class="chat-side">
<div class="chat-header">
<h2>🧠 个人知识库顾问</h2>
<span class="status-badge" id="status-text">初始化中</span>
</div>
<div class="chat-messages" id="chat-messages"></div>
<div class="chat-input-area">
<textarea id="user-input"
placeholder="问问你的知识库...比如'微服务拆分的关键原则'"
rows="2"></textarea>
<button id="send-btn">发送</button>
</div>
</div>
<script src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar@latest.js"></script>
<script src="app.js"></script>
</body>
</html>
② 核心交互逻辑
// app.js
const CONFIG = {
appId: 'YOUR_APP_ID',
appSecret: 'YOUR_APP_SECRET',
gatewayServer: 'https://nebula-agent.xingyun3d.com/user/v1/ttsa/session',
ragApiUrl: 'http://localhost:8765/api/ask',
};
let avatar = null;
let isReady = false;
let isSpeaking = false;
// ===== 初始化数字人 =====
avatar = new XmovAvatar({
containerId: '#sdk-container',
appId: CONFIG.appId,
appSecret: CONFIG.appSecret,
gatewayServer: CONFIG.gatewayServer,
hardwareAcceleration: 'prefer-hardware',
onStateChange(state) {
isSpeaking = (state === 'speak');
updateStatus(isSpeaking ? '回答中...' : '就绪');
// 说话结束后自动回到待机
if (!isSpeaking) {
setTimeout(() => avatar?.interactiveidle(), 600);
}
},
onMessage(msg) {
if (msg?.type === 'error') {
console.error('[SDK Error]', msg);
updateStatus('异常');
}
},
enableLogger: false,
});
avatar.initModel = 'normal';
avatar.init((progress) => {
if (progress >= 100) {
isReady = true;
updateStatus('就绪');
avatar.interactiveidle();
}
});
// ===== 知识库问答 =====
async function askKnowledge(question) {
if (!question.trim() || !isReady) return;
appendMessage('user', question);
document.getElementById('user-input').value = '';
// 如果正在说话,打断
if (isSpeaking) {
avatar.interactiveidle();
await sleep(200);
}
updateStatus('检索中...');
try {
// 1. 调用 RAG 检索 + 大模型生成
const res = await fetch(CONFIG.ragApiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question }),
});
const data = await res.json();
// 2. 显示回答 + 引用来源
let displayText = data.answer;
if (data.sources?.length) {
displayText += `\n\n📎 参考:${data.sources.join('、')}`;
}
appendMessage('assistant', displayText);
// 3. 驱动数字人说话(只说回答部分,不说引用)
updateStatus('回答中...');
avatar.speak(data.answer, true, true);
} catch (err) {
console.error(err);
appendMessage('assistant', '抱歉,检索时出了点问题,请重试。');
avatar.idle();
updateStatus('就绪');
}
}
// ===== UI 工具 =====
function appendMessage(role, text) {
const container = document.getElementById('chat-messages');
const div = document.createElement('div');
div.className = `message ${role}`;
div.innerHTML = text
.replace(/\n/g, '<br>')
.replace(/📎 参考:(.+)/, '<span class="source-tag">📎 $1</span>');
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function updateStatus(text) {
document.getElementById('status-text').textContent = text;
}
function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
// ===== 事件绑定 =====
document.getElementById('send-btn').addEventListener('click', () => {
const input = document.getElementById('user-input');
askKnowledge(input.value);
});
document.getElementById('user-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
askKnowledge(e.target.value);
}
});
4.5 第三步:跑起来
启动命令很简单,开三个终端窗口:
# 终端1:启RAG检索服务
cd rag-backend
python rag_search.py
# → Uvicorn running on http://0.0.0.0:8765
# 终端2:前端静态服务(可以用任意方式,我用的 npx serve)
cd frontend
npx serve .
# → Serving on http://localhost:3000
# 终端3(可选):首次建库
cd rag-backend
python rag_server.py # 只需要跑一次
然后浏览器打开 http://localhost:3000,等数字人加载完(大概 3-5 秒),就可以开始问了。

五、实测效果:问几个真问题试试
搭完之后,我拿自己的知识库做了一轮测试。下面是真实的问答记录:
测试 :具体知识检索
我问:“我的技术栈是什么?”
数字人停顿了大约 0.8 秒,开口回答:

感受:这个回答跟我当初记的内容吻合,而且它把我的两篇笔记关联起来了——一篇是讲微服务的,一篇是讲 DDD 的。这是纯关键词搜索做不到的。

六、开发者视角:几个真实感受
6.1 RAG + 具身交互 = 1+1 > 2
做这个 Demo 之前,我觉得 RAG 的交互就是"搜一下、返回一段文字"。但加上数字人之后,体验完全不一样了。
具体来说,变化在于:
- 等待被"服务"的感觉:纯文字回答的时候,你等着文本一行行吐出来,感觉是在等一个工具的输出。但数字人开口说话的时候,更像是一个同事在回答你
- 回答质量感知提升:同一个回答内容,用语音 + 表情 + 口型同步的方式呈现,比纯文字看起来更可信。这可能是心理学层面的——我们对"人说出来"的信息天然更信任
- 自然的多轮衔接:因为数字人有"正在听"和"正在说"的状态切换,你潜意识里会被引导成对话模式,而不是搜索引擎模式
6.2 SDK 集成比想象中顺滑
魔珐星云的 JS SDK 大概是我用过的最简单的多媒体 SDK 之一了。真正干活的方法就三四个:
-
new XmovAvatar({...})— 创建实例 -
.init(callback)— 初始化 -
.speak(text, isStart, isEnd)— 让数字人说话 -
.interactiveidle()— 进入待机/打断
对于一个不需要深究 3D 渲染、音频编解码的前端开发者来说,这个门槛非常低。我算了一下,从开始写第一行代码到数字人在页面上说出第一句话,大概花了 20 分钟。
6.3 延迟表现
从用户按下发送键到数字人开口,整体链路延迟:
| 环节 | 耗时 |
|---|---|
| 文本向量化 + ChromaDB 检索 | ~50-100ms |
| DeepSeek API 生成回答 | ~600-1500ms |
| 星云 TTS + 口型驱动 + 渲染 | ~300-500ms |
| 端到端总计 | ~1-2 秒 |
星云自己的链路(TTS → 驱动 → 渲染)稳定在 500ms 以内,瓶颈主要在大模型生成。如果在本地跑一个小模型(比如 Qwen2.5-7B 量化版),总延迟能压缩到 800ms 以内。
这背后是星云的AI 端渲和端侧解算技术在起作用——画面渲染在本地设备上完成,不需要等云端回传视频流。再加上参数流架构,整个交互链路极短。
七、还可以怎么玩?
这个 Demo 只是一个起点。基于同样的技术栈,其实可以做很多有意思的东西:
进阶玩法 1:接入你的浏览器书签
写一个 Chrome 插件,自动抓取你收藏的网页内容做向量化。以后你想找一个"上次看过的关于 React 性能优化的文章",直接问数字人就行。
进阶玩法 2:工作日报生成器
把每天的 Slack/飞书消息、Git 提交记录、会议纪要都喂给知识库。下班前跟数字人说一句"帮我总结今天干了什么",它给你生成日报。
进阶玩法 3:代码库问答
把你的项目代码做 Code Embedding。遇到不懂的业务逻辑,不用翻代码,直接问数字人"这个订单状态机是怎么流转的?"
这些场景的共同点是:你每天都会遇到,搭建门槛不高,但体验提升巨大。
八、写在最后
回头看我做这个项目的初衷——“不想再翻几百篇笔记找一个信息”——确实被解决掉了。但过程中更大的收获是:我意识到AI 的交互形态在悄悄发生变化。
大模型已经足够聪明了。DeepSeek、Qwen、GLM,这些模型的理解和生成能力对于日常使用来说绰绰有余。接下来的竞争点不在于"谁的模型更聪明",而在于"谁的交互体验更像人"。
具身交互这件事,不是锦上添花的花活。它解决的是一个根本问题:当 AI 从"工具"变成"伙伴",人跟 AI 之间需要的不再是命令行和文本框,而是自然的、有温度的多模态交互。
如果你的工作或项目里也有类似的场景——需要让 AI 从对话框里走出来,以更自然的方式服务用户——不妨去魔珐星云看看
技术栈:
-
大模型:DeepSeek-V3
-
向量模型:text2vec-base-chinese(本地离线)
-
AI Coding 工具:Cursor
-
具身交互平台:魔珐星云 JS SDK
-
运行环境: Python 3.11 / Node.js 20
更多推荐


所有评论(0)