被动交互开发指南
被动交互开发指南
被动交互是 SDK 最核心的功能:机器人发起请求(音频/视频/文本)→ SDK回调处理 → 通过Response返回结果。
8 种被动回调类型
根据输入输出组合选择合适的回调类型。每个 SDK 实例只能注册一种被动回调。
纯音频类
| 回调类 | 输入 | 输出 | 典型场景 |
|---|---|---|---|
Audio2LlmCallback | 音频流 | ASR + LLM文本 | 语音对话,文本回复 |
Audio2TtsCallback | 音频流 | ASR + LLM + TTS音频 | 语音对话,语音回复 |
纯文本类
| 回调类 | 输入 | 输出 | 典型场景 |
|---|---|---|---|
Asr2LlmCallback | ASR文本 | LLM文本 | 文本对话 |
Asr2TtsCallback | ASR文本 | LLM + TTS音频 | 文本转语音回复 |
多模态类(音频+视频 / 文本+视频)
| 回调类 | 输入 | 输出 | 典型场景 |
|---|---|---|---|
AsrVideo2VlmCallback | ASR文本 + 视频 | VLM文本 | 看图理解 |
AsrVideo2TtsCallback | ASR文本 + 视频 | VLM + TTS音频 | 看图语音回复 |
AudioVideo2VlmCallback | 音频 + 视频 | ASR + VLM文本 | 全模态理解 |
AudioVideo2TtsCallback | 音频 + 视频 | ASR + VLM + TTS | 全模态语音回复 |
v1.4.0:
Audio2Asr/Audio2Audio/AudioVideo2Audio/Text2Tts四种回调及其 Response 类已从 SDK 中删除。
Flag 标志位详解
flag 是区分同一回调中不同数据类型的关键参数。
音频类回调 (Audio2*, AudioVideo2*)
| flag | 含义 | buf 内容 | 触发时机 |
|---|---|---|---|
0 | VAD开始 | null | 用户开始说话 |
1 | 音频帧 | PCM音频数据 | 持续传输中 |
2 | VAD结束 | null | 用户停止说话 |
4 | H264视频帧 | H264编码数据 | 视频流传输 |
5 | 图片帧 | JPEG/PNG图片 | 图片传输 |
ASR+视频类回调 (AsrVideo2*)
| flag | 含义 | 有效参数 | 触发时机 |
|---|---|---|---|
3 | ASR文本 | text 有效, buf=null | ASR识别完成 |
4 | H264视频帧 | buf 有效, text=null | 视频流传输 |
5 | 图片帧 | buf 有效, text=null | 图片传输 |
关键理解:
AsrVideo2*类的onRequest会被多次调用(分别传入文本和视频),需要根据flag判断本次传入的是什么数据。
开发模板
agentSdk.registerAudio2Tts(new Audio2TtsCallback(agentSdk) {
@Override
public void onRobotOnline(String agentId, AgentMeta agentMeta) {
// 机器人上线,初始化该机器人的资源
// agentMeta 包含唤醒词、城市等配置信息
String wakeupWord = agentMeta.getWakeupWord();
}
@Override
public void onRobotOffline(String agentId) {
// 机器人下线,清理该机器人相关的资源
}
@Override
public void onRequest(String agentId, String eventId, String itemId,
int flag, byte[] audio, AgentParam param,
Audio2TtsResponse response) {
switch (flag) {
case 0 -> {
// VAD开始:可以初始化ASR会话
}
case 1 -> {
// 音频帧:发送到ASR服务进行流式识别
sendToAsr(audio);
}
case 2 -> {
// VAD结束:完成ASR,调用LLM,合成TTS
String asrText = finalizeAsr();
response.onAsrFinal(eventId, asrText);
// 调用LLM获取回复
String llmReply = callLlm(asrText);
response.onLlmItemDelta(eventId, itemId, llmReply);
response.onLlmItemDone(eventId, itemId);
response.onLlmDone(eventId);
// 合成TTS音频
byte[] ttsAudio = synthesizeTts(llmReply);
response.onTtsItemDelta(eventId, itemId, ttsAudio);
response.onTtsItemDone(eventId, itemId);
response.onTtsDone(eventId);
}
}
}
});
Response 方法一览
所有 Response 类继承自 AgentSdkResponse,根据回调类型不同,可用的方法组合不同:
| 方法 | 说明 | 参数 |
|---|---|---|
onAsrMiddle(eventId, text) | ASR中间结果(流式) | text: 当前已识别文本 |
onAsrFinal(eventId, text) | ASR最终结果 | text: 完整识别文本 |
onLlmItemDelta(eventId, itemId, text) | LLM流式输出 | text: 本次增量文本 |
onLlmItemDone(eventId, itemId) | LLM单段完成 | |
onLlmDone(eventId) | LLM全部完成 | |
onVlmItemDelta(eventId, itemId, text) | VLM流式输出 | text: 本次增量文本 |
onVlmItemDone(eventId, itemId) | VLM单段完成 | |
onVlmDone(eventId) | VLM全部完成 | |
onTtsItemDelta(eventId, itemId, audio) | TTS音频片段 | audio: PCM字节数组 |
onTtsItemDone(eventId, itemId) | TTS单段完成 | |
onTtsDone(eventId) | TTS全部完成 | |
onSkill(eventId, itemId, skillType, skillName, param) | 下发技能指令 | param: AgentParam,可下发的技能全集见 语义技能参考 |
onInterrupt(eventId, type, tips) | 打断当前对话 | type 取值:risk_control / action / chat / realtime;tips 仅 risk_control 必填,详见 打断 |
onError(eventId, errorCode, errorMsg) | 返回错误 | 所有Response均支持 |
各 Response 可用方法矩阵
| Response 类 | ASR | LLM | VLM | TTS | Skill | Interrupt |
|---|---|---|---|---|---|---|
Audio2LlmResponse | ✓ | ✓ | ✓ | ✓ | ||
Audio2TtsResponse | ✓ | ✓ | ✓ | ✓ | ✓ | |
Asr2LlmResponse | ✓ | ✓ | ✓ | |||
Asr2TtsResponse | ✓ | ✓ | ✓ | ✓ | ||
AsrVideo2VlmResponse | ✓ | ✓ | ✓ | |||
AsrVideo2TtsResponse | ✓ | ✓ | ✓ | ✓ | ||
AudioVideo2VlmResponse | ✓ | ✓ | ✓ | ✓ | ||
AudioVideo2TtsResponse | ✓ | ✓ | ✓ | ✓ | ✓ |
标准响应流程
onRequest(flag=0) → 初始化
onRequest(flag=1) → 累积音频 → ASR流式识别
└→ onAsrMiddle() (可选,中间结果)
onRequest(flag=2) → ASR完成 → onAsrFinal()
→ LLM调用 → onLlmItemDelta() × N
→ onLlmItemDone()
→ onLlmDone()
→ TTS合成 → onTtsItemDelta() × N
→ onTtsItemDone()
→ onTtsDone()
技能返回时机
触发场景: 拿到一段 ASR 文本后,业务侧做语义理解将其分类,
如果分类结果属于"技能"(如动作、移动、表情等),就通过 response.onSkill(...) 把技能下发给机器人执行。
否则只走普通的 LLM/VLM 文本回复链路,不调用 onSkill。
ASR 文本的来源分两种:
| 回调类型 | 文本来源 | 何时分类 |
|---|---|---|
Asr2Llm / Asr2Tts / AsrVideo2Vlm / AsrVideo2Tts | 上游(机器人侧)已完成 ASR,直接通过 onRequest(... text ...) 把 final 文本送进来 | 收到 text 即可分类 |
Audio2Llm / Audio2Tts / AudioVideo2Vlm / AudioVideo2Tts | SDK 侧从 flag=1 的音频帧累积,在 flag=2(VAD 结束)时收尾出 final 文本 | 拿到 final 文本(通常在 flag=2 分支调用 response.onAsrFinal 之后)再分类 |
典型流程(以 Asr2Llm 为例):
onRequest(text="今天天气怎么样")
│
▼
语义理解分类
├─ 拒识(无效查询 / 误唤醒 / 噪音等)
│ └→ 直接 return,不调用任何 response.on* 方法
│ (不返回技能、不打断、不返回 LLM/VLM 任何结果)
│
└─ 有效指令
① 必须先 response.onInterrupt(eventId, type, tips)
│ (type 见下文「打断」小节:风控 / 技能 / 有效闲聊 / realtime)
│
② 再按命中结果回填:
├─ 命中技能 → response.onSkill(eventId, itemId, ...)
├─ 命中有效闲聊 → response.onLlmItemDelta / onLlmItemDone / onLlmDone
├─ 命中多模态 → response.onVlmItemDelta / onVlmItemDone / onVlmDone
└─ TTS 应答(如适用)→ response.onTtsItemDelta / onTtsItemDone / onTtsDone
⚠️ 顺序强约束:一旦判定为有效指令,必须先调用
onInterrupt, 之后才能调用onSkill/onLlm*/onVlm*/onTts*。 调换顺序会导致机器人不能正确进入新一轮应答状态。 唯一例外是拒识——拒识时一个都不调用,让机器人继续保持当前状态。
示例(以 Asr2Llm 为例,Audio2Tts 也一样,只是在 flag=2 分支里做同样的语义理解分类):
@Override
public void onRequest(String agentId, String eventId, String text,
AgentParam param, Asr2LlmResponse response) {
// 业务侧语义理解:把 text 分类成 "技能 / 闲聊 / realtime / 风控 / 拒识"
NluResult nlu = classify(text);
// ① 拒识:直接 return,不调用任何 response.on* 方法
if (nlu.isRejected()) {
return;
}
String itemId = IdGenerator.generateItemId();
// ② 有效指令——必须先打断,再回填技能 / LLM / VLM / TTS 结果
if (nlu.isRiskControl()) {
// 风控:interruptTips 必填
response.onInterrupt(eventId, "risk_control",
"你谈到敏感话题了,咱们换个话题聊吧");
return; // 风控场景下后续通常不再返回 LLM/技能
} else if (nlu.isSkill()) {
response.onInterrupt(eventId, "action", null); // 技能打断
} else {
response.onInterrupt(eventId, "chat", null); // 有效闲聊打断
// 若是 realtime 对话:response.onInterrupt(eventId, "realtime", null);
}
// ③ 命中技能:下发 skillParam(key/value 见《语义技能参考》)
if (nlu.isSkill()) {
AgentParam skillParam = AgentParam.create()
.setString("movement", "move_forward")
.setInteger("meter", 10);
response.onSkill(eventId, itemId, "movement", "move_forward", skillParam);
}
// ④ LLM 流式回复(与技能并行,互不阻塞)
response.onLlmItemDelta(eventId, itemId, "好的,我开始向前走。");
response.onLlmItemDone(eventId, itemId);
response.onLlmDone(eventId);
}
skillType/skillName与skillParam的取值见 语义技能参考。 同一eventId下可多次调用onSkill(多意图场景),每个意图建议用独立itemId标识。
打断(onInterrupt)
response.onInterrupt(eventId, interruptType, interruptTips) 用于在新的应答开始前
打断机器人当前正在进行的播报、动作或对话——再开始新的 TTS / 技能 / 对话内容。
interruptType 取值
| 取值 | 语义 | 场景 | interruptTips |
|---|---|---|---|
risk_control | 风控打断 | 内容违规、敏感话题等,必须立即终止当前对话 | 必填,例如 "你谈到敏感话题了,咱们换个话题聊吧" |
action | 技能打断 | 语义理解命中技能 / 动作,准备执行新动作前打断当前动作或播报 | 传 null |
chat | 有效闲聊打断 | 语义理解命中普通对话(有效闲聊),开始新一轮 LLM/VLM 文本回复时打断旧回复 | 传 null |
realtime | realtime 打断 | 实时(边听边说)对话场景下,新一段实时回复打断旧的 | 传 null |
interruptTips是给用户的文案提示。仅risk_control必须填写——风控打断时机器人会用这句提示反馈给用户; 其他三种类型按约定固定传null。
调用顺序(必须遵守)
⚠️ 任何"有效指令"的应答都必须按下面的顺序进行,否则机器人无法正确进入新一轮应答:
response.onInterrupt(eventId, type, tips)response.onSkill(...)与 / 或response.onLlmItem* / onVlmItem* / onTtsItem*
按语义理解分类结果选择 interruptType:
| 语义理解结果 | 第一步调用 | 之后允许的应答 |
|---|---|---|
| 命中风控 | onInterrupt(..., "risk_control", "<给用户的话术>") | 一般不再返回 LLM/VLM/技能(按业务定) |
| 命中技能 | onInterrupt(..., "action", null) | onSkill(...)(可并行 onLlm* / onVlm* / onTts*) |
| 命中有效闲聊 | onInterrupt(..., "chat", null) | onLlmItem* / onVlmItem* / onTtsItem* |
| realtime 对话 | onInterrupt(..., "realtime", null) | realtime 应答内容 |
| 拒识 | 不调用 onInterrupt | 一个都不调用——保持机器人当前状态 |
示例
// 普通对话回复
response.onInterrupt(eventId, "chat", null);
response.onLlmItemDelta(eventId, itemId, "好的,我来回答你的问题。");
// 命中动作技能
response.onInterrupt(eventId, "action", null);
AgentParam skillParam = AgentParam.create().setString("action", "wave_hands");
response.onSkill(eventId, itemId, "action", "wave_hands", skillParam);
// 实时对话边听边说
response.onInterrupt(eventId, "realtime", null);
response.onLlmItemDelta(eventId, itemId, "嗯嗯,我听到了……");
// 风控命中(interruptTips 必填,作为给用户的话术)
response.onInterrupt(eventId, "risk_control", "你谈到敏感话题了,咱们换个话题聊吧");
基类共享回调(v1.4.0+)
PassiveCallback 基类定义了一组与具体语义模式无关的"通用回调",所有 8 种回调类(Audio2Tts*、Asr2Llm* 等)均自动继承,按需重载即可:
| 方法 | 触发时机 | 说明 |
|---|---|---|
onRobotOnline(agentId, agentMeta) | 智能体上线 | agentId 是灵心平台配置的智能体 ID;agentMeta 含端侧设备元数据(唤醒词、城市等) |
onRobotOffline(agentId) | 智能体下线 | 清理该 agentId 关联的资源 |
onFaceInfo(agentId, eventId, param) | 识别到人脸 / 声纹 UID | param 内含人脸或声纹特征字段 |
onVideoFrame(agentId, eventId, flag, buf, isKeyFrame, localTs, param) | 透传视频流 | flag=2 H264,flag=3 图片 |
onGreetSignal(agentId, eventId, param, GreetResponse response) | 打招呼信令 | 通过 response 应答 VLM 或 TTS |
onState(agentId, eventId, stateName, stateValue) | 机器人状态变化 | stateValue 可能为 JSON 字符串;所有 stateName 模块定义见 机器人端侧状态参考 |
v1.4.0 变更:原独立的
Greet2VlmCallback、StateListenCallback已合并到PassiveCallback,无需再单独register*, 在你的业务回调(如Audio2TtsCallback)中重载这几个方法即可。
打招呼应答(GreetResponse)
onGreetSignal 收到信令后,可在 GreetResponse 上以两种模式应答(不互斥,按需选择):
@Override
public void onGreetSignal(String agentId, String eventId, AgentParam param, GreetResponse response) {
// 模式一:VLM 文本流式应答
response.onGreetVlmDelta(eventId, "你好");
response.onGreetVlmDelta(eventId, ",今天天气真不错");
response.onGreetVlmDone(eventId);
// 模式二:TTS 音频流式应答(参数是合成好的音频字节经 base64 编码后的字符串片段)
byte[] audioChunk1 = synthesizeTts("你好"); // PCM / Opus 等 bytes
byte[] audioChunk2 = synthesizeTts(",欢迎回家");
response.onGreetTtsDelta(eventId, Base64.getEncoder().encodeToString(audioChunk1));
response.onGreetTtsDelta(eventId, Base64.getEncoder().encodeToString(audioChunk2));
response.onGreetTtsDone(eventId);
// 异常处理
// response.onError(eventId, 5101, "打招呼生成失败");
}
| 方法 | 说明 |
|---|---|
onGreetVlmDelta(eventId, text) | VLM 流式文本片段 |
onGreetVlmDone(eventId) | VLM 流式输出结束 |
onGreetTtsDelta(eventId, audioBase64) | TTS 音频流式片段——audioBase64 是合成音频字节经 base64 编码后的字符串 |
onGreetTtsDone(eventId) | TTS 音频输出结束 |
onError(eventId, code, msg) | 应答异常 |
视频帧透传
onVideoFrame 既会收到 Asr2*/AudioVideo2* 等需要视频的回调对应数据,也用于多模态以外的纯视频透传场景:
| flag | 含义 | buf |
|---|---|---|
2 | H264 视频帧 | H264 编码字节,isKeyFrame/localTs 有效 |
3 | 图片帧 | JPEG/PNG 字节,isKeyFrame=false、localTs=-1 |
重要注意事项
- 回调是单例:所有上线的智能体共享同一回调实例,用
agentId(灵心平台配置的智能体 ID)区分 - 线程模型:每个
agentId绑定一个独立线程,同一智能体的请求按顺序执行 - eventId 隔离:不同对话轮次通过
eventId隔离,非线程安全资源应基于eventId管理 - Response 线程安全:
response.on*()方法可在任意线程调用 - Response 超时清理:Response 对象在 120 秒无更新后自动清理,长任务需及时调用 response 方法保持活跃
- 先注册后初始化:必须先
register*Callback()再initialize(),顺序不能反 - 基类回调按需重载:
onGreetSignal/onState/onFaceInfo/onVideoFrame在PassiveCallback中已有默认实现(仅打日志),不重载也能正常运行