跳到主要内容

代码详解

在上一章中,我们成功运行了一个语音智能体。

本章将详细解释代码逻辑,帮助读者理解如何使用 RustPBX SDK。

项目地址:https://github.com/restsend/rustpbxgo

目录结构

rustpbxgo/
├── README.md
├── client.go # SDK 核心定义
├── cmd/ # 示例应用
│ ├── main.go # 程序入口
│ ├── llm.go # 大模型交互
│ ├── media.go # WebRTC
│ └── webhook.go # WebHook 处理
├── go.mod
└── go.sum
  • client.go:包括 RustPBX Go 的核心数据结构 Client 定义及其方法。
  • cmd/ 目录:语音智能体示例代码, 包括 SIP/WebRTC 呼叫, 配合 webhook 的呼入处理,以及大模型交互逻辑。

client.go - 客户端定义

连接 RustPBX

  • NewClient: 创建客户端
    • endpoint: 用于指定 RustPBX 服务器地址
  • Connect: 连接服务器
    • callType: 指定通话类型(这里使用 "webrtc")
  • Shutdown: 关闭客户端

发送命令

RustPBXGo 通过调用方法发送对应的命令到 RustPBX。

这里用到的命令包括:

  • Invite: 发起呼叫
    • 这里使用 WebRTC 呼叫,需要在 CallOption 中设置 offer 为呼叫目标的 SDP offer
  • TTS: 调用文本转语音服务并播放
  • Play: 播放链接中的音频文件
  • Interrupt: 打断当前播放 (TTS 或 文件播放)
  • Hangup: 挂断

注册回调函数

在通话的建立到结束整个过程中,会触发多种事件,如图示:

电话
电话
UserAgent
UserAgent
INVITE
INVITE
BYE
BYE
4xx
4xx
200 OK
200 OK
200 OK
200 OK
事件
事件
Track Start Ringing Reject Answer Track End Hangup
180 Ringing
180 Ringing
Text is not SVG - cannot display

通过设置回调函数, 可以处理对应的事件。

例如处理 AsrFinal 事件(语音识别稳定结果),可以在 OnAsrFinal 字段设置回调函数。

示意图

client 在调用 Connect 方法时,会创建两个协程 (下图中绿色部分):

  • 一个负责读 WebSocket 消息并解析(上)

  • 一个负责处理消息和发送命令(下), 收到事件时调用 processEvent 方法,根据事件类型调用对应的回调函数。

c.conn.ReadMessage()
c.conn.ReadMessage()
RustPBX
RustPBX
事件/WebSocket
事件/WebSocket
事件/c.eventChan
事件/c.eventChan
c.processEvent()
c.processEvent()
OnAsrFinal
OnAsrFinal
client
client
c.conn.WriteJson()
c.conn.WriteJson()
命令/WebSocket
命令/WebSocket
OnSpeaking
OnSpeaking
OnHangup
OnHangup
Text is not SVG - cannot display

在下一节,我们将看到如何使用这些 API 发送命令和处理事件。

main.go - 示例应用入口

main.go 是程序的入口,主要逻辑包括:

创建 WebRTCPeer

创建 WebRTCPeer 并生成 SDP offer, 然后将 SDP offer 写入 callOption.Offer 字段中

main.go
    localSdp, err := mediaHandler.Setup(codec, iceServers)
if err != nil {
logger.Fatalf("Failed to get local SDP: %v", err)
}
logger.Infof("Offer SDP: %v", localSdp)
callOption.Offer = localSdp
信息

WebRTC 呼叫需要设置 callOption.Offer, SIP 呼叫需要设置 callOption.CallercallOption.Callee

参阅:

创建 Client, 并注册回调函数

使用 NewClient 函数创建客户端,这里主要参数是 option.Endpoint 用于设置 RustPBX 服务器地址。

详细参数

这里主要处理的是 AsrFinal 事件, 在 OnAsrFinal 字段设置回调函数。

在这里调用 LLMHandler.QueryStream 方法,将识别结果 event.Text 输入大模型。

