面向 Delphi 和 .NET 的实时语音代理

· 组件
面向 Delphi 和 .NET 的实时语音代理

输入一个问题,读取回答,这是如今大多数应用与语言模型对话的方式。实时 API 改变了这一点。OpenAI Realtime 和 Gemini Live 会在用户说话的同时接收语音,并用自己的语音作答,速度快到足以维持一场对话。要靠手工实现这一切,意味着要在两个方向上流式传输音频、判断用户何时说完、在被打断时停止代理,并在这一切进行的同时运行工具。

sgcWebSockets 2026.10 新增了 TsgcAIVoiceAgent,一个能完成所有这些工作的组件。它会监听、检测用户何时说完一个话轮、开口回应,并调用你的工具。它面向 Delphi 7 到 13 以及 .NET。

TsgcAIVoiceAgent 实战:话轮检测、barge-in、实时转录和工具调用。YouTube 上也有。

快速开始

选择提供商,设置 API 密钥、一个语音和指令,处理 OnTranscript 并调用 Start。这样就是一个可用的语音助手了。

oVoice := TsgcAIVoiceAgent.Create(nil);
oVoice.Provider := vapOpenAI;
oVoice.OpenAIOptions.ApiKey := 'API_KEY';
oVoice.OpenAIOptions.Voice := 'alloy';
oVoice.Instructions := 'You are a friendly assistant. Answer briefly.';
oVoice.OnTranscript := oVoiceTranscript;
oVoice.OnError := oVoiceError;
oVoice.Start;

OnTranscript 会提供对话双方,即用户和代理的实时转录文字,让屏幕能够跟上正在说的内容:

procedure TForm1.oVoiceTranscript(Sender: TObject;
  aRole: TsgcAIVoiceAgentRole; const aText: string; aFinal: Boolean);
begin
  if aFinal then
  begin
    if aRole = varUser then
      Memo1.Lines.Add('You: ' + aText)
    else
      Memo1.Lines.Add('Agent: ' + aText);
  end;
end;

OpenAI Realtime 或 Gemini Live,WebSocket 或 WebRTC

支持两个提供商,OpenAI Realtime 和 Gemini Live。切换只需要一个属性和相应的选项:

oVoice.Provider := vapGemini;
oVoice.GeminiOptions.ApiKey := 'GEMINI_API_KEY';
oVoice.Start;

会话通过 WebSocket 运行。使用 OpenAI 时,也可以通过 WebRTC 运行:

oVoice.Provider := vapOpenAI;
oVoice.Transport := vatWebRTC;
oVoice.OpenAIOptions.ApiKey := 'API_KEY';
oVoice.Start;

内置音频,或使用你自己的

在 Windows、Android 和 iOS 上,麦克风和扬声器是内置的,所以上面的快速开始部分就是你需要的全部内容。在其他任何平台上,或者当音频来自其他地方时,把设备关闭。你的应用通过 SendAudio 发送音频,并在 OnAudioOutput 中接收回答。

oVoice.Audio.UseDevice := False;
oVoice.Audio.Format := vafPCM16;
oVoice.Audio.SampleRate := 16000;
oVoice.Start;

// ... 20 ms of PCM16 mono at 16 kHz = 640 bytes
SetLength(vChunk, 640);
FillChar(vChunk[0], Length(vChunk), 0);
oVoice.SendAudio(vChunk);

接听电话

电话系统使用 G.711,语音代理也是如此。将音频格式设置为 8 kHz 的 mu-law 或 A-law,通话的字节就能直接进、直接出,你的代码里不需要任何转换。

oVoice.Provider := vapOpenAI;
oVoice.OpenAIOptions.ApiKey := 'API_KEY';
oVoice.Instructions := 'You answer the phone of a restaurant and take bookings.';
oVoice.Audio.UseDevice := False;
oVoice.Audio.Format := vafG711ULaw;
oVoice.OnAudioOutput := oVoicePhoneAudioOutput;
oVoice.Start;
procedure TForm1.DoPhoneAudioReceived(const aData: TBytes);
begin
  // ... G.711 mu-law, 8 kHz, as it arrives from the RTP stream of the call
  oVoice.SendAudio(aData);
end;

procedure TForm1.oVoicePhoneAudioOutput(Sender: TObject;
  const aData: TBytes);
begin
  // ... already G.711 mu-law at 8 kHz: send it back to the caller
  SendToPhone(aData);
end;

话轮检测

