代码详解

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

本章将详细解释代码逻辑,帮助读者理解如何使用 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: 挂断

注册回调函数

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

CallEvent

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

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

示意图

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

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

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

GoArch

在下一节,我们将看到如何使用这些 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 工具,实现挂断

参见:OpenAI Function Calling

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

    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,中间片段
        }
    }
}

TTSWriter 参数说明

  • text: 要播放的文本片段
  • endOfStream: 是否为最后一段
  • autoHangup: 播放完毕后是否挂断

总结

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

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

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