如何在 AI 语音代理中使用 SIP 电话进行呼叫转接

发布: (2025年12月29日 GMT+8 18:47)
12 min read
原文: Dev.to

Source: Dev.to

如何在 AI 语音代理中使用 SIP 电话实现呼叫转接

在构建基于 AI 的语音代理(如聊天机器人或虚拟客服)时,呼叫转接是提升用户体验的关键功能之一。通过 SIP(Session Initiation Protocol)电话系统,开发者可以在通话过程中将用户从 AI 代理无缝转接到真人坐席或其他服务。本文将逐步演示如何在 AI 语音代理中实现呼叫转接。


目录

  1. 前置条件
  2. 概念回顾:SIP 与呼叫转接
  3. 步骤一:配置 SIP 服务器
  4. 步骤二:在 AI 代理中捕获转接意图
  5. 步骤三:使用 SIP REFER 实现转接
  6. 完整示例代码
  7. 常见问题排查
  8. 结论

前置条件

条件说明
SIP 服务器推荐使用 Asterisk、FreeSWITCH、或商业托管的 SIP 提供商(如 Twilio SIP Trunk、Plivo)。
AI 语音平台本文以 Google Dialogflow CXMicrosoft Bot FrameworkRasa 为例。
编程语言示例代码使用 Node.jssip.js)和 Pythonpjsua2),但思路可迁移到其他语言。
网络确保服务器对外开放 5060/5061 端口(UDP/TCP),并已配置防火墙规则。
证书若使用 TLS(推荐),请准备有效的 SSL 证书。

概念回顾:SIP 与呼叫转接

  • SIP(Session Initiation Protocol):用于在 IP 网络上建立、修改和终止多媒体会话的信令协议。
  • INVITE:发起通话的请求。
  • ACK:确认收到的响应。
  • BYE:结束通话。
  • REFER:请求对端将当前会话转接到另一个 SIP URI。
  • Replaces:在转接过程中,用新的会话取代旧的会话(常与 REFER 配合使用)。

关键点:在 AI 代理内部,通常会先通过 INVITE 与用户建立通话,然后在需要转接时发送 REFER,并在收到 200 OK 后结束原始会话(BYE)。


步骤一:配置 SIP 服务器

  1. 创建 SIP 域/租户

    • 在 Asterisk 中编辑 sip.conf,添加一个用于 AI 代理的用户:
    [ai_agent]
    type=friend
    host=dynamic
    secret=YourStrongPassword
    context=from-ai
    disallow=all
    allow=ulaw
  2. 设置转接策略

    • extensions.conf 中定义转接拨号计划(示例为转接到 1001 坐席):
    [from-ai]
    exten => _X.,1,NoOp(Incoming call from AI agent)
    same => n,Dial(SIP/1001,30)
    same => n,Hangup()
  3. 启用 REFER 支持

    • 确保 allowtransfer=yes(Asterisk)或相应的 allow_transfer 参数已打开。
  4. 重启 SIP 服务

    sudo asterisk -rx "sip reload"

步骤二:在 AI 代理中捕获转接意图

在对话流中,需要检测用户是否请求转接(如 “我想和人工客服聊聊”)。以下以 Dialogflow CX 为例:

  1. 创建意图Transfer_To_Human,训练示例包括 “人工客服”“转接”“和真人说话”。

  2. 在 Fulfillment 中返回转接信号

    {
      "sessionInfo": {
        "parameters": {
          "transfer": true,
          "targetNumber": "sip:1001@yourdomain.com"
        }
      }
    }
  3. 在后端监听该参数,当 transfertrue 时触发转接逻辑。


步骤三:使用 SIP REFER 实现转接

下面展示 Node.jssip.js)的实现思路。核心步骤:

  1. 获取当前会话对象session)。
  2. 构造 REFER 请求,目标为坐席的 SIP URI。
  3. 发送 REFER 并监听响应
  4. 收到 200 OK 后发送 BYE 结束原始会话(可选,取决于业务需求)。
// 假设 `session` 为当前的 SIP 会话实例
function transferCall(targetSipUri) {
  const referRequest = session.refer(targetSipUri, {
    // 可选的 Refer-To Header
    extraHeaders: [
      `Refer-To: ${targetSipUri}`,
      'Replaces: ' + session.id // 若需要使用 Replaces
    ]
  });

  referRequest.then(response => {
    if (response.statusCode === 200) {
      console.log('Transfer accepted, ending original call...');
      session.bye(); // 结束原始通话
    } else {
      console.warn('Transfer failed with status:', response.statusCode);
    }
  }).catch(err => {
    console.error('Error sending REFER:', err);
  });
}

