百炼「实时多模态交互」API 验证记录
安全说明 本文中的 API Key、Workspace ID、App ID 等账号信息已全部脱敏(写作
sk-****、llm-****、mm_****),只保留字段结构与调用方式;历史命令保留可复现的字段名,不含真实凭据。
背景 在这个项目里,语音对话客户端要跑在 RV1106G3 板端(armv7l + uClibc)。官方只提供 x86_64 与 aarch64 的预编译 SDK,板子用不了;但底层的 WebSocket 协议是公开的。于是先在虚拟机上**用纯标准库(不装任何第三方包)**把协议跑通,再往板子上移植。
一、为什么不用官方 SDK
| 方案 | 结论 |
|---|---|
| 官方 Linux C++ SDK | 只提供 x86_64 / aarch64 预编译包;板子是 armv7l + uClibc,架构和 C 库都对不上 |
官方服务端 Python SDK(dashscope) |
能跑在虚拟机或服务器上,但板子装不了,还引入大量依赖 |
| WebSocket 裸协议自研客户端 | ✅ 只依赖 libc 与 OpenSSL,协议公开且完整 |
二、协议要点
- 服务地址:
wss://dashscope.aliyuncs.com/api-ws/v1/inference - 鉴权:WebSocket 握手(HTTP Upgrade)时带
Authorization: Bearer <API Key> - 消息分两类:二进制 = 音频;文本 = JSON(
header+payload) header固定三字段:task_id(本次连接唯一)、streaming: "duplex"、action取run-task/continue-task/finish-taskStart里的固定值直接照抄:task_group=aigc、task=multimodal-generation、function=generation、model=multimodal-dialog- 关键时序:
Start→ 服务端回Started→ 必须再等到DialogStateChanged的state=Listening,才可以发音频或发文本请求
音频格式(push2talk 模式)
| 方向 | 要求 |
|---|---|
| 上行 | PCM,16 kHz、16 bit、单声道、有符号、little-endian |
| 分包 | 建议每 100 ms 一包 → 16000 × 2 × 0.1 = 3200 字节 |
| 下行 | 默认 24 kHz,可按需选 pcm / mp3 / opus |
三、验证环境
- Ubuntu 22.04 虚拟机,Python 3.10.12 + OpenSSL 3.0.2
- 不装任何第三方库:握手用
socket+ssl自己拼 HTTP Upgrade,WebSocket 帧编解码按 RFC6455 手写(客户端帧加掩码、服务端帧不加),PCM 重采样用标准库audioop - 音频来源:把 TTS 下行音频重采样成 16 kHz 后回灌给 ASR,全程不需要麦克风
四、四项验证
1. 文本触发(打通大模型 + 语音合成)
Start(run-task) → Started → DialogStateChanged(Listening)
→ RequestToRespond(type=prompt, text="…")
→ SpeechContent → DialogStateChanged(Thinking) → DialogStateChanged(Responding)
→ RespondingStarted → 二进制音频帧… → RespondingEnded → Stop
RespondingContent 是流式推送的(finished=false 一路推到 true),可以直接拿来做屏幕字幕。
2. 语音识别(ASR)
Start(mode=push2talk) → 等 Listening
→ SendSpeech → 按 100 ms / 3200 字节送 PCM → StopSpeech
→ SpeechContent(识别文本,流式全量)
实测:把 TTS 生成的一句话重采样成 16 kHz 后送上去,识别结果与原句完全一致。识别完成后服务端会继续走大模型与语音合成,等于一次拿到「识别 → 回复 → 合成」全链路。
3. 对话打断
播报中(已收到 27048 字节音频)→ 发 RequestToSpeak
→ RequestAccepted → DialogStateChanged(Listening)
打断被接受后服务端立刻停止下发音频,并回到可以继续说话的状态。
4. 心跳保活
服务端规则:任意连续 60 秒没有消息就会断开。客户端每 50 秒发一次 HeartBeat,服务端会原样回一条。
实测往返 0.06 秒。
五、协议覆盖情况
| 消息 | 状态 |
|---|---|
| Start / Stop | ✅ |
| DialogStateChanged(Listening / Thinking / Responding) | ✅ |
| RequestToRespond(文本触发) | ✅ |
| SendSpeech / StopSpeech / 二进制音频上行 | ✅ |
| SpeechContent | ✅ |
| RespondingStarted / RespondingContent / RespondingEnded | ✅ |
| 二进制音频下行 | ✅ |
| RequestToSpeak | ✅ |
| HeartBeat | ✅ |
| 尚未验证:VQA 图片、UpdateInfo、CancelSpeech、自定义对话历史 | — |
六、踩过的坑
1. 音色必须与合成模型对应
应用配置的是多模态交互轻量版语音合成(AppSpecificTTS),它要求显式指定音色,否则报
426 InvalidTtsVoice: tts voice error, need AppSpecificTTS voice.
- 可用:
xiaoyun、aixia、aimei、aiqi、zhixiaoxia - 不可用:
longxiaochun(属于 CosyVoice 大模型的音色)
2. 「插件」与「指令」是两回事
- 插件:云端工具(天气、万年历、新闻…),回答"问什么"
- 指令:云端下发、设备执行的动作(驱动灯效、舵机…),让设备"动起来"
开了意图识别但没配任何工具时,extra_info 里能看到判定结果:
{"agent_info":{"intent_infos":[{"domain":"text_chat","intent":"chat"}]},"tool_infos":[]}tool_infos 为空,说明本轮没有工具被调用。
3. extra_info.commands 是 JSON 字符串
若启用「指令」,下发内容位于 RespondingContent.extra_info.commands,且是字符串形式,需要二次解析:
[{"name":"VOLUME_SET","params":[{"name":"series","normValue":"70","value":"70"}]}]4. 没有 AEC 的板子优先选 push2talk
tap2talk / duplex 都要求客户端持续上传音频,Listening 状态超过 20 秒不传就会报错;push2talk 只在按键期间上传,逻辑最简单,也不会出现"设备听见自己喇叭"的自激问题。
七、板端内存实测(Python 版)
在目标板(RV1106G3,Buildroot,Python 3.11.6)上实测了这套脚本的常驻内存。结论:Python 方案可用,但很吃内存。
模块可用性
os / sys / json / socket / struct / base64 / time / uuid / wave / audioop / ssl 全部可用(audioop 仅有「Python 3.13 将移除」的弃用警告),TLS 1.3 握手正常。也就是说同一套脚本原则上可以直接在板端运行,只需补根证书与凭据。
分阶段 VmRSS
| 阶段 | VmRSS |
|---|---|
| Python 解释器启动 | 5660 kB |
| 导入全部所需标准库后 | 9184 kB |
| 建立 TLS 连接后 | 10840 kB |
加上运行时缓冲
| 场景 | 额外缓冲 | 峰值 |
|---|---|---|
| 文本触发(累积 TTS 音频) | 约 48 KB/秒 → 30 秒约 1.4 MB | ≈ 12 MB |
| 语音识别(录音与回复都驻留内存) | 5 秒录音 160 KB + 回复 1.4 MB | ≈ 12.5 MB |
| 语音识别(录 30 秒) | 960 KB + 1.4 MB | ≈ 13.2 MB |
放进板子的内存预算
板子总量 45060 kB (约 44 MB)
移除 Samba 后可用 约 21860 kB (约 21 MB)
Python 客户端 约 11000 kB (约 11 MB) ← 占掉一半
剩余 约 10860 kB (约 10 MB)
与 C 实现的对比
| 项目 | Python 版 | C 版(预计) |
|---|---|---|
| 语言运行时 | 约 10.8 MB | 0 |
| TLS 库状态 | 已含在上面 | 约 0.3~0.8 MB(OpenSSL / mbedTLS) |
| 收发与音频缓冲 | 1~2.5 MB | 约 0.1~0.3 MB(流式,无需全量驻留) |
| 合计 | 约 11~13 MB | 约 0.5~1.5 MB |
结论:板端可用内存只有约 21 MB,Python 单进程就要占掉一半,之后还要给屏幕显示与 NPU 人脸检测留空间,因此正式实现仍走 C;Python 版保留作为板端排障工具——协议一致、无额外依赖,出问题时在板端原样跑一遍,就能快速区分是网络/协议问题还是 C 实现的问题。
八、结论与下一步
- 协议层面已验证完整:四条典型路径(文本触发 / 语音识别 / 打断 / 心跳)都有可运行的参考实现
- 下一步是用 C 重写这套逻辑并交叉编译到 RV1106G3 板端,解决板端的 TLS(OpenSSL 1.1.1 + 根证书)与 uClibc 问题
- 数据流规划:麦克风采集(16 kHz / 16 bit / 单声道)→ 上行;下行 PCM → 喇叭播放;
SpeechContent与RespondingContent→ 屏幕字幕
相关文章
评论
评论区由 giscus 提供(基于 GitHub Discussions),内容会保存在 GitHub 上;详见《隐私政策》。