被动交互开发指南

被动交互开发指南

被动交互是 SDK 最核心的功能:机器人发起请求(音频/视频/文本)→ SDK回调处理 → 通过Response返回结果

8 种被动回调类型

根据输入输出组合选择合适的回调类型。每个 SDK 实例只能注册一种被动回调。

纯音频类

回调类输入输出典型场景
Audio2LlmCallback音频流ASR + LLM文本语音对话,文本回复
Audio2TtsCallback音频流ASR + LLM + TTS音频语音对话,语音回复

纯文本类

回调类输入输出典型场景
Asr2LlmCallbackASR文本LLM文本文本对话
Asr2TtsCallbackASR文本LLM + TTS音频文本转语音回复

多模态类(音频+视频 / 文本+视频)

回调类输入输出典型场景
AsrVideo2VlmCallbackASR文本 + 视频VLM文本看图理解
AsrVideo2TtsCallbackASR文本 + 视频VLM + TTS音频看图语音回复
AudioVideo2VlmCallback音频 + 视频ASR + VLM文本全模态理解
AudioVideo2TtsCallback音频 + 视频ASR + VLM + TTS全模态语音回复

v1.4.0Audio2Asr / Audio2Audio / AudioVideo2Audio / Text2Tts 四种回调及其 Response 类已从 SDK 中删除。

Flag 标志位详解

flag 是区分同一回调中不同数据类型的关键参数。

音频类回调 (Audio2*, AudioVideo2*)

flag含义buf 内容触发时机
0VAD开始null用户开始说话
1音频帧PCM音频数据持续传输中
2VAD结束null用户停止说话
4H264视频帧H264编码数据视频流传输
5图片帧JPEG/PNG图片图片传输

ASR+视频类回调 (AsrVideo2*)

flag含义有效参数触发时机
3ASR文本text 有效, buf=nullASR识别完成
4H264视频帧buf 有效, text=null视频流传输
5图片帧buf 有效, text=null图片传输

关键理解AsrVideo2* 类的 onRequest 会被多次调用(分别传入文本和视频),需要根据 flag 判断本次传入的是什么数据。

开发模板

java
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 / realtimetipsrisk_control 必填,详见 打断
onError(eventId, errorCode, errorMsg)返回错误所有Response均支持

各 Response 可用方法矩阵

Response 类ASRLLMVLMTTSSkillInterrupt
Audio2LlmResponse
Audio2TtsResponse
Asr2LlmResponse
Asr2TtsResponse
AsrVideo2VlmResponse
AsrVideo2TtsResponse
AudioVideo2VlmResponse
AudioVideo2TtsResponse

标准响应流程

text
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 / AudioVideo2TtsSDK 侧从 flag=1 的音频帧累积,在 flag=2(VAD 结束)时收尾出 final 文本拿到 final 文本(通常在 flag=2 分支调用 response.onAsrFinal 之后)再分类

典型流程(以 Asr2Llm 为例):

text
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 分支里做同样的语义理解分类):

java
@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 / skillNameskillParam 的取值见 语义技能参考。 同一 eventId 下可多次调用 onSkill(多意图场景),每个意图建议用独立 itemId 标识。

打断(onInterrupt)

response.onInterrupt(eventId, interruptType, interruptTips) 用于在新的应答开始前 打断机器人当前正在进行的播报、动作或对话——再开始新的 TTS / 技能 / 对话内容。

interruptType 取值

取值语义场景interruptTips
risk_control风控打断内容违规、敏感话题等,必须立即终止当前对话必填,例如 "你谈到敏感话题了,咱们换个话题聊吧"
action技能打断语义理解命中技能 / 动作,准备执行新动作前打断当前动作或播报null
chat有效闲聊打断语义理解命中普通对话(有效闲聊),开始新一轮 LLM/VLM 文本回复时打断旧回复null
realtimerealtime 打断实时(边听边说)对话场景下,新一段实时回复打断旧的null

interruptTips 是给用户的文案提示。仅 risk_control 必须填写——风控打断时机器人会用这句提示反馈给用户; 其他三种类型按约定固定传 null

调用顺序(必须遵守)

⚠️ 任何"有效指令"的应答都必须按下面的顺序进行,否则机器人无法正确进入新一轮应答:

  1. response.onInterrupt(eventId, type, tips)
  2. 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一个都不调用——保持机器人当前状态

示例

java
// 普通对话回复
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)识别到人脸 / 声纹 UIDparam 内含人脸或声纹特征字段
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 变更:原独立的 Greet2VlmCallbackStateListenCallback 已合并到 PassiveCallback,无需再单独 register*, 在你的业务回调(如 Audio2TtsCallback)中重载这几个方法即可。

打招呼应答(GreetResponse)

onGreetSignal 收到信令后,可在 GreetResponse 上以两种模式应答(不互斥,按需选择):

java
@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
2H264 视频帧H264 编码字节,isKeyFrame/localTs 有效
3图片帧JPEG/PNG 字节,isKeyFrame=falselocalTs=-1

重要注意事项

  1. 回调是单例:所有上线的智能体共享同一回调实例,用 agentId(灵心平台配置的智能体 ID)区分
  2. 线程模型:每个 agentId 绑定一个独立线程,同一智能体的请求按顺序执行
  3. eventId 隔离:不同对话轮次通过 eventId 隔离,非线程安全资源应基于 eventId 管理
  4. Response 线程安全response.on*() 方法可在任意线程调用
  5. Response 超时清理:Response 对象在 120 秒无更新后自动清理,长任务需及时调用 response 方法保持活跃
  6. 先注册后初始化:必须先 register*Callback()initialize(),顺序不能反
  7. 基类回调按需重载onGreetSignal / onState / onFaceInfo / onVideoFramePassiveCallback 中已有默认实现(仅打日志),不重载也能正常运行