注意

  • targetSipUri 必须是 SIP 服务器已注册的用户(如 sip:1001@yourdomain.com)。
  • 若使用 TLS,请在 session 初始化时指定 transport: 'TLS'

完整示例代码

以下示例整合了 Dialogflow CX webhook 与 sip.js,实现从 AI 代理到坐席的自动转接。

// server.js
const express = require('express');
const bodyParser = require('body-parser');
const { UserAgent, Registerer, Inviter, Session } = require('sip.js');

const app = express();
app.use(bodyParser.json());

// SIP UA 配置(请替换为实际凭证)
const ua = new UserAgent({
  uri: 'sip:ai_agent@yourdomain.com',
  transportOptions: {
    server: 'wss://sip-ws.yourdomain.com'
  },
  authorizationUsername: 'ai_agent',
  authorizationPassword: 'YourStrongPassword'
});

let currentSession = null;

// 注册到 SIP 服务器
ua.start().then(() => {
  const registerer = new Registerer(ua);
  return registerer.register();
}).catch(console.error);

// 接收 Dialogflow CX webhook
app.post('/dialogflow-webhook', async (req, res) => {
  const intent = req.body.queryResult.intent.displayName;
  const parameters = req.body.queryResult.parameters;

  if (intent === 'Transfer_To_Human' && parameters.transfer) {
    const target = parameters.targetNumber; // 如 sip:1001@yourdomain.com
    if (currentSession) {
      transferCall(target);
      res.json({ fulfillmentText: '正在为您转接,请稍候…' });
    } else {
      res.json({ fulfillmentText: '当前没有活跃通话,无法转接。' });
    }
  } else {
    // 其他意图的常规处理
    res.json({ fulfillmentText: '抱歉,我没有听懂,请再说一遍。' });
  }
});

// 发起呼叫(示例:从外部系统触发)
app.post('/start-call', async (req, res) => {
  const callee = req.body.callee; // 如 sip:user@example.com
  const inviter = new Inviter(ua, callee);
  try {
    const outgoingSession = await inviter.invite();
    currentSession = outgoingSession;
    res.json({ status: 'call_started' });
  } catch (e) {
    console.error(e);
    res.status(500).json({ error: 'call_failed' });
  }
});

// 转接函数(与上文相同)
function transferCall(targetSipUri) {
  if (!currentSession) {
    console.warn('No active session to transfer.');
    return;
  }
  const referRequest = currentSession.refer(targetSipUri, {
    extraHeaders: [
      `Refer-To: ${targetSipUri}`,
      'Replaces: ' + currentSession.id
    ]
  });

  referRequest.then(response => {
    if (response.statusCode === 200) {
      console.log('Transfer successful, ending original call.');
      currentSession.bye();
      currentSession = null;
    } else {
      console.warn('Transfer failed:', response.statusCode);
    }
  }).catch(err => {
    console.error('REFER error:', err);
  });
}

app.listen(3000, () => console.log('Webhook server listening on :3000'));

代码说明

  • SIP UA 初始化:使用 sip.js 创建 WebSocket‑based SIP 用户代理。
  • Webhook 入口/dialogflow-webhook 负责解析 Dialogflow 的请求并判断是否需要转接。
  • 转接实现transferCall 通过 REFER 将通话转给坐席,并在成功后发送 BYE
  • 错误处理:所有网络/协议错误均记录到控制台,便于调试。

常见问题排查

场景可能原因解决方案
REFER 返回 403SIP 服务器未授权当前用户发送 REFER在 SIP 服务器上为 ai_agent 开启 allowtransfer=yes(Asterisk)或对应的转接权限。
坐席未收到来电Refer-To 中的 URI 拼写错误或未在服务器注册。确认坐席的 SIP URI 正确且已注册(sip:1001@yourdomain.com)。
通话在转接后立即挂断BYE 发送过早,导致坐席未能建立新会话。在收到 200 OKREFER)后,等待坐席确认(如 180 Ringing)再发送 BYE,或使用 Replaces 机制。
TLS 握手失败证书不受信任或端口未开放。使用受信任的 CA 证书,确保防火墙放行 5061(TLS)或对应的 WebSocket 端口。
Dialogflow 未触发转接意图匹配阈值过低或参数未正确返回。检查意图训练短语,确认 webhook 返回的 JSON 结构与 Dialogflow 期望一致。

