代码详解
在上一章中,我们成功运行了一个语音智能体。
本章将详细解释代码逻辑,帮助读者理解如何使用 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
- 这里使用 WebRTC 呼叫,需要在
TTS: 调用文本转语音服务并播放Play: 播放链接中的音频文件Interrupt: 打断当前播放 (TTS 或 文件播放)Hangup: 挂断
注册回调函数
在通话的建立到结束整个过程中,会触发多种事件,如图示:
通过设置回调函数, 可以处理对应的事件。
例如处理 AsrFinal 事件(语音识别稳定结果),可以在 OnAsrFinal 字段设置回调函数。
示意图
client 在调用 Connect 方法时,会创建两个协程 (下图中绿色部分):
-
一个负责读 WebSocket 消息并解析(上)
-
一个负责处理消息和发送命令(下), 收到事件时调用
processEvent方法,根据事件类型调用对应的回调函数。
在下一节,我们将看到如何使用这些 API 发送命令和处理事件。
main.go - 示例应用入口
main.go 是程序的入口,主要逻辑包括:
创建 WebRTCPeer
创建 WebRTCPeer 并生成 SDP offer, 然后将 SDP offer 写入 callOption.Offer 字段中
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.Caller 和 callOption.Callee。
参阅:
创建 Client, 并注册回调函数
使用 NewClient 函数创建客户端,这里主要参数是 option.Endpoint 用于设置 RustPBX 服务器地址。
这里主要处理的是 AsrFinal 事件, 在 OnAsrFinal 字段设置回调函数。
在这里调用 LLMHandler.QueryStream 方法,将识别结果 event.Text 输入大模型。
client.OnAsrFinal = func(event rustpbxgo.AsrFinalEvent) {
...
response, err := option.LLMHandler.QueryStream(option.OpenaiModel, event.Text, option.StreamingTTS, client, option.ReferCaller)
...
}
连接/断开连接 RustPBX
err = client.Connect(callType)
if err != nil {
logger.Fatalf("Failed to connect to server: %v", err)
}
defer client.Shutdown()
这里我们使用 webrtc 通话,因此 callType 参数为 "webrtc"。
使用 TTS 命令发送欢迎语
client.TTS(greeting, "", "1", true, false, nil, nil)
这里 greeting 参数的值从命令行读入,作为 TTS 命令的 text 参数, 即要转换的文本。
llm.go - 大模型交互
llm.go 负责与大模型交互,包括定义工具、发起请求和响应处理。
响应包括文本和工具调用。如果是文本,我们将通过 TTS 命令播放。
这里我们选择 Go OpenAI 作为大模型的客户端。
定义工具(Tools)
什么是工具调用(Tool Calling)?
工具调用(也叫 Function Calling)允许 LLM 在需要时调用外部定义的函数。例如:
我们将 Hangup 和 Refer 两个命令包装成工具。
- 当用户说"帮我转人工"时,LLM 会调用
Refer工具,实现转接 - 当用户说"再见"时,LLM 会调用
Hangup工具,实现挂断
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": []
}`),
}
当大模型返回的响应中包含工具调用时,我们会分别调用 Hangup 和 Refer 方法。
这两个方法又会分别发送 Hangup 和 Refer 命令到 RustPBX。
发起请求
这里我们使用 CreateChatCompletionStream 方法发起请求。
stream, err := h.client.CreateChatCompletionStream(h.ctx, request)
if err != nil {
return "", err
}
defer stream.Close()
由于我们在 request 中设置了 Stream: true,因此会返回一个流式响应。
request := openai.ChatCompletionRequest{
Model: model,
Messages: h.messages,
Temperature: 0.7,
Stream: true,
}
流式响应通过 Server-Sent Events 技术实现。每一次调用 stream.Recv() 方法,都会返回部分结果。
处理响应
当 LLM 决定调用工具时,结果中会包含 toolCall。
我们根据 toolCall.Function.Name 字段判断调用哪个工具,然后调用对应的工具。
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,中间片段
}
}
}
text: 要播放的文本片段endOfStream: 是否为最后一段autoHangup: 播放完毕后是否挂断
总结
在这一章中,我们介绍了 RustPBX Go 的代码结构。
以及如何连接 RustPBX、发送命令、处理事件,并简单介绍了大模型交互和工具调用。
下一章我们将介绍如何添加自定义工具,实现一个天气查询 Agent。