银发经济的正确解法:如何在Vue + vite中开发魔珐星云SDK打造低延时养老陪伴大屏

0 阅读18分钟

非广告,是产品体验,以下有详细代码参考

银发经济的正确解法:智能养老陪伴大屏架构拆解与落地

在这里插入图片描述


前言

2026年,中国60岁以上人口突破3亿,超过总人口的20%,中国已步入中度老龄化社会。银发经济正从边缘赛道跃迁为万亿级蓝海。然而在 “适老化改造” 的浪潮中,绝大多数智能设备仍停留在放大字体、增大音量的表层优化,没能真正解决老年人的数字鸿沟问题。老年人在跨越操作障碍的同时,仍深陷情感陪伴的系统性缺失 —— 冰冷的智能音箱与机械交互,始终无法替代一个能察言观色、微笑倾听的 “人”。 要破解这些痛点,我们需要具备情感表达与自然交互能力的具身智能体。本文从架构与落地视角,深度拆解如何依托魔珐星云 SDK,打造一款真正低时延(约 500ms)、可随时打断、有温度的智能养老陪伴大屏。


一、可行性分析:陪伴大屏能否走入千家万户?

当我们谈论“陪伴大屏”时,必须回答一个根本问题:它是否具备大规模落地的现实条件? 下面从政策、市场与技术三个维度进行拆解。

1.1 需求:两个尚未被填平的痛点

在这里插入图片描述

当前养老陪伴产品面临两个核心痛点。

  • 第一,数字鸿沟与生理衰退叠加。视力模糊、反应变慢,让复杂的 App 与智能电视 UI 成为障碍。技术带来的不是赋能,而是新的隔离。

  • 第二,情感陪伴系统性缺失。独居老人一天可能说不上十句话。智能音箱虽能对话,却是一个冰冷的“黑盒子”。人类沟通依赖察言观色与肢体回应——老年人需要的是一个能看着他的眼睛、微笑倾听的“人”。


1.2 政策与市场:银发经济的双重驱动力

在这里插入图片描述

政策端,2024 年国务院办公厅《关于发展银发经济增进老年人福祉的意见》明确提出“打造智慧健康养老新业态”,多地将带屏智能设备纳入居家适老化改造补贴清单,为陪伴大屏打开了从孵化到规模化的绿色通道。

市场端,中国 60 岁以上人口已突破 3 亿,超过一半处于空巢或独居状态,专业养老护理员缺口达千万级。与此同时,短视频、微信、移动支付在银发人群中渗透率逐年走高,为陪伴型 AI 设备完成了初步的用户教育。需求真实、基数庞大、支付意愿渐显——市场可行性已基本成立。


1.3 技术:端侧架构打破规模化的最后一堵墙

在这里插入图片描述

政策和市场条件具备之后,真正的瓶颈落在技术上。

传统数字人交互方案依赖云端集中渲染:语音上传后,云端依次完成识别、生成、语音合成,再在 GPU 上渲染为带口型的视频流推回终端。这种模式对带宽与网络稳定性要求高,弱网环境(如厨房冰箱屏)易出现卡顿、响应迟缓;同时,每一路并发都占用云端 GPU,规模化部署成本高;且视频流作为一个整体,难以实时打断,交互生硬。

魔珐星云采用的是端侧参数流渲染架构。云端仅下发轻量级驱动指令(口型权重、表情参数、骨骼动画关键帧等),真正的 3D 渲染在终端本地完成,打通了 感知→理解→决策→表达→执行 的全链路。这意味着:

  • 低时延:端侧渲染使端到端响应延迟稳定在500ms以内;
  • 高稳定:不再依赖持续的高带宽视频流,弱网环境同样流畅;
  • 可规模化:单 GPU 可支撑千路以上并发,成本不再随用户规模线性膨胀;
  • 可实时打断:参数流天然支持随时中断与无缝切换,交互如真人对话般自然。

