被动交互开发指南 · iOS

被动交互开发指南 · iOS

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

Swift 端 API 与 Java v1.5.0 一一对应,本页以 Swift 语法重写。跨语言共享的语义(打断时序、拒识、技能下发时机等)与 Java 被动交互指南 一致。

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全模态语音回复

Flag 标志位详解

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

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

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

ASR + 视频类回调(AsrVideo2*)

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

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

开发模板

swift
agentSdk.registerAudio2Tts(Audio2TtsCallback(agentSdk: agentSdk) {})

// 或继承并重载:
final class MyAudio2Tts: Audio2TtsCallback {
    override func onRobotOnline(agentId: String, agentMeta: AgentMeta) {
        // 机器人上线:初始化该机器人资源
        // agentMeta 含唤醒词、城市等配置
        let wakeup = agentMeta.wakeupWord
    }

    override func onRobotOffline(agentId: String) {
        // 机器人下线:清理该机器人资源
    }

    override func onRequest(
        agentId: String, eventId: String, itemId: String,
        flag: Int, audio: Data?, param: AgentParam,
        response: Audio2TtsResponse
    ) {
        switch flag {
        case 0:
            // VAD 开始:初始化 ASR 会话
            break
        case 1:
            // 音频帧:发给 ASR 做流式识别
            sendToAsr(audio)
        case 2:
            // VAD 结束:完成 ASR → LLM → TTS
            let asrText = finalizeAsr()
            response.onAsrFinal(eventId: eventId, finalAsr: asrText)

            let llmReply = callLlm(asrText)
            response.onLlmItemDelta(eventId: eventId, itemId: itemId, textDelta: llmReply)
            response.onLlmItemDone(eventId: eventId, itemId: itemId)
            response.onLlmDone(eventId: eventId)

            let ttsAudio = synthesizeTts(llmReply)   // Data
            response.onTtsItemDelta(eventId: eventId, itemId: itemId, audio: ttsAudio)
            response.onTtsItemDone(eventId: eventId, itemId: itemId)
            response.onTtsDone(eventId: eventId)
        default:
            break
        }
    }
}
agentSdk.registerAudio2Tts(MyAudio2Tts(agentSdk: agentSdk))

Response 方法一览

所有 Response 类继承自 AgentSdkResponse,根据回调类型不同,可用的方法组合不同:

方法说明参数
onAsrMiddle(eventId:middleAsr:)ASR 中间结果(流式)middleAsr: 当前已识别文本
onAsrFinal(eventId:finalAsr:)ASR 最终结果finalAsr: 完整识别文本
onLlmItemDelta(eventId:itemId:textDelta:)LLM 流式输出textDelta: 本次增量文本
onLlmItemDone(eventId:itemId:)LLM 单段完成
onLlmDone(eventId:)LLM 全部完成
onVlmItemDelta(eventId:itemId:textDelta:)VLM 流式输出textDelta: 本次增量文本
onVlmItemDone(eventId:itemId:)VLM 单段完成
onVlmDone(eventId:)VLM 全部完成
onTtsItemDelta(eventId:itemId:audio:)TTS 音频片段audio: Data(PCM 字节)
onTtsItemDone(eventId:itemId:)TTS 单段完成
onTtsDone(eventId:)TTS 全部完成
onSkill(eventId:itemId:skillType:skillName:skillParam:)下发技能指令skillParam: AgentParam?,技能全集见 语义技能参考
onInterrupt(eventId:interruptType:interruptTips:)打断当前对话见 打断
onControl(eventId:controlType:controlParam:)下发控制指令controlParam: AgentParam?
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收到 text 即可分类
Audio2Llm / Audio2Tts / AudioVideo2Vlm / AudioVideo2TtsSDK 侧从 flag=1 音频帧累积,flag=2(VAD 结束)收尾拿到 final 文本(通常在 flag=2 分支 onAsrFinal 之后)再分类

