端到端 SDK 实战,三段代码做一个会听、会说、能被打断的网页具身交互智能体


一、ASR + LLM + TTS 拼接方案的三个坑

老板一句话,官网能不能有个"真人感"的接待

上个月开周会,老板指着官网首页说,每天进来的人不少,看两眼就走了,能不能放个前台在这儿,有人来就打个招呼,问什么答什么。

我当场答应了。脑子里的方案很快就有了,语音识别把话转成文字,大模型想答案,语音合成念出来,再找个形象对上口型。三样都有现成的云服务,接口文档也齐,我给自己留了三天。

三天后,我拼出了一个"会说话的木头人"

三天后它能用了。你问一句,它停一两秒,开口回答。

看着别扭的地方有三处。

现象原因
每次开口前都愣一下ASR 转完才交给 LLM,LLM 写完才交给 TTS,三段等待叠在一起
嘴在动,脸是僵的口型跟着音频走,表情和动作另外配,两边互相不知道
语气一直是一个调TTS 只拿到一段文字,分不出这句是在道歉还是在介绍产品

客户插了句嘴,它还在自顾自地念

给客户演示那天出了岔子。它在介绍公司的产品线,讲得挺长,客户听到一半问了一句,你们支持私有化部署吗。

它没停,把整段介绍念完才去处理那句问题。

我回去翻代码,问题出在整条流程里根本没有"被打断"这回事。TTS 在播的时候,ASR 那头就算收到了声音,也没有地方通知播放停下来。我试着让麦克风一直开着,结果它把自己喇叭里的声音也当成了用户在说话。

后来我推倒重做,做完以后它就待在官网页面的一侧,访客边看网页边跟它说话,下面这段官方演示和我想要的样子几乎一样。

在这里插入图片描述


二、端到端具身交互智能,从串行流程到持续交互循环

每一环都在等上一环

我原来的方案画出来是一条直线。

用户说话 → ASR → LLM → TTS → 屏幕播放

ASR、LLM、TTS 单拿出来都很成熟,我用的几家识别准、回答好、声音也自然。毛病出在它们排队干活,后一环必须等前一环交卷,谁也看不见别人当下在做什么。

人和人聊天的时候,这几件事是一起进行的。

真人聊天是怎么做到边说边听的

想一下你跟公司前台说话。她说着话,耳朵一直开着,你一开口她就停下来听。她的语气、表情和手势跟那句话一起出来,没有先说完再补动作。你皱一下眉,她可能马上换个说法。

听、想、说、动同时在跑,还会跟着你的反应改。

把整段交互当成一个循环

后来读魔珐星云的资料,发现他们讲的端到端就是在做这件事。官方对星云的定义是这样一句话。

魔珐星云是一套端到端具身交互智能平台,让 AI 通过屏幕和机器人进入真实世界,成为能够感知、理解、表达并行动的智能体。

星云围绕一次完整的人机交互去设计整个系统。识别、大脑、语音这些模块都在,它们共享同一套会话状态和时间线,由同一个运行时调度,简单拼接给不了这一点。官方把这套运转方式叫 Continuous Interaction Loop,也就是持续交互循环。

在这里插入图片描述

持续感知 → 持续理解 → 持续决策 → 持续表达与行动 → 持续反馈 → 回到感知

用官方的大白话讲,AI 一边和你交流,一边继续听、继续看、继续判断接下来该怎么回应。

撑起这个循环的是五项能力,放到我的 AI 前台里,各管一摊。

在这里插入图片描述

能力在 AI 前台里管什么
多模态感知听清访客的话,分出哪句是访客、哪句是自己的回声,判断是不是有效插话
专属智脑知道自己是这家公司的前台,知道公司的产品和接待规则
多模态表达语音、口型、表情、眼神、手势一起生成
AI 端渲形象在浏览器本地渲染,不用云端推视频流
数字与物理行动在屏幕上把答案说出来、演出来,以后进了机器人还能转身带路