魔珐星云作为 AI 屏幕操作系统与具身智能表达层基础设施,用这套端侧架构解决了养老场景中“难用、无温度、难规模化”三大痛点,让养老陪伴大屏从概念走向普惠落地。


二、产品对比与选择:为什么最终选择了星云

在这里插入图片描述

在技术选型阶段,我们横向对比了市面上主流的数字人交互方案,最终选定魔珐星云,核心原因在于延迟、真实感与成本三角的一次性突破。


2.1 传统云端推流架构的死穴

绝大多数数字人厂商的技术路线可概括为“视频作为交互界面”或“传统纯端渲染”,但这两种方案在落地时都存在致命缺陷:

graph TD
    A["本地终端: 语音收音"] --> B["上传云端"]
    
    subgraph "路线1:云推流方案(最普遍)"
        B --> C["云端 ASR 语音识别"]
        C --> D["云端 LLM 文本生成"]
        D --> E["云端 TTS 语音合成"]
        E --> F["云端 GPU 实时视频渲染 (昂贵/耗时)"]
        F --> G["视频编码后下发推流 (高带宽要求)"]
        G --> H["终端播放器被动解码 (延迟保底 3~5s)"]
    end
    
    subgraph "路线2:传统纯端渲染"
        B --> I["云端仅作语义计算"]
        I --> J["终端 GPU 进行重度 3D 渲染"]
        J -.-> K["痛点:对轻量 IoT 设备(如冰箱屏/音箱)算力要求极高,难以普及"]
    end

这条链路的物理延迟保底在 3~5 秒,还不包括网络波动带来的额外卡顿。试想,老人问一句“今天天气怎么样?”,屏幕里的助手愣在那里整整 3 秒才开口。在这种尴尬的沉默中,老人可能会怀疑“它是不是没听见”,于是重复提问,反而触发重听与打断的连锁混乱。沟通的自然感被彻底破坏,陪伴的温度荡然无存。


2.2 星云方案:端侧渲染破解“不可能三角”

星云彻底摒弃了视频推流,选择了端侧渲染 + 参数流的具身智能架构:

graph TD
    subgraph "传统数字人架构"
        A["老人语音提问"] --> B["云端全链路处理与视频渲染"]
        B --> C["下发视频流 (带宽>2Mbps, 延迟>3s)"]
        C --> D["屏幕被播放"]
    end
graph TD
    subgraph "魔珐星云具身架构"
        A2["老人语音提问"] --> B2["云端语义理解与参数生成"]
        B2 --> C2["下发动作/表情参数包 (数KB, 延迟<200ms)"]
        C2 --> D2["本地实时3D渲染 (极低延迟, 自然打断)"]
    end

这两条路径的差异是根本性的。

在这里插入图片描述

  • 极致的响应速度:抛弃视频流的沉重包袱后,云端只需下发文本驱动所需的语义参数,网络传输极其轻量。端侧在收到第一帧参数后即刻开始渲染,数字人可以在 500 ms内启动倾听姿态(如微微点头),并在 TTS 首包到达时同步张嘴,接近人类对话的自然停顿,彻底告别“等待的尴尬”。

请添加图片描述

  • 真实的微表情与“边说边动”:由于端侧 3D 渲染引擎拥有对模型本体的完全控制权,数字人不再是一段被动的视频。它可以在说话的同时伴随自然的微表情变化——眼睛微微睁大表示好奇,眉毛轻蹙表示理解,头部微侧表示倾听。这些细腻的表达完全通过参数流实时驱动,而非事先录好的动画,使交互充满“活人感”。

请添加图片描述

  • 可打断性与状态流畅切换:云端渲染画面采用集中式处理,遇到打断需要终止推流、清理缓冲区、重新发起请求,极易产生突兀跳变。而在星云的端渲染模式下,打断只是参数流的中断与更新,引擎以极低开销即时中止当前动作/语音,无缝过渡到下一个状态,毫无闪烁或僵直,极大提升了交互的聪慧感。

2.3 成本与规模化的巨大优势

在这里插入图片描述