main.go
client.OnAsrFinal = func(event rustpbxgo.AsrFinalEvent) {
...
response, err := option.LLMHandler.QueryStream(option.OpenaiModel, event.Text, option.StreamingTTS, client, option.ReferCaller)
...
}

连接/断开连接 RustPBX

main.go
    err = client.Connect(callType)
if err != nil {
logger.Fatalf("Failed to connect to server: %v", err)
}
defer client.Shutdown()

这里我们使用 webrtc 通话,因此 callType 参数为 "webrtc"

使用 TTS 命令发送欢迎语

main.go
    client.TTS(greeting, "", "1", true, false, nil, nil)

这里 greeting 参数的值从命令行读入,作为 TTS 命令的 text 参数, 即要转换的文本。

llm.go - 大模型交互

llm.go 负责与大模型交互,包括定义工具、发起请求和响应处理。

响应包括文本和工具调用。如果是文本,我们将通过 TTS 命令播放。

这里我们选择 Go OpenAI 作为大模型的客户端。

定义工具(Tools)

什么是工具调用(Tool Calling)?

工具调用(也叫 Function Calling)允许 LLM 在需要时调用外部定义的函数。例如:

我们将 HangupRefer 两个命令包装成工具。

  • 当用户说"帮我转人工"时,LLM 会调用 Refer 工具,实现转接
  • 当用户说"再见"时,LLM 会调用 Hangup 工具,实现挂断

参见:OpenAI Function Calling

这里定义了挂断转接两个工具:

llm.go
    var hangupDefinition = openai.FunctionDefinition{
Name: "hangup",
Description: "End the conversation and hang up the call",
Parameters: json.RawMessage(`{
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "Reason for hanging up the call"
}
},
"required": []
}`),
}

var referDefinition = openai.FunctionDefinition{
Name: "refer",
Description: "Refer the call to another target",
Parameters: json.RawMessage(`{
"type": "object",
"properties": {
},
"required": []
}`),
}

当大模型返回的响应中包含工具调用时,我们会分别调用 HangupRefer 方法。

这两个方法又会分别发送 HangupRefer 命令到 RustPBX。

发起请求

这里我们使用 CreateChatCompletionStream 方法发起请求。

llm.go
    stream, err := h.client.CreateChatCompletionStream(h.ctx, request)
if err != nil {
return "", err
}
defer stream.Close()

由于我们在 request 中设置了 Stream: true,因此会返回一个流式响应。

llm.go
	request := openai.ChatCompletionRequest{
Model: model,
Messages: h.messages,
Temperature: 0.7,
Stream: true,
}

流式响应通过 Server-Sent Events 技术实现。每一次调用 stream.Recv() 方法,都会返回部分结果。

处理响应

当 LLM 决定调用工具时,结果中会包含 toolCall。

我们根据 toolCall.Function.Name 字段判断调用哪个工具,然后调用对应的工具。

llm.go
    if len(response.Choices) > 0 && len(response.Choices[0].Delta.ToolCalls) > 0 {
for _, toolCall := range response.Choices[0].Delta.ToolCalls {
if toolCall.Function.Name == "hangup" {
err := tools.HandleHangup("LLM requested hangup");
}
if toolCall.Function.Name == "refer" {
err := tools.HandleRefer();
}
}
}

对于最终的文本响应,我们通过 TTS 命令播放。

for {
response, err := stream.Recv()
if err == io.EOF {
break // 流结束
}

// 获取文本内容
content := response.Choices[0].Delta.Content
if content != "" {
fullResponse += content

// 立即发送到 TTS(流式播放)
if isFinished {
ttsWriter.Write(content, true, false) // endOfStream=true,最后一段
} else {
ttsWriter.Write(content, false, false) // endOfStream=false,中间片段
}
}
}
TTSWriter 参数说明
  • text: 要播放的文本片段
  • endOfStream: 是否为最后一段
  • autoHangup: 播放完毕后是否挂断

总结

在这一章中,我们介绍了 RustPBX Go 的代码结构。

以及如何连接 RustPBX、发送命令、处理事件,并简单介绍了大模型交互和工具调用。

下一章我们将介绍如何添加自定义工具,实现一个天气查询 Agent。