把五项串起来的是 Real-time Interaction Runtime,事件流转、会话状态、音画同步、打断和断线重连都归它管。我那条直线缺的就是它。


三、三段代码跑通端到端流程,init、ask() 和 startASR()

重做的时候我只给自己定了一条,不再写胶水代码。

先拿钥匙

  1. 去[魔珐星云官网]注册,注册时填兑换码 JSUAY3IS96。
  2. 进控制台创建应用,配好形象、身份和知识。官方的接入顺序是创建应用、无代码效果预览、SDK 接入三步,第二步不用写代码就能先跟它聊几句看效果。
  3. 记下 appId 和 appSecret。

第一段代码,让它站上页面

原生 HTML 用 CDN 引入,文件名里带 _e2e 的才是端到端版本。

<script src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar_e2e@latest.js"></script>

项目用 Vue 或 React 的话走 npm。

npm install @xmov/avatar@latest
import XingyunAvatarAgent from "@xmov/avatar/agent";

然后创建实例,初始化。

const agent = new XingyunAvatarAgent({
  containerId: "#agent-avatar",
  appId: "your-app-id",
  appSecret: "your-app-secret",
  gatewayServer: "https://nebula-agent.xingyun3d.com/user/v1/ttsa_v2/session",
  onMessage(err) { console.error("渲染错误", err.code, err.message); },
  agentCallbacks: {
    onError(err) { console.error("Agent 错误", err.domain, err.code); },
  },
});

try {
  await agent.init({ onDownloadProgress: (p) => console.log(p + "%") });
} catch (e) {
  await agent.destroy("init_failed"); // 初始化失败就销毁,别留半个实例
  throw e;
}

这段有两处我差点写错。

网关地址的路径是 ttsa_v2。旧版渲染 SDK 用的是 ttsa,照着旧教程抄很容易少两个字符,报错信息还不太好认。

错误回调要写两个。顶层的 onMessage 收渲染层的错,agentCallbacks.onError 收识别、大脑、网络和配额的错。只写一个,另一半的报错就没人接。

init() 成功返回,实例就进入了 running 状态,可以开始对话。

第二段代码,一句 ask() 走完听、想、说

以前我要拿到文字、调大模型、拼 SSML、调 TTS、再喂给形象播放。现在只写一行。

agent.ask("你好,请用一句话欢迎来访的客人");

文字交给它的大脑,生成回答,再用语音、口型、表情和动作一起说出来。

在这里插入图片描述

ask() 返回只说明请求发出去了,它说没说完要看回调。

agentCallbacks: {
  onConversationChange({ state }) {
    // 取值 idle | asking | speaking | completed | failed
    if (state === "completed") console.log("这一轮说完了");
  },
},

它怎么知道该回答什么,靠的是星云的专属智脑。

这里我踩过一个坑。第一次跑的时候我没在控制台填身份,就让它介绍一下自己。它报出了底层大模型的名字,接着现编了一家做企业软件的公司,产品线讲得有模有样,可那些产品根本不存在。

所以上线前一定要先把身份配好,XX 公司前台,代表谁,服务谁,再把公司介绍和常见问题传上去。专属智脑管的就是这件事,让它知道自己是谁、掌握哪些知识、该按什么规则说话。已经有自己大模型的团队也可以换上去,这一节最后会讲。

第三段代码,打开麦克风

文字能聊了,接着让它能听。

const agent = new XingyunAvatarAgent({
  // 前面的参数不变
  features: { auto_send_asr_to_llm: true }, // 识别完自动交给大脑
});

micButton.onclick = () => agent.startASR(); // 放在用户点击里
stopButton.onclick = () => agent.stopASR();

startASR() 会申请麦克风权限。浏览器只允许在用户点击这类手势里申请,页面一加载就调用会被拦下,所以要绑在按钮上。

auto_send_asr_to_llm 打开以后,用户一说完,识别结果自动进大脑,回答自动播出来。我原来手写的那一串识别、请求模型、合成语音的代码,到这里整段删掉了。