陪伴大屏若要走进千家万户,成本红线至关重要。传统方案中,每一个并发交互都需要云端 GPU 进行实时视频渲染,假设面向百万用户,背后的 GPU 集群建设和运维成本将是天文数字,这也意味着厂商必然通过订阅费或高价硬件转嫁给消费者。 而星云的参数流模式,云端只负责轻量级语义计算,单GPU 可支持数千路并发。端侧渲染利用的是设备本就存在的 GPU 算力,变相实现了“算力民主化”,让一台千元级带屏音箱也能承载电影级数字人。这为大规模普惠养老提供了现实的经济基础。


三、快速上手开发

基于我们的实际实践,星云 SDK 的接入非常直观,官方也提供了 JS SDK Demo接入。以下是笔者在 Vue3 + Vite 项目中集成魔珐星云数字人的完整步骤:

3.1 环境准备

1. 创建 Vue3 + Vite 项目
pnpm create vite@latest demo --template vue
cd demo
pnpm install
2. 引入 SDK 脚本

在 index.html 中添加:

<script src="https://media.xingyun3d.com/xingyun3d/general/litesdk/xmovAvatar@latest.js"></script>
3.2 创建数字人组件

请添加图片描述

创建 DigitalHuman.vue 组件。注意:星云 SDK 的 DOM 挂载点应保持“纯净”,避免 Vue 的响应式系统直接干预其内部。