结论

Overview

当来电者联系企业时,他们期望得到快速且明确的解决方案。AI 语音代理应当能够判断何时提供帮助,何时将通话转接给人工客服。VideoSDK 中的通话转接功能让您的 AI 代理能够在不挂断来电者的情况下,将正在进行的 SIP 通话转移到另一个电话号码,从而确保对话流畅无缝。

呼叫转接工作原理

  • AI 代理评估来电者的意图,以确定是否需要转接。
  • 当需要转接时,代理会触发一个 function tool
  • function tool 指示系统立即将正在进行的 SIP 通话转接到指定号码,无需来电者重新拨号。
  • 从来电者的角度来看,转接是自动的,通话会持续进行,不会出现掉线或尴尬的停顿。

设置呼叫转接代理

下面是一个最小示例,展示如何使用 transfer_call 工具转接通话的代理。

import os
import logging

from videosdk.agents import (
    Agent, AgentSession, CascadingPipeline, function_tool,
    WorkerJob, ConversationFlow, JobContext, RoomOptions, Options
)
from videosdk.plugins.deepgram import DeepgramSTT
from videosdk.plugins.google import GoogleLLM
from videosdk.plugins.cartesia import CartesiaTTS
from videosdk.plugins.silero import SileroVAD
from videosdk.plugins.turn_detector import TurnDetector, pre_download_model

# Set up basic logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    handlers=[logging.StreamHandler()]
)

# Pre‑download the turn‑detector model
pre_download_model()


class CallTransferAgent(Agent):
    def __init__(self):
        super().__init__(
            instructions=(
                "You are the Call Transfer Agent. "
                "Help transfer ongoing calls to a new number using the transfer_call tool."
            )
        )

    async def on_enter(self) -> None:
        await self.session.say("Hello! How can I help you today?")

    async def on_exit(self) -> None:
        await self.session.say("Goodbye, and thank you for calling!")

    @function_tool
    async def transfer_call(self) -> None:
        """Transfer the call to the provided number."""
        token = os.getenv("VIDEOSDK_AUTH_TOKEN")
        transfer_to = os.getenv("CALL_TRANSFER_TO")
        return await self.session.call_transfer(token, transfer_to)


async def entrypoint(ctx: JobContext):
    agent = CallTransferAgent()
    conversation_flow = ConversationFlow(agent)

    pipeline = CascadingPipeline(
        stt=DeepgramSTT(),
        llm=GoogleLLM(),
        tts=CartesiaTTS(),
        vad=SileroVAD(),
        turn_detector=TurnDetector()
    )

    session = AgentSession(
        agent=agent,
        pipeline=pipeline,
        conversation_flow=conversation_flow
    )

    await session.start(wait_for_participant=True, run_until_shutdown=True)


def make_context() -> JobContext:
    room_options = RoomOptions(name="Call Transfer Agent", playground=True)
    return JobContext(room_options=room_options)


if __name__ == "__main__":
    job = WorkerJob(
        entrypoint=entrypoint,
        jobctx=make_context,
        options=Options(
            agent_id="YOUR_AGENT_ID",
            register=True,
            host="localhost",
            port=8081
        )
    )
    job.start()

代码要点

  • transfer_call 函数工具:从环境变量中获取 VideoSDK 授权令牌和目标电话号码,然后调用 self.session.call_transfer
  • 管道组件:Deepgram 用于语音转文字,Google LLM 用于语言理解,Cartesia 用于文字转语音,Silero VAD 用于语音活动检测,TurnDetector 用于管理说话者轮次。
  • 作业配置:使用期望的 agent_id、主机和端口设置 WorkerJob

运行代理

  1. 定义环境变量 VIDEOSDK_AUTH_TOKENCALL_TRANSFER_TO
  2. 执行脚本(python your_script.py)。
  3. 当调用 transfer_call 工具时,代理将等待入站参与者,处理对话并转接通话。

呼叫转接的好处

  • 无缝交接:来电者永不遇到掉线或需要重新拨号的情况。
  • 提升用户体验:对话自然流畅,降低挫败感。
  • 可扩展的支持:AI 代理可以处理常规查询,并自动将复杂问题转接给人工客服。

附加资源

  • Quick Start Example – 入站/出站呼叫处理和路由规则的详细指南。
  • Call‑Transfer Implementation on GitHub – 包含示例项目的完整仓库。
  • VideoSDK Documentation – 所有 SDK 功能的综合参考。
Back to Blog

相关文章

阅读更多 »