模块可以换成自己的

写到这里我担心过一件事,全用一家的服务,以后想换怎么办。

翻了资料,星云的组件是可插拔的。

模块可选
ASR魔珐自研大模型 ASR 或小模型 ASR,也可以用火山引擎、腾讯云
大脑魔珐自研大脑,带 RAG、FAQ 库、自研大模型和可视化编排;也能接任意 OpenAI 协议的大模型、客户自建大脑,或 Dify、Coze 上搭好的工作流
TTS魔珐自研,也可以用微软 Azure、字节豆包、MiniMax
机器人运控魔珐运控,或机器人本体厂商自带的运控

想换成自己的大模型,构造参数里加一段 brain_config。

brain_config: {
  provider: "openai-compatible",
  base_url: "YOUR_SERVICE_BASE_URL",
  api_key: "YOUR_API_KEY",
  model: "YOUR_MODEL_NAME",
},

换了模块,打断和状态同步照样能用。官方的解释是各模块之间有统一的接口契约和状态管理,替换以后事件流转、会话状态、时间同步和异常处理都保持一致。

api_key 写在前端只适合本地测试,生产环境要放到平台上配置。


四、全双工打断与 agentCallbacks 状态回调

我故意在它说到一半时开口

Demo 跑通以后,我先把那天的场面重演了一遍。让它介绍产品线,说到一半,我开口问支持私有化部署吗。

处理插话的代码是这样写的。

let speaking = false;

agentCallbacks: {
  onSpeakStateChange({ state }) {
    speaking = state === "speak_start"; // speak_start、speak_end、speak_error
  },
  onASRResult({ text, isFinal }) {
    if (text && speaking) agent.interrupt(); // 我开口时它还在说,先让它停
  },
},

interrupt() 没有参数,返回打断耗时,单位毫秒。它只停播报,麦克风照样开着,所以我后面那句话会被完整听完,再进入新一轮回答。

这回它停下来了。

在这里插入图片描述

上面这段是我实测录下来的。我先让它慢慢介绍公司的产品,说到一半,在输入框里插了一句"等一下,先介绍一下你自己"。Demo 的发送按钮里也写了一句 if (speaking) agent.interrupt(),所以文字插话和语音插话走的是同一个 interrupt()。它停掉了那段长介绍,转去回答我的新问题。

官方文档里还有一种做法,放一个打断按钮,点击时调 agent.interrupt(),接着调 agent.listen() 切到聆听姿态。enableClientInterrupt 默认就是 true,不用另外设置。

它怎么分清是我,还是它自己的回声

麦克风一直开着,喇叭里它自己的声音也会被收进来。我原来的方案就卡在这里。

星云把这件事放在多模态感知里处理。官方资料列出的语音感知能力有算法降噪、AEC 抗回声、本体声音抑制、VAD、ASR、远场语音,以及 Double Talk 和 Barge-in 智能打断。

一次有效打断背后要连着做完几步。

判断真实用户声音 → 去除本体回声 → 区分噪声、咳嗽、附和声与真实插话
→ 检测有效打断 → 停止当前表达 → 持续接收用户完整输入 → 进入新一轮交互

在 SDK 里开回声消除,两个开关都要写上。

features: {
  auto_send_asr_to_llm: true,
  speech_frontend: { enabled: true, enable_aec: true },
},

文档专门提醒过,只写 enable_aec: true 不够,语音前处理的 enabled 也得打开。我第一遍就漏了这个。

官方把这叫持续感知。落到代码里,它说话的时候耳朵也一直开着,还能分出哪句是你说的。

把它的状态打印出来看

调试的时候我把所有回调都打到页面上,持续交互循环怎么转,看日志一目了然。

let reply = "";

