被动交互开发指南 · iOS
被动交互开发指南 · iOS
被动交互是 SDK 最核心的能力:机器人发起请求(音频/视频/文本)→ SDK 回调处理 → 通过 Response 返回结果。
Swift 端 API 与 Java v1.5.0 一一对应,本页以 Swift 语法重写。跨语言共享的语义(打断时序、拒识、技能下发时机等)与 Java 被动交互指南 一致。
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 | 全模态语音回复 |
Flag 标志位详解
flag 是区分同一回调中不同数据类型的关键参数。
音频类回调(Audio2*、AudioVideo2*)
| flag | 含义 | audio / buf 内容 | 触发时机 |
|---|---|---|---|
0 | VAD 开始 | nil | 用户开始说话 |
1 | 音频帧 | PCM 音频数据(Data?) | 持续传输中 |
2 | VAD 结束 | nil | 用户停止说话 |
4 | H264 视频帧 | H264 编码数据 | 视频流传输 |
5 | 图片帧 | JPEG/PNG 图片 | 图片传输 |
ASR + 视频类回调(AsrVideo2*)
| flag | 含义 | 有效参数 | 触发时机 |
|---|---|---|---|
3 | ASR 文本 | text 有效,buf = nil | ASR 识别完成 |
4 | H264 视频帧 | buf 有效,text = nil | 视频流传输 |
5 | 图片帧 | buf 有效,text = nil | 图片传输 |
关键理解:
AsrVideo2*类的onRequest会被多次调用(分别传入文本和视频),需要根据flag判断本次传入的是什么数据。
开发模板
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 类 | 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 | 收到 text 即可分类 |
Audio2Llm / Audio2Tts / AudioVideo2Vlm / AudioVideo2Tts | SDK 侧从 flag=1 音频帧累积,flag=2(VAD 结束)收尾 | 拿到 final 文本(通常在 flag=2 分支 onAsrFinal 之后)再分类 |
典型流程(以 Asr2Llm 为例):
onRequest(text="今天天气怎么样")
│
▼
语义理解分类
├─ 拒识 → 直接 return,不调用任何 response.on* 方法
│
└─ 有效指令
① 必须先 response.onInterrupt(...)
② 再按命中结果回填 onSkill / onLlm* / onVlm* / onTts*
⚠️ 顺序强约束:一旦判定为有效指令,必须先调用
onInterrupt,之后才能调用onSkill/onLlm*/onVlm*/onTts*。唯一例外是拒识——拒识时一个都不调用。
示例:
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 |
realtime | realtime 打断 | 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(默认空实现,按需重载):
override func onFunctionCall(agentId: String, eventId: String,
functionCall: FunctionCallInfo, response: Asr2LlmResponse) {
// functionCall 描述了本次待处理的 function call
}
FunctionCallInfo 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
source | String? | function call 来源 |
policy | String? | 处理策略标识 |
type | String? | function call 类型 |
value | String? | 主体取值 |
param | [String: Any]? | 附带参数 |
回传仲裁决策(onArbiter)
通过对应 Response 的 onArbiter 回传,SDK 下发一条 agentsdk.function_call.arbiter:
response.onArbiter(eventId: eventId, decision: ArbiterDecision.delegate.decision, arbParam: nil)
ArbiterDecision(enum ArbiterDecision: String):
| 枚举 | wire 值 | 语义 |
|---|---|---|
.delegate | delegate | 委派:交由智能体默认链路处理 |
.override | override | 覆盖:由二开应用接管处理 |
常见仲裁分流(按 FunctionCallInfo.policy 区分本体技能 / 云侧技能):
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.arbiter | SDK → 网关 | 回传仲裁决策,由 onArbiter 发出 |
基类共享回调
PassiveCallback 基类定义了一组与具体语义模式无关的"通用回调",所有 8 种回调类自动继承,按需重载即可:
| 方法 | 触发时机 | 说明 |
|---|---|---|
onRobotOnline(agentId:agentMeta:) | 智能体上线 | agentId 是灵心平台配置的智能体 ID |
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:response:) | 打招呼信令 | 通过 response 应答 VLM 或 TTS |
onState(agentId:eventId:stateName:stateValue:) | 机器人状态变化 | stateValue 可能为 JSON 字符串;模块定义见 机器人端侧状态参考 |
onHistories(agentId:eventId:histories:) | 对话历史透传(v1.5.0 新增) | 网关下发当前会话历史清单。histories 为 [HistoryInfo] |
前提:陌生人打招呼需在灵心平台开启。否则不触发
onGreetSignal。
打招呼应答(GreetResponse)
onGreetSignal 收到信令后,可在 GreetResponse 上应答(两种模式不互斥):
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 |
|---|---|---|
2 | H264 视频帧 | H264 编码字节,isKeyFrame / localTs 有效 |
3 | 图片帧 | JPEG/PNG 字节,isKeyFrame=false、localTs=-1 |
重要注意事项
- 回调可复用:所有上线的智能体共享同一回调实例,用
agentId区分。 - 线程模型:每个
agentId绑定一个独立工作线程,同一智能体的请求按顺序处理。 - eventId 隔离:不同对话轮次通过
eventId隔离。 - 先用后初始化:必须先
register*再initialize(),顺序不能反。 - 基类回调按需重载:基类方法已有默认空实现(打日志),不重载也能正常运行。
- Response 超时清理:Response 对象在超时无更新后自动清理,长任务应及时调用 response 方法保持活跃。