手动控制说话开始和结束
当你的业务场景不适合依赖服务端自动判定用户何时开始说话或结束说话时,可以使用手动 SoS/EoS 能力,由客户端显式声明用户回合边界。该能力适用于 AI 面试、互动答题、对讲机模式等需要“按下开始、点击提交”的交互。本文介绍如何通过创建智能体配置和客户端组件接口,实现手动控制用户说话边界。
- 手动 SoS/EoS 依赖 RTM。使用前必须同时开启
advanced_features.enable_rtm = true,并将parameters.data_channel设为rtm。 manual模式只控制用户回合边界,不等同于“打断智能体说话”。如果你要中断智能体当前播报,请参考打断智能体。- 公开接口不支持
manual_config。manual模式下无需传入额外配置。
适用场景
手动控制用户说话边界适用于以下场景:
- AI 面试:候选人单次回答时间较长,过程中可能包含停顿、思考和语气词。业务侧可保留自动 SoS,只在用户点击“提交答案”时触发手动 EoS,减少误判结束说话导致的抢话。
- 互动答题:在需要由用户点击“开始作答”或“提交答案”的场景中,可通过手动 SoS/EoS 显式控制回答边界,避免答题过程中的停顿被误判为回答结束。
- 对讲机模式:在 PTT 类场景中,业务侧通常需要同时控制“开始说话”和“结束说话”,避免服务端根据静音或语义自动切分回合。
功能介绍
手动轮次控制包含两个独立能力:
- 手动 SoS:客户端显式声明“用户开始说话”。
- 手动 EoS:客户端显式声明“用户说完了”。
两者可分别开启:
- 仅
end_of_speech.mode = "manual":适合“开始说话仍交给 VAD、结束说话由用户主动提交”的场景。 - 同时设置
start_of_speech.mode = "manual"和end_of_speech.mode = "manual":适合完整的 PTT 或对讲机模式。
实现原理
手动 SoS/EoS 的底层通过 RTM 点对点消息完成:
- 手动 SoS 的外层
customType为user.manual_sos - 手动 EoS 的外层
customType为user.manual_eos - 外层消息格式统一为
customType + publisher + message - 内层
message为 JSON 字符串,至少包含request_id
request_id 用于把服务端成功/失败回调与原始请求精确关联,建议业务侧为每次请求生成唯一值。
- 客户端主动发送手动 SoS/EoS 请求时,需要通过外层
customType区分user.manual_sos和user.manual_eos。 - 服务端回调时,端侧无需依赖外层
customType区分事件类型,可直接解析内层message中的event_type字段进行处理。
前提条件
开始前,请确保完成以下准备工作:
- 已集成 RTC SDK v4.5.1 及以上版本,且已在 App 中实现基本的实时音视频功能并获取相关设备权限。请参考实现音视频互动。
- 已在控制台为项目启用 RTM 服务,并在 App 中实现基本的实时消息功能。请参考实现收发消息。
- 已参考实现对话式智能体实现与智能体对话的基本逻辑。
- 确保 RTC 可用、RTM 已登录,且 RTC 和 RTM 实例的生命周期大于组件的生命周期。组件内部不负责维护 RTC,RTM 的初始化、生命周期以及鉴权/登录状态的逻辑。
此外,请确保:
- 你的 RTC Token 同时具备 RTC 和 RTM 权限。
- 已完成 RTM SDK 的基础集成,并可正常收发 RTM 消息。
- 已在创建智能体时启用 RTM。
实现手动控制用户说话开始和结束
创建智能体时开启手动轮次控制
调用 POST 创建对话式智能体 接口时,开启 RTM,并根据业务场景将 start_of_speech.mode 和 end_of_speech.mode 设为 manual。
如果你只需要手动控制结束说话,可以保留自动 SoS,仅将 end_of_speech.mode 设为 manual:
{
"name": "manual-eos-agent",
"properties": {
"channel": "channel_name",
"token": "token",
"agent_rtc_uid": "0",
"remote_rtc_uids": ["123"],
"advanced_features": {
"enable_rtm": true
},
"turn_detection": {
"mode": "default",
"config": {
"start_of_speech": {
"mode": "vad",
"vad_config": {
"interrupt_duration_ms": 160,
"speaking_interrupt_duration_ms": 320,
"prefix_padding_ms": 800
}
},
"end_of_speech": {
"mode": "manual"
}
}
},
"parameters": {
"data_channel": "rtm"
}
}
}
如果你需要完整的对讲机模式,则同时将 start_of_speech.mode 和 end_of_speech.mode 设为 manual:
{
"name": "manual-sos-eos-agent",
"properties": {
"channel": "channel_name",
"token": "token",
"agent_rtc_uid": "0",
"remote_rtc_uids": ["123"],
"advanced_features": {
"enable_rtm": true
},
"turn_detection": {
"mode": "default",
"config": {
"start_of_speech": {
"mode": "manual"
},
"end_of_speech": {
"mode": "manual"
}
}
},
"parameters": {
"data_channel": "rtm"
}
}
}
集成并初始化客户端组件
先完成组件集成,再使用 RTC 和 RTM 实例初始化客户端组件:
- Android
- iOS
- Web
- Android
- iOS
- Web
val config = ConversationalAIAPIConfig(
rtcEngine = rtcEngineInstance,
rtmClient = rtmClientInstance,
enableLog = true
)
val api = ConversationalAIAPIImpl(config)
let config = ConversationalAIAPIConfig(
rtcEngine: rtcEngine,
rtmEngine: rtmEngine,
enableLog: true
)
convoAIAPI = ConversationalAIAPIImpl(config: config)
ConversationalAIAPI.init({
rtcEngine,
rtmEngine,
})
const conversationalAIAPI = ConversationalAIAPI.getInstance()
注册消息回调并订阅频道消息
手动 SoS/EoS 的处理结果通过专用回调或事件返回:
你需要先注册这些回调或事件,再订阅智能体所在频道的消息:
- Android
- iOS
- Web
api.addHandler(object : IConversationalAIAPIEventHandler {
override fun onUserManualSosEvent(agentUserId: String, event: UserManualSosEvent) {
// 处理手动 SOS 结果
}
override fun onUserManualEosEvent(agentUserId: String, event: UserManualEosEvent) {
// 处理手动 EOS 结果
}
override fun onAssistantManualEosEvent(agentUserId: String, event: AssistantManualEosEvent) {
// 处理服务端超限自动 EOS 通知
}
})
api.subscribeMessage("channelName") { error ->
if (error != null) {
// 处理错误
}
}
convoAIAPI.addHandler(handler: self)
func onUserManualSosEvent(agentUserId: String, event: UserManualSosEvent) {
// 处理手动 SOS 结果
}
func onUserManualEosEvent(agentUserId: String, event: UserManualEosEvent) {
// 处理手动 EOS 结果
}
func onAssistantManualEosEvent(agentUserId: String, event: AssistantManualEosEvent) {
// 处理服务端超限自动 EOS 通知
}
convoAIAPI.subscribeMessage(channelName: channelName) { error in
if let error = error {
print("订阅失败: \(error.message)")
}
}
import {
EConversationalAIAPIEvents,
TUserManualSosEvent,
TUserManualEosEvent,
TAssistantManualEosEvent,
} from '@/conversational-ai-api/type'
conversationalAIAPI.on(
EConversationalAIAPIEvents.USER_MANUAL_SOS_EVENT,
(agentUserId: string, event: TUserManualSosEvent) => {
// 处理手动 SOS 结果
}
)
conversationalAIAPI.on(
EConversationalAIAPIEvents.USER_MANUAL_EOS_EVENT,
(agentUserId: string, event: TUserManualEosEvent) => {
// 处理手动 EOS 结果
}
)
conversationalAIAPI.on(
EConversationalAIAPIEvents.ASSISTANT_MANUAL_EOS_EVENT,
(agentUserId: string, event: TAssistantManualEosEvent) => {
// 处理服务端超限自动 EOS 通知
}
)
conversationalAIAPI.subscribeMessage(channelName)
让智能体加入频道
创建智能体后,让智能体加入与你的用户相同的频道:
调用 POST 创建对话式智能体接口,并完成以下参数设置:
advanced_features.enable_rtm: true—— (必选)启动 RTM 服务parameters.data_channel: "rtm"—— (必选)开启 RTM 数据传输通道parameters.enable_metrics: true—— (按需开启)接收智能体性能数据parameters.enable_error_message: true—— (按需开启)接收智能体错误事件
调用成功后,智能体会加入指定 RTC 频道,用户可以开始与智能体互动。
在用户开始说话时发送手动 SoS
当业务确认用户开始说话时,调用 manualSOS:
- Android
- iOS
- Web
api.manualSOS(
agentUserId = "agentUserId",
requestId = "sos-req-20260612-001"
) { error ->
if (error != null) {
Log.e("ManualSOS", "发送失败: ${error.errorMessage}")
}
}
convoAIAPI.manualSOS(
agentUserId: agentUid,
requestId: "sos-req-20260612-001"
) { error in
if let error = error {
print("发送失败: \(error.message)")
}
}
await conversationalAIAPI.manualSOS(agentUserId, {
requestId: "sos-req-20260612-001",
})
手动 SoS 生效后:
- 智能体进入用户说话状态,开始接收后续音频帧做 ASR。
- 在此之前到达的音频不会被计入本轮用户发言。
- 仅当
start_of_speech.mode = "manual"时才会被服务端处理;否则会收到失败回调。
在用户结束说话时发送手动 EoS
当业务确认用户说完后,调用 manualEOS:
- Android
- iOS
- Web
api.manualEOS(
agentUserId = "agentUserId",
requestId = "eos-req-20260612-001"
) { error ->
if (error != null) {
Log.e("ManualEOS", "发送失败: ${error.errorMessage}")
}
}
convoAIAPI.manualEOS(
agentUserId: agentUid,
requestId: "eos-req-20260612-001"
) { error in
if let error = error {
print("发送失败: \(error.message)")
}
}
await conversationalAIAPI.manualEOS(agentUserId, {
requestId: "eos-req-20260612-001",
})
手动 EoS 生效后:
- 智能体按现有链路提交 ASR、触发 LLM 推理,并在后续生成回复。
- 该信号只表示“用户当前发言段结束”,不等同于整个 turn 立即结束。
- 仅当
end_of_speech.mode = "manual"时才会被服务端处理;否则会收到失败回调。
开发注意事项
处理成功和失败回调
下文展示的是服务端返回的原始 RTM message 解码后结构;如果你使用客户端组件,上述原始结构会被进一步映射为平台对应的事件对象或事件参数。
成功回调的内层 message 解码后为统一事件结构:
{
"event_id": "a1b2c3d4",
"event_type": "user.manual_sos.result",
"event_ms": 1710000000100,
"payload": {
"success": true,
"turn_id": 23,
"request_id": "sos-req-20260612-001"
}
}
手动 EoS 成功时,event_type 为 user.manual_eos.result。其中:
event_id:事件唯一 ID。event_type:事件类型。event_ms:事件发送时间,单位为毫秒。payload.success:请求是否处理成功。成功时为true,失败时为false。payload.turn_id:本次请求关联的对话轮次 ID。payload.request_id:与你发送请求时传入的request_id对应。
客户端组件会将服务端返回的 event_type 映射为专用回调或事件。若你绕过客户端组件直接消费原始 RTM 消息,建议以 message 中的 event_type 为准区分 user.manual_sos.result、user.manual_eos.result 和 assistant.manual_eos.result,而不是依赖外层 RTM 消息的 customType。
失败回调的内层 message 同样采用统一事件结构:
{
"event_id": "i9j0k1l2",
"event_type": "user.manual_eos.result",
"event_ms": 1710000005200,
"payload": {
"success": false,
"request_id": "eos-req-20260612-001",
"error_message": "Current mode is not manual."
}
}
处理失败回调时,建议至少根据以下字段做分支:
payload.request_id:关联原始请求。payload.error_message:查看具体描述。
常见失败场景包括:
- 当前对应 mode 不是
manual。 - 当前不存在可标记的活跃用户发言。
- 同一轮内重复提交已生效的手动 SoS 或手动 EoS。
当前协议仅返回 error_message 原始字符串,客户端应保留原值。以下为当前常见返回值示例,后续可能扩展:
error_message | 含义 | 常见触发场景 |
|---|---|---|
Current mode is not manual. | 当前对应 mode 配置不允许手动信令。 | 未开启对应的 manual 模式。 |
No turns available for EOS labeling. | 当前没有可用于 EoS 标记的用户发言。 | 重复提交 EoS,或当前 turn 已提交。 |
No turns available for SOS labeling. | 当前没有可用于 SoS 标记的用户发言。 | 重复提交 SoS,或当前已处于录音中。 |
处理服务端自动 EoS 通知
当 end_of_speech.mode = "manual" 且服务端触发 max_speech_duration_ms 上限保护时,即使客户端没有主动调用 manualEOS,也会收到一条自动 EoS 通知。
其内层 message 解码后示例如下:
{
"event_id": "q7r8s9t0",
"event_type": "assistant.manual_eos.result",
"event_ms": 1710000060000,
"payload": {
"reason": "Audio duration exceeded the max_audio_duration_ms limit.",
"max_duration_ms": 600000,
"turn_id": 23
}
}
收到该事件表示:
- 服务端已主动结束当前用户发言段。
- 后续仍会按正常链路进入 ASR commit、LLM 推理和 TTS 回复。
- 业务侧可据此提示用户“已达到单轮最长说话时长限制”。
集成建议
- 如果你的场景只需要防止用户长停顿被误判,优先考虑“VAD 自动 SoS + 手动 EoS”。
- 如果你的场景是按钮按住说话、松开发送,建议同时使用手动 SoS 和手动 EoS。
- 为每次手动 SoS/EoS 请求生成唯一
request_id,便于做幂等和重试。
相关信息
销毁组件实例
结束 AI 对话场景后或关闭 App 前,你需要销毁组件实例,以释放组件的所有资源。
- Android
- iOS
- Web
api.destroy()
convoAIAPI.destroy()
conversationalAIAPI.destroy()