一、你有没有遇到过这种情况?

上周我在找一个半年前记的笔记——关于微服务拆分的几个要点,当时写在 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 数字人    │
                    │  语音+表情+动作 │
                    └──────────────┘

流程

  1. 提前把你的笔记、文档做向量化处理,存入本地向量数据库

  2. 用户提问时,先从向量库检索最相关的内容片段

  3. 把检索结果作为上下文,连同用户问题一起发给 DeepSeek 大模型

  4. 大模型生成的回答,通过星云 SDK 驱动 3D 数字人"说"出来

4.2 准备工作

要用到的工具和凭证

组件 用途 获取方式
魔珐星云 SDK 3D 数字人驱动 登录 xingyun3d.com → 应用中心创建应用,拿到 appIdappSecret
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

Logo

纵情码海钱塘涌,杭州开发者创新动! 属于杭州的开发者社区!致力于为杭州地区的开发者提供学习、合作和成长的机会;同时也为企业交流招聘提供舞台!

更多推荐