话轮检测决定用户何时说完话,代理应该开始回答。共有四种模式。

oVoice.TurnDetection.Mode := vtdServerVAD;
oVoice.TurnDetection.Threshold := 0.6;
oVoice.TurnDetection.PrefixPaddingMs := 300;
oVoice.TurnDetection.SilenceDurationMs := 700;
oVoice.TurnDetection.Mode := vtdSemanticVAD;
oVoice.TurnDetection.Eagerness := 'low';

使用按键说话时,在用户松开按钮时提交音频并请求回答:

procedure TForm1.btnTalkMouseUp(Sender: TObject);
begin
  // ... TurnDetection.Mode is vtdManual
  oVoice.CommitAudio;
  oVoice.CreateResponse;
end;

Barge-in

人们会打断别人说话,一个好的语音代理应该允许这样做。启用 barge-in 后,当用户开始说话时,代理会停止说话,回答也会被取消。回答还会被截断到用户实际听到的部分,这样对话记录就不会声称代理说了从未播放过的话。

oVoice.BargeIn := True;
oVoice.OnTurnStarted := oVoiceTurnStarted;
oVoice.OnTurnEnded := oVoiceTurnEnded;
oVoice.OnBargeIn := oVoiceBargeIn;
procedure TForm1.oVoiceBargeIn(Sender: TObject);
begin
  Memo1.Lines.Add('The user interrupted the agent.');
end;

工具

语音代理只有在能够执行操作时才会真正有用。声明一个工具,包含名称、描述和参数的 JSON schema,然后在 OnToolCall 中回应它。

procedure TForm1.StartWeatherAssistant;
var
  oTool: TsgcAIVoiceAgentTool;
begin
  oVoice.Provider := vapOpenAI;
  oVoice.OpenAIOptions.ApiKey := 'API_KEY';
  oVoice.Instructions := 'You are a weather assistant. ' +
    'Call get_weather to know the weather of a city before answering.';

  oVoice.Tools.Clear;
  oTool := oVoice.Tools.Add;
  oTool.Name := 'get_weather';
  oTool.Description := 'Returns the current weather of a city.';
  oTool.Parameters := '{"type":"object","properties":' +
    '{"city":{"type":"string","description":"Name of the city"}},' +
    '"required":["city"]}';

  oVoice.OnToolCall := oVoiceWeatherToolCall;
  oVoice.OnTranscript := oVoiceTranscript;
  oVoice.Start;
end;
procedure TForm1.oVoiceWeatherToolCall(Sender: TObject;
  const aCallId, aName, aArguments: string; var aResult: string;
  var aHandled: Boolean);
var
  oJSON: TsgcJSON;
  vCity: string;
begin
  if aName <> 'get_weather' then
    Exit; // ... not mine: the MCP client, if any, gets the call

  oJSON := TsgcJSON.Create(nil);
  Try
    oJSON.Read(aArguments);
    vCity := '';
    if Assigned(oJSON.Node['city']) then
      vCity := oJSON.Node['city'].Value;
  Finally
    oJSON.Free;
  End;

  // ... replace with a call to your weather service
  aResult := '{"city":"' + vCity + '","temperature":21,"sky":"clear"}';
  aHandled := True;
end;

工具也可以来自 MCP 服务器。指定一个 MCP 客户端,它的工具就会自动提供给代理:

oMCP := TsgcWSAPIClient_MCP.Create(nil);
oMCP.MCPOptions.HttpOptions.URL := 'https://localhost:5001/mcp';

oVoice.MCPClient := oMCP;
oVoice.Start;

演示程序:语音购物助手

Demos\15.AI\02.Applications\09.VoiceAgent 中的 Delphi 演示程序是一个可以与之对话的商店助手。它基于保存在 TDataSet 中的商品目录运行,并为代理提供了四个工具:find_products 用于搜索目录,get_product 用于读取某个商品,update_stock 用于修改库存,count_low_stock 用于列出需要补货的商品。问问它什么快没货了,然后让它给某个商品补货,看着表格在它回答的同时发生变化。

可用性

TsgcAIVoiceAgent 将随 sgcWebSockets 2026.10 一起发布,面向 Delphi 7 到 13 以及 .NET,包含在 Enterprise 版本中。本次发布的其他新增内容在 sgcWebSockets 2026.10 的文章中,其余的 AI 组件在 AI 产品页面上。

延伸阅读

有疑问或反馈?联系我们。回复你的会是真正编写这些代码的人。