agentCallbacks: {
  onAgentStateChange: (s) => log("[Agent]", s),
  onASRStateChange:   (s) => log("[麦克风]", s),
  onASRResult: ({ text, isFinal }) => log(isFinal ? "[说完]" : "[正在说]", text),
  onLLMResponse: (r) => {
    if (r.event === "chunk") reply += r.text || "";        // 回复一段段流过来
    if (r.event === "done") { log("[大脑]", reply); reply = ""; }
  },
  onSpeakStateChange: ({ state }) => log("[播报]", state),
  onConversationChange: ({ state }) => log("[一轮]", state),
  onError: (e) => log("[错误]", e.domain, e.code),
},

onLLMResponse 这里我第一版写错了。我以为 done 事件会带上完整回复,结果实测打出来是空的。大模型的回答是按 chunk 一段段流过来的,要自己拼起来,到 done 再一次性用。

下面是那次插话测试的真实日志,中间省掉了大段回复文字。

[Agent] running
[一轮对话] asking
[播报] speak_start
[一轮对话] speaking          ← 正在长篇介绍产品
[一轮对话] interrupted       ← 我插话了
[一轮对话] asking
[播报] speak_end             ← 旧播报收尾
[一轮对话] completed
[大脑回复] 您好呀……          ← 新问题的回答
[播报] speak_start
[一轮对话] speaking
[播报] speak_end
[一轮对话] completed

顺序很能说明问题。对话状态先变成 interrupted,马上进入新一轮 asking,旧播报的 speak_end 晚一步才到。写业务逻辑时要注意,判断"它还在不在说话"用播报状态,判断"这一轮算不算完"用对话状态,两个别混着用。

在这里插入图片描述

onASRResult 会先给出几条 isFinal: false 的中间结果,最后一条才是 isFinal: true。用户话还没说完,我在中间结果阶段就能决定要不要打断。


五、上线前的三个坑,容器高度、appSecret 和异常兜底

它在页面上隐身了

第一次跑,控制台没报错,资源进度走到了 100%,页面上什么都没有。

原因是容器 div 没给高度,画布跟着缩成了 0。

/* 容器必须有明确宽高 */
#agent-avatar { width: 405px; height: 720px; }

官方的踩坑记录里也写了这一条。同一类的坑还有两个。页面要跑在 HTTPS 或 localhost 上,直接双击打开 html 文件不行。有些编辑器自带的预览窗口禁了网络请求,会渲染失败,换外部浏览器打开就好。

钥匙贴在了门上

Demo 里 appSecret 直接写在前端代码里,本地调试可以,上线不行,任何人按 F12 就能看到。

上线前我会做三件事。大模型、TTS 这些供应商密钥放到星云平台上配置,不写进浏览器代码。页面加访问控制和频率限制,免得被人拿去刷积分。私有化部署的项目,用官方提供的 e2eServer 加 authToken 这组参数,接自己的鉴权。

三道兜底

情况处理
初始化失败catch 里调 agent.destroy("init_failed"),页面提示稍后再试
对话失败onConversationChange 收到 failed 或 onError 报错时,让它说一句刚才没听清能再说一遍吗,然后回到聆听
用户离开页面调 agent.destroy(),只删 DOM 不会释放麦克风
想在页面上显示回复文字在 chunk 事件里自己拼接,done 事件的 text 实测为空

onError 里的 domain 很好用,能直接看出是哪一类问题。permission 是没给麦克风权限,network 是网络,asr、brain、ttsa 分别对应识别、大脑和播报,quota 是积分用完了,sdk 是 SDK 自身。不同类别给不同提示,用户就不会对着一句"出错了"发呆。

完整 Demo 是一个约 150 行的 HTML 文件,放在文末。填上自己的 appId 和 appSecret,用 npx serve 在 localhost 起一下就能跑。


六、一个智能体,多种身体,从网页到大屏和机器人

它现在住在网页里,以后能搬家

做完以后回头看,少写的那些代码倒在其次。我原来以为给 AI 一个身体,就是把大模型的回答搬到屏幕上。现在的理解是,它得一直感知、理解、表达和行动,人在变,它也跟着变。端到端具身交互智能处理的就是这个。