典型流程(以 Asr2Llm 为例):

text
onRequest(text="今天天气怎么样")
      │
      ▼
语义理解分类
      ├─ 拒识 → 直接 return,不调用任何 response.on* 方法
      │
      └─ 有效指令
           ① 必须先 response.onInterrupt(...)
           ② 再按命中结果回填 onSkill / onLlm* / onVlm* / onTts*

⚠️ 顺序强约束:一旦判定为有效指令,必须先调用 onInterrupt,之后才能调用 onSkill / onLlm* / onVlm* / onTts*。唯一例外是拒识——拒识时一个都不调用。

示例:

swift
override func onRequest(agentId: String, eventId: String, text: String,
                        param: AgentParam, response: Asr2LlmResponse) {

    let nlu = classify(text)   // 业务侧语义理解

    // ① 拒识:直接 return
    if nlu.isRejected { return }

    let itemId = IdGenerator.generateItemId()

    // ② 有效指令——必须先打断
    if nlu.isRiskControl {
        response.onInterrupt(eventId: eventId, interruptType: "risk_control",
                             interruptTips: "你谈到敏感话题了,咱们换个话题聊吧")
        return
    } else if nlu.isSkill {
        response.onInterrupt(eventId: eventId, interruptType: "action", interruptTips: nil)
    } else {
        response.onInterrupt(eventId: eventId, interruptType: "chat", interruptTips: nil)
    }

    // ③ 命中技能:下发 skillParam
    if nlu.isSkill {
        let skillParam = AgentParam.create()
            .setString("movement", "move_forward")
            .setInt("meter", 10)
        response.onSkill(eventId: eventId, itemId: itemId,
                         skillType: "movement", skillName: "move_forward",
                         skillParam: skillParam)
    }

    // ④ LLM 流式回复
    response.onLlmItemDelta(eventId: eventId, itemId: itemId, textDelta: "好的,我开始向前走。")
    response.onLlmItemDone(eventId: eventId, itemId: itemId)
    response.onLlmDone(eventId: eventId)
}

打断(onInterrupt)

response.onInterrupt(eventId:interruptType:interruptTips:) 用于在新应答开始前 打断机器人当前正在进行的播报、动作或对话。

interruptType 取值

取值语义interruptTips
risk_control风控打断必填,给用户的话术
action技能打断nil
chat有效闲聊打断nil
realtimerealtime 打断nil

调用顺序(必须遵守)

语义理解结果第一步调用之后允许的应答
命中风控onInterrupt(..., "risk_control", "<话术>")一般不再返回 LLM/VLM/技能
命中技能onInterrupt(..., "action", nil)onSkill(...)
命中有效闲聊onInterrupt(..., "chat", nil)onLlm* / onVlm* / onTts*
realtime 对话onInterrupt(..., "realtime", nil)realtime 应答内容
拒识不调用一个都不调用

Function Call 开放与 Arbiter 仲裁(v1.5.0 新增)

⚠️ 尚未上线:本能力为 v1.5.0-SNAPSHOT 开发中特性,接口与协议可能调整,正式发布前请勿用于生产环境。

v1.5.0 在被动交互链路上开放 Function Call 分发 与 Arbiter 仲裁:网关把一个待处理的 function call 透传给 SDK(onFunctionCall),二开应用据此做出仲裁决策并通过 response.onArbiter(...) 回传。

接收 function call(onFunctionCall)

8 种被动回调类均新增 onFunctionCall(默认空实现,按需重载):

swift
override func onFunctionCall(agentId: String, eventId: String,
                             functionCall: FunctionCallInfo, response: Asr2LlmResponse) {
    // functionCall 描述了本次待处理的 function call
}

FunctionCallInfo 字段:

字段类型说明
sourceString?function call 来源
policyString?处理策略标识
typeString?function call 类型
valueString?主体取值
param[String: Any]?附带参数

回传仲裁决策(onArbiter)

