被动交互开发指南
被动交互开发指南
被动交互是 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 开始 | None | 用户开始说话 |
1 | 音频帧 | PCM 音频数据(bytes) | 持续传输中 |
2 | VAD 结束 | None | 用户停止说话 |
3 | H264 视频帧 | H264 字节(AudioVideo2* 才出现) | 视频流传输 |
ASR+视频类回调 (AsrVideo2*)
| flag | 含义 | 有效参数 | 触发时机 |
|---|---|---|---|
3 | ASR 文本 | text 有效,buf=None | ASR 识别完成 |
4 | H264 视频帧 | buf 有效,text=None | 视频流传输 |
5 | 图片帧 | buf 有效,text=None | 图片传输 |
关键理解:
AsrVideo2*类的on_request会被多次调用(分别传入文本和视频),需要根据flag判断本次传入的是什么数据。
开发模板
from linksoul_agentsdk import AgentMeta, AgentParam, AgentSdk
from linksoul_agentsdk.passive import Audio2TtsCallback, Audio2TtsResponse
class MyCallback(Audio2TtsCallback):
def on_robot_online(self, agent_id: str, agent_meta: AgentMeta) -> None:
# 机器人上线,初始化该机器人的资源
print(f"on_robot_online => {agent_id}, wakeup={agent_meta.get_wakeup_word()}")
def on_robot_offline(self, agent_id: str) -> None:
# 机器人下线,清理该机器人相关的资源
print(f"on_robot_offline => {agent_id}")
def on_request(
self,
agent_id: str,
event_id: str,
item_id: str,
flag: int,
buf: bytes | None,
param: AgentParam,
response: Audio2TtsResponse,
) -> None:
if flag == 0:
# VAD 开始:可以初始化 ASR 会话
init_asr_session(agent_id, event_id)
elif flag == 1:
# 音频帧:发送到 ASR 服务进行流式识别
feed_asr(agent_id, event_id, buf)
elif flag == 2:
# VAD 结束:完成 ASR,调用 LLM,合成 TTS
asr_text = finalize_asr(agent_id, event_id)
response.on_asr_final(event_id, asr_text)
# 有效指令必须先打断
response.on_interrupt(event_id, "chat", None)
# 调用 LLM 获取回复
llm_reply = call_llm(asr_text)
response.on_llm_item_delta(event_id, item_id, llm_reply)
response.on_llm_item_done(event_id, item_id)
response.on_llm_done(event_id)
# 合成 TTS 音频
tts_audio = synthesize_tts(llm_reply)
response.on_tts_item_delta(event_id, item_id, tts_audio)
response.on_tts_item_done(event_id, item_id)
response.on_tts_done(event_id)
agent_sdk = AgentSdk.create(url, app_id, app_key, app_secret)
agent_sdk.register_audio2_tts(MyCallback(agent_sdk))
agent_sdk.initialize()
Response 方法一览
所有 Response 类都继承自 AgentSdkResponse,根据回调类型不同,可用的方法组合不同:
| 方法 | 说明 | 参数 |
|---|---|---|
on_asr_middle(event_id, text) | ASR 中间结果(流式) | text: 当前已识别文本 |
on_asr_final(event_id, text) | ASR 最终结果 | text: 完整识别文本 |
on_llm_item_delta(event_id, item_id, text) | LLM 流式输出 | text: 本次增量文本 |
on_llm_item_done(event_id, item_id) | LLM 单段完成 | |
on_llm_done(event_id) | LLM 全部完成 | |
on_vlm_item_delta(event_id, item_id, text) | VLM 流式输出 | text: 本次增量文本 |
on_vlm_item_done(event_id, item_id) | VLM 单段完成 | |
on_vlm_done(event_id) | VLM 全部完成 | |
on_tts_item_delta(event_id, item_id, audio) | TTS 音频片段 | audio: bytes |
on_tts_item_done(event_id, item_id) | TTS 单段完成 | |
on_tts_done(event_id) | TTS 全部完成 | |
on_skill(event_id, item_id, skill_type, skill_name, param) | 下发技能指令 | param: AgentParam,可下发的技能全集见 语义技能参考 |
on_interrupt(event_id, type, tips) | 打断当前对话 | type 取值:risk_control / action / chat / realtime;tips 仅 risk_control 必填,详见 打断 |
on_error(event_id, error_code, error_msg) | 返回错误 | 所有 Response 均支持 |
各 Response 可用方法矩阵
| Response 类 | ASR | LLM | VLM | TTS | Skill | Interrupt |
|---|---|---|---|---|---|---|
Audio2LlmResponse | ✓ | ✓ | ✓ | ✓ | ||
Audio2TtsResponse | ✓ | ✓ | ✓ | ✓ | ✓ | |
Asr2LlmResponse | ✓ | ✓ | ✓ | |||
Asr2TtsResponse | ✓ | ✓ | ✓ | ✓ | ||
AsrVideo2VlmResponse | ✓ | ✓ | ✓ | |||
AsrVideo2TtsResponse | ✓ | ✓ | ✓ | ✓ | ||
AudioVideo2VlmResponse | ✓ | ✓ | ✓ | ✓ | ||
AudioVideo2TtsResponse | ✓ | ✓ | ✓ | ✓ | ✓ |
标准响应流程
on_request(flag=0) → 初始化
on_request(flag=1) → 累积音频 → ASR 流式识别
└→ on_asr_middle()(可选,中间结果)
on_request(flag=2) → ASR 完成 → on_asr_final()
→ 语义理解分类
├─ 拒识 → return(不调用任何 response.on_*)
└─ 有效指令
① on_interrupt(...)(必须先于 ②)
② 按需 on_skill / on_llm_* / on_vlm_* / on_tts_*
技能返回时机
触发场景: 拿到一段 ASR 文本后,业务侧做语义理解将其分类,
如果分类结果属于"技能"(如动作、移动、表情等),就通过 response.on_skill(...) 把技能下发给机器人执行。
否则只走普通的 LLM/VLM 文本回复链路,不调用 on_skill。
ASR 文本的来源分两种:
| 回调类型 | 文本来源 | 何时分类 |
|---|---|---|
Asr2Llm / Asr2Tts / AsrVideo2Vlm / AsrVideo2Tts | 上游(机器人侧)已完成 ASR,直接通过 on_request(... text ...) 把 final 文本送进来 | 收到 text 即可分类 |
Audio2Llm / Audio2Tts / AudioVideo2Vlm / AudioVideo2Tts | SDK 侧从 flag=1 的音频帧累积,在 flag=2(VAD 结束)时收尾出 final 文本 | 拿到 final 文本(通常在 flag=2 分支调用 response.on_asr_final 之后)再分类 |
典型流程(以 Asr2Llm 为例):
on_request(text="今天天气怎么样")
│
▼
语义理解分类
├─ 拒识(无效查询 / 误唤醒 / 噪音等)
│ └→ 直接 return,不调用任何 response.on_* 方法
│
└─ 有效指令
① 必须先 response.on_interrupt(event_id, type, tips)
│ (type 见下文「打断」小节:风控 / 技能 / 有效闲聊 / realtime)
│
② 再按命中结果回填:
├─ 命中技能 → response.on_skill(event_id, item_id, ...)
├─ 命中有效闲聊 → response.on_llm_item_delta / on_llm_item_done / on_llm_done
├─ 命中多模态 → response.on_vlm_item_delta / on_vlm_item_done / on_vlm_done
└─ TTS 应答(如适用) → response.on_tts_item_delta / on_tts_item_done / on_tts_done
⚠️ 顺序强约束:一旦判定为有效指令,必须先调用
on_interrupt, 之后才能调用on_skill/on_llm_*/on_vlm_*/on_tts_*。 调换顺序会导致机器人不能正确进入新一轮应答状态。 唯一例外是拒识——拒识时一个都不调用,让机器人继续保持当前状态。
示例:
def on_request(self, agent_id, event_id, text, param, response):
nlu = classify(text)
# ① 拒识:直接 return
if nlu.is_rejected():
return
item_id = IdGenerator.generate_item_id()
# ② 有效指令必须先打断
if nlu.is_risk_control():
# 风控:interrupt_tips 必填
response.on_interrupt(
event_id, "risk_control",
"你谈到敏感话题了,咱们换个话题聊吧",
)
return # 风控场景下后续通常不再返回 LLM/技能
elif nlu.is_skill():
response.on_interrupt(event_id, "action", None) # 技能打断
else:
response.on_interrupt(event_id, "chat", None) # 有效闲聊打断
# 若是 realtime 对话:response.on_interrupt(event_id, "realtime", None)
# ③ 命中技能:下发 skill_param(key/value 见《语义技能参考》)
if nlu.is_skill():
skill_param = (
AgentParam.create()
.set_string("movement", "move_forward")
.set_integer("meter", 10)
)
response.on_skill(event_id, item_id, "movement", "move_forward", skill_param)
# ④ LLM 流式回复
response.on_llm_item_delta(event_id, item_id, "好的,我开始向前走。")
response.on_llm_item_done(event_id, item_id)
response.on_llm_done(event_id)
skill_type/skill_name与skill_param的取值见 语义技能参考。 同一event_id下可多次调用on_skill(多意图场景),每个意图建议用独立item_id标识。
打断 (on_interrupt)
response.on_interrupt(event_id, interrupt_type, interrupt_tips) 用于在新的应答开始前
打断机器人当前正在进行的播报、动作或对话——再开始新的 TTS / 技能 / 对话内容。
interrupt_type 取值
| 取值 | 语义 | 场景 | interrupt_tips |
|---|---|---|---|
risk_control | 风控打断 | 内容违规、敏感话题等,必须立即终止当前对话 | 必填,例如 "你谈到敏感话题了,咱们换个话题聊吧" |
action | 技能打断 | 语义理解命中技能 / 动作,准备执行新动作前打断当前动作或播报 | 传 None |
chat | 有效闲聊打断 | 语义理解命中普通对话(有效闲聊),开始新一轮 LLM/VLM 文本回复时打断旧回复 | 传 None |
realtime | realtime 打断 | 实时(边听边说)对话场景下,新一段实时回复打断旧的 | 传 None |
interrupt_tips是给用户的文案提示。仅risk_control必须填写——风控打断时机器人会用这句提示反馈给用户; 其他三种类型按约定固定传None。
调用顺序(必须遵守)
⚠️ 任何"有效指令"的应答都必须按下面的顺序进行,否则机器人无法正确进入新一轮应答:
response.on_interrupt(event_id, type, tips)response.on_skill(...)与 / 或response.on_llm_item_* / on_vlm_item_* / on_tts_item_*
按语义理解分类结果选择 interrupt_type:
| 语义理解结果 | 第一步调用 | 之后允许的应答 |
|---|---|---|
| 命中风控 | on_interrupt(..., "risk_control", "<给用户的话术>") | 一般不再返回 LLM/VLM/技能(按业务定) |
| 命中技能 | on_interrupt(..., "action", None) | on_skill(...)(可并行 on_llm_* / on_vlm_* / on_tts_*) |
| 命中有效闲聊 | on_interrupt(..., "chat", None) | on_llm_item_* / on_vlm_item_* / on_tts_item_* |
| realtime 对话 | on_interrupt(..., "realtime", None) | realtime 应答内容 |
| 拒识 | 不调用 on_interrupt | 一个都不调用——保持机器人当前状态 |
示例
# 普通对话回复
response.on_interrupt(event_id, "chat", None)
response.on_llm_item_delta(event_id, item_id, "好的,我来回答你的问题。")
# 命中动作技能
response.on_interrupt(event_id, "action", None)
skill_param = AgentParam.create().set_string("action", "wave_hands")
response.on_skill(event_id, item_id, "action", "wave_hands", skill_param)
# 实时对话边听边说
response.on_interrupt(event_id, "realtime", None)
response.on_llm_item_delta(event_id, item_id, "嗯嗯,我听到了……")
# 风控命中(interrupt_tips 必填,作为给用户的话术)
response.on_interrupt(event_id, "risk_control", "你谈到敏感话题了,咱们换个话题聊吧")
基类共享回调(v1.4.0+)
PassiveCallback 基类定义了一组与具体语义模式无关的"通用回调",所有 8 种回调类(Audio2TtsCallback、Asr2LlmCallback 等)均自动继承,按需重载即可:
| 方法 | 触发时机 | 说明 |
|---|---|---|
on_robot_online(agent_id, agent_meta) | 智能体上线 | agent_id 是灵心平台配置的智能体 ID;agent_meta 含端侧设备元数据(唤醒词、城市等) |
on_robot_offline(agent_id) | 智能体下线 | 清理该 agent_id 关联的资源 |
on_face_info(agent_id, event_id, param) | 识别到人脸 / 声纹 UID | param 内含人脸或声纹特征字段 |
on_video_frame(agent_id, event_id, flag, buf, is_key_frame, local_ts, param) | 透传视频流 | flag=2 H264,flag=3 图片 |
on_greet_signal(agent_id, event_id, param, response) | 打招呼信令 | 通过 GreetResponse 应答 VLM 或 TTS |
on_state(agent_id, event_id, state_name, state_value) | 机器人状态变化 | state_value 可能为 JSON 字符串;所有 state_name 模块定义见 机器人端侧状态参考 |
打招呼应答 (GreetResponse)
on_greet_signal 收到信令后,可在 GreetResponse 上以两种模式应答(不互斥,按需选择):
def on_greet_signal(self, agent_id, event_id, param, response):
# 模式一:VLM 文本流式应答
response.on_greet_vlm_delta(event_id, "你好")
response.on_greet_vlm_delta(event_id, ",今天天气真不错")
response.on_greet_vlm_done(event_id)
# 模式二:TTS 音频流式应答(参数是合成好的音频字节经 base64 编码后的字符串片段)
import base64
audio_chunk1 = synthesize_tts("你好") # PCM / Opus 等 bytes
audio_chunk2 = synthesize_tts(",欢迎回家")
response.on_greet_tts_delta(event_id, base64.b64encode(audio_chunk1).decode())
response.on_greet_tts_delta(event_id, base64.b64encode(audio_chunk2).decode())
response.on_greet_tts_done(event_id)
| 方法 | 说明 |
|---|---|
on_greet_vlm_delta(event_id, text) | VLM 流式片段 |
on_greet_vlm_done(event_id) | VLM 流式输出结束 |
on_greet_tts_delta(event_id, audio_base64) | TTS 音频流式片段——audio_base64 是合成音频字节经 base64 编码后的字符串 |
on_greet_tts_done(event_id) | TTS 音频输出结束 |
on_error(event_id, code, msg) | 应答异常 |
重要注意事项
- 回调是单例:所有上线的智能体共享同一回调实例,用
agent_id(灵心平台配置的智能体 ID)区分 - 线程模型:每个
agent_id绑定一个独立 worker,同一智能体的请求按顺序执行 - event_id 隔离:不同对话轮次通过
event_id隔离,非线程安全资源应基于event_id管理 - Response 线程安全:
response.on_*()方法可在任意线程调用(内部threading.Lock串行化) - Response 超时清理:Response 对象在 120 秒无更新后由
AgentSdkResponseMgr自动清理 - 先注册后初始化:必须先
register_*()再initialize(),顺序不能反 - 基类回调按需重载:
on_greet_signal/on_state/on_face_info/on_video_frame在PassiveCallback中已有默认日志实现,不重载也能正常运行