还有一件事我没想到,这个前台不用一直待在网页里。

星云的说法是,同一个智能体,可以拥有不同的身体。身份、知识、记忆、任务和业务能力都能复用,换了身体,能力还会多出来。

在这里插入图片描述

身体多出来的能力
Web、PC、手机、Pad随开随用
AI 屏幕,比如大屏和自助终端摄像头看到有人走近,主动打招呼
移动 AI 屏幕移动、导航、带路
人形机器人转身、靠近、上肢动作

从网页搬到公司前台的一块屏幕上,它就能接待真正走进门的访客。下面这段是官方素材里的办公楼前台,访客说想拜访市场部,它在屏幕上接待、指路。

在这里插入图片描述

再往前一步,就是人形机器人。前台的活还是那些,机器人还能转身、走动、用手势指路。

在这里插入图片描述

形象在终端本地渲染,这是 AI 端渲,不用云端推视频流。官网给的数据是百元级入门芯片就能流畅运行,从提问到回答在 1200ms 以内。公司前台那块现成的屏幕,以后也能直接用上它。官方有一句话概括这件事,智能体持续存在,身体不断升级。

下一篇,把旧项目的胶水代码删掉

手上有用旧版渲染 SDK XmovAvatar 写的项目、自己接了 ASR 和大模型的,下一篇我带着迁移到端到端 SDK,看看能删掉多少行。


附录 完整 Demo

单文件 index.html,原生 HTML 加 CDN,把配置区换成自己的凭证就能跑。