通过对应 Response 的 onArbiter 回传,SDK 下发一条 agentsdk.function_call.arbiter:

swift
response.onArbiter(eventId: eventId, decision: ArbiterDecision.delegate.decision, arbParam: nil)

ArbiterDecision(enum ArbiterDecision: String):

枚举wire 值语义
.delegatedelegate委派:交由智能体默认链路处理
.overrideoverride覆盖:由二开应用接管处理

常见仲裁分流(按 FunctionCallInfo.policy 区分本体技能 / 云侧技能):

swift
override func onFunctionCall(agentId: String, eventId: String,
                             functionCall: FunctionCallInfo, response: Audio2TtsResponse) {
    if functionCall.policy == "body" {
        response.onArbiter(eventId: eventId, decision: ArbiterDecision.delegate.decision, arbParam: nil)
    } else if functionCall.policy == "cloud" {
        response.onArbiter(eventId: eventId, decision: ArbiterDecision.override.decision, arbParam: nil)
    }
}

事件族

事件类型字符串方向说明
agentsdk.function_call.expose网关 → SDK透传待仲裁 function call,触发 onFunctionCall
agentsdk.function_call.arbiterSDK → 网关回传仲裁决策,由 onArbiter 发出

基类共享回调

PassiveCallback 基类定义了一组与具体语义模式无关的"通用回调",所有 8 种回调类自动继承,按需重载即可:

方法触发时机说明
onRobotOnline(agentId:agentMeta:)智能体上线agentId 是灵心平台配置的智能体 ID
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:response:)打招呼信令通过 response 应答 VLM 或 TTS
onState(agentId:eventId:stateName:stateValue:)机器人状态变化stateValue 可能为 JSON 字符串;模块定义见 机器人端侧状态参考
onHistories(agentId:eventId:histories:)对话历史透传(v1.5.0 新增)网关下发当前会话历史清单。histories 为 [HistoryInfo]

前提:陌生人打招呼需在灵心平台开启。否则不触发 onGreetSignal。

打招呼应答(GreetResponse)

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

swift
override func onGreetSignal(agentId: String, eventId: String,
                            param: AgentParam, response: GreetResponse) {
    // 模式一:VLM 文本流式应答
    response.onGreetVlmDelta(eventId: eventId, textDelta: "你好")
    response.onGreetVlmDone(eventId: eventId)

    // 模式二:TTS 音频流式应答(textDelta 是 base64 编码的音频字符串片段)
    response.onGreetTtsDelta(eventId: eventId, textDelta: base64Chunk1)
    response.onGreetTtsDone(eventId: eventId)

    // response.onError(eventId: eventId, errorCode: 5101, errorMsg: "打招呼生成失败")
}
方法说明
onGreetVlmDelta(eventId:textDelta:)VLM 流式文本片段
onGreetVlmDone(eventId:)VLM 流式结束
onGreetTtsDelta(eventId:textDelta:)TTS 音频流式片段——textDelta 是 base64 编码的字符串
onGreetTtsDone(eventId:)TTS 音频输出结束
onError(eventId:errorCode:errorMsg:)应答异常

视频帧透传

flag含义buf
2H264 视频帧H264 编码字节,isKeyFrame / localTs 有效
3图片帧JPEG/PNG 字节,isKeyFrame=false、localTs=-1

重要注意事项

  1. 回调可复用:所有上线的智能体共享同一回调实例,用 agentId 区分。
  2. 线程模型:每个 agentId 绑定一个独立工作线程,同一智能体的请求按顺序处理。
  3. eventId 隔离:不同对话轮次通过 eventId 隔离。
  4. 先用后初始化:必须先 register* 再 initialize(),顺序不能反。
  5. 基类回调按需重载:基类方法已有默认空实现(打日志),不重载也能正常运行。
  6. Response 超时清理:Response 对象在超时无更新后自动清理,长任务应及时调用 response 方法保持活跃。