<template>
  <div class="digital-human-container">
    <div class="avatar-wrapper">
      <!-- SDK 的专属纯净挂载点 -->
      <div class="avatar-container" id="avatar-container"></div>
      
      <!-- 加载遮罩 -->
      <div v-if="status === '正在初始化数字人...'" class="loading-overlay">
        <div class="loading-spinner"></div>
        <p>{{ status }}</p>
      </div>
    </div>

    <div class="controls">
      <div class="input-group">
        <input v-model="message" type="text" placeholder="请输入要数字人说的话..." @keyup.enter="sendMessage" />
        <button @click="sendMessage" :disabled="!message">发送</button>
      </div>
      
      <div class="status-info">
        <p>状态: {{ status }}</p>
        <p>连接: {{ connectionStatus }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const message = ref('')
const status = ref('待命')
const connectionStatus = ref('未连接')
let liteSDK = null

onMounted(() => {
  // 建议延迟初始化以确保 DOM 准备就绪
  setTimeout(initDigitalHuman, 1000)
})

onUnmounted(() => {
  if (liteSDK && liteSDK.destroy) {
    liteSDK.destroy()
  }
})

const initDigitalHuman = async () => {
  try {
    status.value = '正在初始化数字人...'
    
    // 1. 创建 SDK 实例
    liteSDK = new XmovAvatar({
      containerId: '#avatar-container',
      appId: import.meta.env.VITE_XINGYUN_APP_ID, // 推荐使用环境变量
      appSecret: import.meta.env.VITE_XINGYUN_APP_SECRET,
      gatewayServer: 'https://nebula-agent.xingyun3d.com/user/v1/ttsa/session',
      onStateChange: (state) => status.value = state,
      onStatusChange: (status) => connectionStatus.value = status
    })
    
    // 2. 【关键】调用 init() 真正建立连接并加载资源
    await liteSDK.init({
      onDownloadProgress: (progress) => console.log(`加载中: ${progress}%`)
    })
    
    status.value = '就绪'
  } catch (error) {
    console.error('初始化失败:', error)
    status.value = '初始化失败'
  }
}

const sendMessage = async () => {
  if (!message.value || !liteSDK) return
  
  try {
    // 封装标准 SSML,确保数字人动作衔接自然
    const ssml = `<speak>${message.value}</speak>`
    liteSDK.speak(ssml, true, true)
    message.value = ''
  } catch (error) {
    console.error('发送失败:', error)
  }
}
</script>

<style scoped>
/* 容器保持固定比例,适配养老陪伴大屏的长辈视觉习惯 */
.avatar-container {
  width: 100%;
  height: 800px;
  background: #f0f0f0;
  border-radius: 12px;
}
.controls { margin-top: 20px; }
.input-group { display: flex; gap: 10px; }
input { flex: 1; padding: 12px; border-radius: 25px; border: 1px solid #ddd; }
button { padding: 10px 25px; border-radius: 25px; background: #667eea; color: white; cursor: pointer; }
</style>

3.3 关键配置与 API 说明

在实际开发中,我们总结了星云 SDK 的几个关键点:

    1. 资源初始化:
    • 仅仅 new XmovAvatar() 是不够的,必须调用 await liteSDK.init()。这个方法会触发底层 WebGL 资源的加载和 WebSocket 的握手。
    • 建议在 index.html 中通过 CDN 引入脚本,以利用浏览器缓存。
    1. 核心 API 概览:
    • speak(ssml, isStart, isEnd):核心语音动作驱动方法。推荐始终包裹 标签。
    • destroy():页面销毁时务必调用,释放显存和网络连接。
    • changeLayout(config):实时调整数字人的位置、缩放(scale)和偏移(offset),适配大屏 UI。
    1. 适老化设计细节:
    • 视觉增强:在 App.vue 中我们使用了深色渐变背景,以突显数字人的轮廓。
    • 交互反馈:增加了 loading-overlay。由于数字人资源加载(约 10-20MB)在弱网环境下会有感官延迟,明确的进度提示对老人非常重要。
    • 状态可视化:将 SDK 内部状态(如“倾听中”、“思考中”)翻译为直观的中文描述显示。 通过这一套流程,我们成功将数字人从复杂的 3D 渲染降维成了简单的 Web 组件开发,大大缩短了养老陪伴系统的上线周期。

四、实现“听-想-说”闭环:ASR 与 LLM 的实战接入

要让数字人真正“活”起来,我们需要为它装上“耳朵”(语音识别 ASR)和“大脑”(大语言模型 LLM),打通“听-想-说”的全链路。结合我们的 Demo,以下是具体的接入实战经验。

在这里插入图片描述 请添加图片描述


4.1 ASR 接入:精准倾听老人的心声

在养老场景中,老人的语速往往较慢且带有口音,因此我们选择了识别率极高的火山引擎豆包流式语音识别(ASR)服务。 由于豆包 ASR V3 版本使用了高性能的 WebSocket 二进制协议,我们在 asrService.js 中进行了底层封装。 核心挑战与解决:

    1. 浏览器 WebSocket 鉴权限制:原生 WebSocket 无法在 JS 中设置自定义 Header。我们通过 Vite 的 http-proxy 代理拦截 proxyReqWs 事件,动态注入 X-Api-Key,完美解决了前端直连的鉴权问题。
    1. 实时音频采集与分包:使用浏览器 AudioContext 获取麦克风流,重采样至 16000Hz PCM 格式。为了保证流式识别的最佳延迟,我们将 ScriptProcessor 的 buffer size 设为 2048(约 128ms 极低延迟分包)。
    1. 二进制协议解析:严格遵循 4 字节 Header + Payload 的结构,动态判断序列号与压缩方式。
智能判停与状态流转:

当解析到 ASR 返回的 isFinal: true(表示老人一句话说完)时,前端不再死板地等待手动点击,而是立刻触发大模型请求,打造持续倾听的自然对话体验。

/**
 * 火山引擎 ASR (流式语音识别) 服务封装
 * 适配豆包大模型流式语音识别 API v3
 */

import { v4 as uuidv4 } from 'uuid';
import pako from 'pako'; // 需要安装 pako 处理 gzip

// 由于浏览器原生 WebSocket 不支持自定义 Header 鉴权,
// 我们在 vite.config.js 中配置了代理,由开发服务器代为注入认证头
const ASR_WS_URL = `ws://${window.location.host}/api/asr/api/v3/sauc/bigmodel_async`;

export class VolcASRService {
  constructor(options = {}) {
    this.ws = null;
    this.onResult = options.onResult || (() => {});
    this.onError = options.onError || (() => {});
    this.audioContext = null;
    this.processor = null;
    this.stream = null;
    this.sequence = 1; // 消息序列号
    this.isStopping = false;
    this.retryCount = 0;
  }

  async start() {
    this.isStopping = false;
    this.sequence = 1;
    try {
      if (!this.stream) {
        this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      }
      
      console.log('[ASR] Connecting via proxy:', ASR_WS_URL);
      this.ws = new WebSocket(ASR_WS_URL);
      this.ws.binaryType = 'arraybuffer';

      this.ws.onopen = () => {
        console.log('[ASR] WebSocket connected');
        this.sendFullClientRequest();
        this.startRecording();
      };

      this.ws.onmessage = (event) => {
        this.handleMessage(event.data);
      };

      this.ws.onerror = (err) => {
        console.error('[ASR] WebSocket error:', err);
        this.onError(err);
      };

      this.ws.onclose = (event) => {
        console.log('[ASR] WebSocket closed, code:', event.code, 'reason:', event.reason);
        if (!this.isStopping && this.retryCount < 3) {
          this.retryCount++;
          console.log(`[ASR] 重连中... (${this.retryCount}/3)`);
          setTimeout(() => this.start(), 2000);
        } else if (this.retryCount >= 3) {
          this.onError(new Error('ASR 连接失败,已重试 3 次'));
        }
      };

    } catch (err) {
      console.error('[ASR] Start failed:', err);
      this.onError(err);
    }
  }

  stop() {
    this.isStopping = true;
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.sendLastAudio();
      this.ws.close();
    }
    this.stopRecording();
  }

  stopRecording() {
    if (this.processor) {
      this.processor.disconnect();
      this.processor = null;
    }
    if (this.audioContext) {
      this.audioContext.close();
      this.audioContext = null;
    }
  }

  sendFullClientRequest() {
    const params = {
      user: { uid: uuidv4() },
      audio: {
        format: 'pcm',
        rate: 16000,
        bits: 16,
        channel: 1
      },
      request: {
        model_name: 'bigmodel',
        enable_itn: true,
        enable_punc: true,
        result_type: 'full',
        enable_nonstream: true // 开启二遍识别,提高最终准确率
      }
    };

    const payload = pako.gzip(JSON.stringify(params));
    
    // 构造 4 字节 Header(严格对照文档示例)
    // byte0: version=0b0001(4bit) | header_size=0b0001(4bit) = 0x11
    // byte1: msg_type=0b0001(full client req) | flags=0b0000(无sequence) = 0x10
    // byte2: serialization=0b0001(JSON) | compression=0b0001(Gzip) = 0x11
    // byte3: reserved = 0x00
    const header = new Uint8Array([0x11, 0x10, 0x11, 0x00]);

    const size = new Uint32Array(1);
    new DataView(size.buffer).setUint32(0, payload.length, false); // 大端

    const msg = new Uint8Array(4 + 4 + payload.length);
    msg.set(header, 0);
    msg.set(new Uint8Array(size.buffer), 4);
    msg.set(payload, 8);

    this.ws.send(msg);
  }

  startRecording() {
    if (this.audioContext) return;
    
    this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
    this.processor = this.audioContext.createScriptProcessor(2048, 1, 1); // 2048 样本约为 128ms,符合 100-200ms 建议
    const source = this.audioContext.createMediaStreamSource(this.stream);

    source.connect(this.processor);
    this.processor.connect(this.audioContext.destination);

    this.processor.onaudioprocess = (e) => {
      if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;

      const inputData = e.inputBuffer.getChannelData(0);
      const pcmData = this.floatTo16BitPCM(inputData);
      this.sendAudio(pcmData);
    };
  }

  sendAudio(pcmData) {
    const payload = pako.gzip(pcmData);
    // byte0: version=1 | header_size=1 = 0x11
    // byte1: msg_type=0b0010(audio only) | flags=0b0000(无sequence) = 0x20
    // byte2: serialization=0b0000(none) | compression=0b0001(Gzip) = 0x01
    // byte3: reserved = 0x00
    const header = new Uint8Array([0x11, 0x20, 0x01, 0x00]);

    const size = new Uint32Array(1);
    new DataView(size.buffer).setUint32(0, payload.length, false);

    const msg = new Uint8Array(4 + 4 + payload.length);
    msg.set(header, 0);
    msg.set(new Uint8Array(size.buffer), 4);
    msg.set(payload, 8);

    this.ws.send(msg);
  }

  sendLastAudio() {
    // 最后一包:flags=0b0010 表示这是最后一包(负包)
    // byte1: msg_type=0b0010(audio only) | flags=0b0010(last) = 0x22
    // byte2: serialization=0b0000 | compression=0b0001(Gzip) = 0x01
    const header = new Uint8Array([0x11, 0x22, 0x01, 0x00]);
    // 负包也需要附带 payload size(为 0)
    const size = new Uint8Array(4); // 全 0,表示无 payload
    const msg = new Uint8Array(8);
    msg.set(header, 0);
    msg.set(size, 4);
    this.ws.send(msg);
  }

  handleMessage(data) {
    try {
      const view = new DataView(data);
      const byte0 = view.getUint8(0);
      const byte1 = view.getUint8(1);
      const byte2 = view.getUint8(2);

      // 解析 Header 字段
      const headerSize = (byte0 & 0x0F) * 4;       // Header 大小(字节)
      const msgType = (byte1 >> 4) & 0x0F;          // 消息类型
      const msgFlags = byte1 & 0x0F;                // 消息标志
      const compression = byte2 & 0x0F;             // 压缩方式:0=无,1=Gzip

      // 判断是否有 Sequence Number(flags 的 bit0 为 1 时有)
      const hasSequence = (msgFlags & 0x01) === 1;
      let offset = headerSize;
      if (hasSequence) {
        offset += 4; // 跳过 4 字节的 Sequence Number
      }

      console.log(`[ASR] msg type=0x${msgType.toString(16)}, flags=0x${msgFlags.toString(16)}, compression=${compression}, headerSize=${headerSize}, hasSeq=${hasSequence}`);

      // 0xF = Error message
      if (msgType === 0xF) {
        const errorCode = view.getUint32(offset, false);
        offset += 4;
        const errorMsgSize = view.getUint32(offset, false);
        offset += 4;
        const errorMsg = new TextDecoder().decode(new Uint8Array(data.slice(offset, offset + errorMsgSize)));
        console.error('[ASR] Server error:', errorCode, errorMsg);
        return;
      }

      // 0x9 = Full server response
      if (msgType === 0x9) {
        const payloadSize = view.getUint32(offset, false);
        offset += 4;
        const rawPayload = new Uint8Array(data.slice(offset, offset + payloadSize));

        let jsonStr;
        if (compression === 1) {
          // Gzip 压缩
          jsonStr = new TextDecoder().decode(pako.ungzip(rawPayload));
        } else {
          // 无压缩
          jsonStr = new TextDecoder().decode(rawPayload);
        }

        const result = JSON.parse(jsonStr);
        console.log('[ASR] Result:', result?.result?.text);

        if (result.result && result.result.text) {
          const utterances = result.result.utterances || [];
          const lastUtterance = utterances[utterances.length - 1];
          const isFinal = lastUtterance?.definite === true;
          this.onResult(result.result.text, isFinal);
        }
      }
    } catch (e) {
      console.warn('[ASR] Parse error:', e.message);
      // 打印前 20 字节帮助调试
      const preview = new Uint8Array(data.slice(0, Math.min(20, data.byteLength)));
      console.warn('[ASR] Raw bytes:', Array.from(preview).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' '));
    }
  }

  floatTo16BitPCM(input) {
    const output = new Int16Array(input.length);
    for (let i = 0; i < input.length; i++) {
      const s = Math.max(-1, Math.min(1, input[i]));
      output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
    }
    return new Uint8Array(output.buffer);
  }
}

请添加图片描述


4.2 LLM 接入:智慧大脑的适老化调优

有了 ASR 转写的文本,接下来我们需要 LLM 生成回复。在 llmService.js 中,我们并没有直接把用户输入丢给大模型,而是进行了深度的适老化提示词(Prompt)工程调优。

/**
 * LLM 服务封装 (适配养老陪伴大屏)
 */

const LLM_BASE_URL = import.meta.env.VITE_LLM_BASE_URL || 'https://api.deepseek.com';
const LLM_MODEL = import.meta.env.VITE_LLM_MODEL || 'deepseek-chat';
const LLM_API_KEY = import.meta.env.VITE_LLM_API_KEY || '';

/**
 * 获取系统提示词 (注入养老陪伴背景)
 */
export function getSystemPrompt() {
  return `你是一个智能养老陪伴助手,名字叫"小星"。你的主要任务是陪伴老人聊天、提供健康建议和生活提醒。

【你的性格与风格】
1. 亲切、耐心、专业,像一个体贴的晚辈。
2. 回复必须极其简短有力,控制在 30 个字以内!老人不喜欢长篇大论。
3. 使用温暖、生活化的语言,多用关怀性话语。
4. 如果老人提到身体不适,给予安慰并建议咨询专业医生。

【回复限制】
1. 只输出纯文本。
2. 不要使用 Markdown 格式(如加粗、列表等)。
3. 严禁提供任何处方药建议。`;
}

/**
 * 检查 LLM 是否已配置
 */
export function isLLMConfigured() {
  return !!LLM_API_KEY;
}

/**
 * 非流式调用大模型 (为了快速集成,先实现基础版本)
 */
export async function chatWithLLM(userMessage) {
  const url = `/api/llm/chat/completions`;

  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${LLM_API_KEY}`,
      },
      body: JSON.stringify({
        model: LLM_MODEL,
        messages: [
          { role: 'system', content: getSystemPrompt() },
          { role: 'user', content: userMessage },
        ],
        stream: false,
        max_tokens: 100,
      }),
    });

    if (!response.ok) {
      const errText = await response.text();
      throw new Error(`API 请求失败: ${errText}`);
    }

    const data = await response.json();
    return data.choices?.[0]?.message?.content || '抱歉,我现在有点走神了。';
  } catch (error) {
    console.error('LLM 请求错误:', error);
    return '奶奶,刚才网络好像断了一下,您可以再说一遍吗?';
  }
}

老人需要的不是长篇大论的百科全书,而是简短、有温度的陪伴。我们的 System Prompt 是这样设计的:

export function getSystemPrompt() {
  return `你是一个智能养老陪伴助手,名字叫"小星"。你的主要任务是陪伴老人聊天、提供健康建议和生活提醒。

【你的性格与风格】
1. 亲切、耐心、专业,像一个体贴的晚辈。
2. 回复必须极其简短有力,控制在 30 个字以内!老人不喜欢长篇大论。
3. 使用温暖、生活化的语言,多用关怀性话语。
4. 如果老人提到身体不适,给予安慰并建议咨询专业医生。

【回复限制】
1. 只输出纯文本。
2. 不要使用 Markdown 格式(如加粗、列表等)。
3. 严禁提供任何处方药建议。`;
}

连接数字人与大模型:

当大模型返回如“王奶奶,这几天降温,您注意膝盖保暖。”这样的温情话语时,我们直接将其组装成 SSML,调用星云 SDK 驱动数字人发声并辅以自然的肢体动作:

// 假设 aiReply 是大模型的回复文本
const ssml = `<speak>${aiReply}</speak>`;
// 数字人开始同步语音与口型、动作
liteSDK.speak(ssml, true, true);

通过这一套“ASR -> LLM -> 星云 SDK”的丝滑流转,屏幕里的数字人不再是一个冰冷的 3D 模型,而变成了一个能够察言观色、知冷知热的智能伙伴。


4.3 云端记忆:让大模型真正“记住”老人

LLM 天生是“无状态”的——每次对话结束,它就会忘记一切。但养老陪伴要求的恰恰是长期记忆:记住王奶奶对花生过敏,记住李爷爷每天下午三点要吃降压药,记住张奶奶最近三天的血压都偏高。要实现这种“有记忆的关怀”,我们必须为大模型接入一套云端健康数据库。 我们设计了一套轻量级的健康档案系统,将老人的生活数据按维度分表存储:

-- 每日餐食记录
CREATE TABLE meal_records (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  elder_id VARCHAR(32) NOT NULL,
  meal_type ENUM('breakfast', 'lunch', 'dinner', 'snack'),
  content TEXT,           -- “小米粥、蒸鸡蛋、少量咸菜”
  allergy_alert BOOLEAN DEFAULT FALSE,
  recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 用药记录
CREATE TABLE medication_records (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  elder_id VARCHAR(32) NOT NULL,
  drug_name VARCHAR(100),  -- “氨氯地平 5mg”
  dosage VARCHAR(50),
  taken_at DATETIME,
  is_taken BOOLEAN DEFAULT FALSE  -- 是否已服用
);

-- 运动与身体指标(来自智能手环/血压计等 IoT 设备)
CREATE TABLE health_metrics (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  elder_id VARCHAR(32) NOT NULL,
  metric_type ENUM('blood_pressure', 'heart_rate', 'steps', 'sleep_hours', 'blood_sugar'),
  value VARCHAR(50),       -- “138/85” 或 “6800步”
  recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

在每次对话发起时,后端会从数据库中提取当日的健康摘要,作为上下文注入到 LLM 的请求中:

// 构造带有健康记忆的对话请求
const healthContext = await fetchTodayHealthSummary(elderId);
// 示例输出:“今日血压 138/85(偏高),已服降压药,午餐吃了红烧肉,步数 2300 步。”

const messages = [
  { role: 'system', content: getSystemPrompt() },
  { role: 'system', content: `【今日健康档案】${healthContext}` },
  { role: 'user', content: userMessage },
];

这样,当老人随口说一句“小星,我今天能吃花生米吗?”,大模型不再给出泛泛的建议,而是结合数据库中的过敏记录明确回复:“奶奶,您对花生过敏哦,我给您推荐核桃仁吧,补脑又好吃。”

更进一步,当 IoT 设备检测到“连续两天深度睡眠不足 1 小时”或“晨起血压连续偏高”时,系统可以主动生成关怀话术,通过星云 SDK 驱动数字人做出主动问候——不仅有语音播报,还同步做出“关怀前倾、微微蹙眉”的肢体动作,让老人感受到的不是冰冷的数据警报,而是一句真诚的嘘寒问暖。


五、未来畅想

在这里插入图片描述

基于魔珐星云端侧渲染与参数流架构,养老陪伴设备将从 “单一屏幕” 升级为全屋具身智能体。 依托星云多终端统一驱动能力,同一数字人形象可在电视、冰箱屏、浴室魔镜、卧室音箱等设备间无缝流转,实现跨屏记忆接续、全场景陪伴。 未来结合视觉感知与健康 IoT 数据,可实现主动关怀、安全监测、情感陪伴,让具身智能真正融入老人日常生活,成为有温度、可信赖的家庭伙伴。

欢迎大家一起来试用一下:产品体验url


六、总结

银发经济的真正解法,不是叠加界面功能,而是用具身智能重构人机交互与情感连接。

魔珐星云作为AI 屏幕操作系统、具身智能时代的表达层基础设施,凭借端侧渲染 + 参数流核心架构,打通感知→理解→决策→表达→执行全链路,让普通屏幕升级为可倾听、可回应、可表达、可关怀的具身智能体。

该方案实现低延迟、可打断、低成本规模化部署,为银发经济提供可落地、可普惠、有温度的养老陪伴标准答案。

技术终将老去,但嵌入技术中的关怀永远年轻。陪伴,正是技术最温暖的落点。