npx serve .      # 任意静态服务器都行,确保是 localhost
# 浏览器打开提示的地址,点"连接"
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>AI 前台 · 魔珐星云端到端 SDK Demo</title>
  <style>
    body { margin: 0; font-family: system-ui, sans-serif; background: #0f1222; color: #e8e8f0; }
    .wrap { display: flex; gap: 24px; padding: 24px; height: 100vh; box-sizing: border-box; }
    /* 关键:容器必须有明确宽高,否则画布会折叠成 0 */
    #agent-avatar { width: 405px; height: 720px; flex: none; border-radius: 12px; background: #1a1e36; }
    .panel { flex: 1; display: flex; flex-direction: column; gap: 12px; min-width: 0; }
    .row { display: flex; gap: 8px; }
    input { flex: 1; padding: 10px; border-radius: 8px; border: 1px solid #3a3f66; background: #151933; color: inherit; }
    button { padding: 10px 14px; border-radius: 8px; border: 0; background: #5b6cff; color: #fff; cursor: pointer; }
    button:disabled { opacity: .4; cursor: not-allowed; }
    #log { flex: 1; overflow: auto; background: #151933; border-radius: 8px; padding: 12px; font: 12px/1.6 monospace; white-space: pre-wrap; }
    #status { font-size: 13px; color: #9aa0c8; }
  </style>
</head>
<body>
  <div class="wrap">
    <div id="agent-avatar"></div>
    <div class="panel">
      <div id="status">未连接</div>
      <div class="row">
        <button id="btnConnect">连接</button>
        <button id="btnMic" disabled>开麦</button>
        <button id="btnStop" disabled>打断</button>
        <button id="btnDestroy" disabled>销毁</button>
      </div>
      <div class="row">
        <input id="txt" placeholder="输入问题,回车发送" disabled />
        <button id="btnAsk" disabled>发送</button>
      </div>
      <div id="log"></div>
    </div>
  </div>

  <!-- 端到端 SDK:注意文件名带 _e2e,全局对象是 XingyunAvatarAgent -->
  <script src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar_e2e@latest.js"></script>
  <script>
    // ====== 配置区:换成你在星云控制台创建应用后拿到的凭证 ======
    // 注意:appSecret 写在前端只适合本地 Demo,上线请看文章第五节
    const APP_ID = "your-app-id";
    const APP_SECRET = "your-app-secret";
    // ===========================================================

    const $ = (id) => document.getElementById(id);
    const log = (...a) => { $("log").textContent += a.join(" ") + "\n"; $("log").scrollTop = 1e9; };
    const setStatus = (s) => ($("status").textContent = s);

    let agent = null;
    let speaking = false;   // 智能体当前是否在说话
    let micOn = false;
    let reply = "";      // 拼接大模型流式回复

    function setReady(ready) {
      ["btnMic", "btnStop", "btnDestroy", "txt", "btnAsk"].forEach((id) => ($(id).disabled = !ready));
      $("btnConnect").disabled = ready;
    }

    async function connect() {
      setStatus("连接中…");
      agent = new window.XingyunAvatarAgent({
        containerId: "#agent-avatar",
        appId: APP_ID,
        appSecret: APP_SECRET,
        gatewayServer: "https://nebula-agent.xingyun3d.com/user/v1/ttsa_v2/session",
        // 识别完自动交给大脑;同时打开语音前处理 + 回声消除
        features: {
          auto_send_asr_to_llm: true,
          speech_frontend: { enabled: true, enable_aec: true },
        },
        // 渲染层错误(数字错误码)
        onMessage(err) { log("[渲染错误]", err.code, err.message); },
        agentCallbacks: {
          onAgentStateChange(state) { log("[Agent]", state); },
          onASRStateChange(state) { log("[麦克风]", state); },
          onASRResult({ text, isFinal }) {
            if (!text) return;
            log(isFinal ? "[我说完了]" : "[我正在说]", text);
            // 我插嘴了,而它还在说:先让它闭嘴
            if (speaking) agent.interrupt();
          },
          onLLMResponse(res) {
            // 文字在 chunk 里流式到达,done 时 text 可能为空,自己拼
            if (res.event === "chunk") reply += res.text || "";
            if (res.event === "done") { log("[大脑回复]", reply); reply = ""; }
          },
          onSpeakStateChange({ state }) {
            speaking = state === "speak_start";
            log("[播报]", state);
          },
          onConversationChange({ state }) {
            log("[一轮对话]", state);
            if (state === "completed") setStatus("已就绪,等你说话");
            if (state === "speaking") setStatus("它在说…");
          },
          // Agent 层错误(字符串错误码 + domain)
          onError(err) { log("[Agent错误]", err.domain, err.code, err.message); },
        },
      });

      try {
        await agent.init({
          onDownloadProgress(p) { setStatus("资源加载 " + p.toFixed(0) + "%"); },
        });
        setStatus("已就绪,等你说话");
        setReady(true);
        agent.ask("你好,请用一句话欢迎来访的客人");
      } catch (e) {
        log("[初始化失败]", e && e.message);
        await agent.destroy("init_failed").catch(() => {});
        agent = null;
        setStatus("连接失败,看日志");
      }
    }

    $("btnConnect").onclick = connect;

    $("btnAsk").onclick = () => {
      const t = $("txt").value.trim();
      if (!t || !agent) return;
      if (speaking) agent.interrupt();
      agent.ask(t);
      $("txt").value = "";
    };
    $("txt").onkeydown = (e) => e.key === "Enter" && $("btnAsk").onclick();

    // 麦克风必须在用户点击里打开(浏览器权限策略)
    $("btnMic").onclick = async () => {
      if (!agent) return;
      if (micOn) { await agent.stopASR(); micOn = false; $("btnMic").textContent = "开麦"; }
      else { await agent.startASR(); micOn = true; $("btnMic").textContent = "关麦"; }
    };

    $("btnStop").onclick = () => {
      if (!agent) return;
      const ms = agent.interrupt();
      log("[手动打断] 耗时", ms, "ms");
      agent.listen();
    };

    $("btnDestroy").onclick = async () => {
      if (!agent) return;
      await agent.destroy("user");   // 只删 DOM 不会释放麦克风,必须 destroy
      agent = null; micOn = false; speaking = false;
      setReady(false); $("btnMic").textContent = "开麦";
      setStatus("已销毁");
    };
  </script>
</body>
</html>
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