Xây dựng giao diện giọng nói liên tục với OpenAI Realtime API

05 tháng 4, 2026·6 phút đọc

Bài viết cung cấp hướng dẫn kỹ thuật chi tiết về cách xây dựng hệ thống lệnh giọng nói từ đầu đến cuối, xử lý mọi thứ từ dữ liệu microphone thô đến việc thực thi công cụ. Khám phá kiến trúc ba lớp, các thách thức trong xử lý âm thanh PCM và cách tích hợp WebSocket Relay để giao tiếp hiệu quả với OpenAI.

Xây dựng giao diện giọng nói liên tục với OpenAI Realtime API

Xây dựng giao diện giọng nói liên tục với OpenAI Realtime API

Dưới đây là một hướng dẫn kỹ thuật về cách hệ thống lệnh giọng nói ABD Assistant hoạt động từ đầu đến cuối, xử lý dữ liệu từ các byte thô của microphone đến việc thực thi công cụ (tool execution).

Kiến trúc cốt lõi

Hệ thống bao gồm ba thành phần chính: một lớp thu âm thanh Web Audio trên trình duyệt, một bộ chuyển tiếp (relay) WebSocket sử dụng Express, và OpenAI Realtime API đóng vai trò là "bộ não" xử lý giọng nói.

Trình duyệt truyền trực tuyến dòng âm thanh PCM đến OpenAI thông qua một kết nối WebSocket được duy trì trong suốt phiên làm việc. OpenAI thực hiện phát hiện hoạt động giọng nói (Voice Activity Detection - VAD) phía máy chủ, chuyển đổi giọng nói thành văn bản một cách gia tăng, chạy mô hình LLM trên lịch sử hội thoại, và truyền lại các token âm thanh ngay khi chúng được tạo ra.

Điều này đồng nghĩa với việc bạn không cần logic phát hiện sự im lặng phía client, không cần quản lý lượt nói và không cần bước chuyển đổi văn bản riêng biệt — tất cả nằm trong một đường dẫn dữ liệu, do máy chủ điều khiển hoàn toàn.

Thu thập âm thanh: Phần khó nhất

Việc thu thập âm thanh đúng cách là nơi mà hầu hết các bản triển khai thất bại. Ràng buộc quan trọng là: OpenAI Realtime API yêu cầu định dạng mono PCM ở tần số 24kHz với số nguyên có dấu 16-bit. Trong khi đó, MediaRecorder của trình duyệt lại tạo ra định dạng audio/webm hoặc audio/opus — một định dạng hoàn toàn khác.

Giải pháp nằm ở việc sử dụng ScriptProcessorNode (hoặc AudioWorklet):

const processor = audioContext.createScriptProcessor(4096, 1, 1);
processor.onaudioprocess = (ev) => {
  const inputData = ev.inputBuffer.getChannelData(0);
  const pcmData = new Int16Array(inputData.length);
  for (let i = 0; i < inputData.length; i++) {
    const s = Math.max(-1, Math.min(1, inputData[i]));
    pcmData[i] = s < 0 ? s * 32768 : s * 32767;
  }
  const base64 = btoa(String.fromCharCode(...new Uint8Array(pcmData.buffer)));
  ws.send(JSON.stringify({ type: "input_audio_buffer.append", audio: base64 }));
};

Mỗi đoạn dữ liệu được mã hóa base64 và gửi dưới dạng tin nhắn WebSocket. Không có bộ đệm, không có gom nhóm (batching) — chỉ là truyền phát liên tục.

Bộ chuyển tiếp WebSocket (Relay)

URL WebSocket của OpenAI Realtime API yêu cầu API key nằm trong header, nhưng các client WebSocket trên trình duyệt không thể đặt header này. Một máy chủ Express mỏng sẽ xử lý việc chuyển tiếp:

wss.on("connection", (clientWs, req) => {
  const apiKey = new URL(req.url, `http://${req.headers.host}`).searchParams.get("api_key");
  if (apiKey !== process.env.VOICE_API_KEY) { clientWs.close(4401); return; }

  const openAiWs = new WebSocket("wss://api.openai.com/v1/realtime?model=gpt-realtime-2025-08-28", {
    headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` }
  });

  openAiWs.on("message", (data) => clientWs.send(data.toString()));
  clientWs.on("message", (data) => openAiWs.send(data.toString()));
  clientWs.on("close", () => openAiWs.close());
});

Tất cả việc truyền tin nhắn đều là thô — không có chuyển đổi nào. Bộ chuyển tiếp này là không trạng thái (stateless).

Cấu hình phiên Realtime API (GA)

Phiên bản Realtime API chính thức (Generally Available - GA) sử dụng lược đồ phiên khác với bản beta. Payload session.update phải bao gồm type: "realtime" và các trường được tổ chức dưới audio.inputaudio.output:

const sessionUpdate = {
  type: "session.update",
  session: {
    type: "realtime",
    model: "gpt-realtime-2025-08-28",
    audio: {
      input: {
        format: { type: "audio/pcm", rate: 24000 },
        turn_detection: { type: "server_vad", prefix_padding_ms: 1000, silence_duration_ms: 400 }
      },
      output: { format: { type: "audio/pcm", rate: 24000 }, voice: "alloy" }
    },
    output_modalities: ["audio"]
  }
};

Các trường chính dễ gây nhầm lẫn khi triển khai:

  • session.type phải là "realtime" (không phải "conversation").
  • modalities không phải là trường cấp cao nhất (hãy sử dụng output_modalities: ["audio"]).
  • voice nằm dưới audio.output.

Xử lý sự kiện

API GA phát ra các tên sự kiện khác so với bản beta. Dưới đây là các sự kiện quan trọng cần xử lý:

  • conversation.item.input_audio_transcription.delta → bản ghi âm thanh của người dùng (từng từ một).
  • response.output_audio.delta → đoạn âm thanh AI (byte nằm trong event.delta, không phải event.audio).
  • response.output_audio_transcript.delta → bản chuyển ngữ của AI.
  • input_audio_buffer.speech_started / speech_stopped → thay đổi trạng thái VAD.
  • response.done → phản hồi hoàn tất, phiên tiếp tục.

Gọi hàm (Tool Calling)

Realtime API hỗ trợ gọi hàm một cách tự nhiên (native) ngay trong phiên. Bạn định nghĩa các công cụ trong session.update:

sessionUpdate.session.tools = toolDefinitions.map(t => ({ type: "function", ...t }));

Khi mô hình phát ra response.done với output[0].type === "function_call", hãy thực thi công cụ phía client, sau đó tiêm kết quả lại:

const result = await executeTool(name, args);
ws.send(JSON.stringify({
  type: "conversation.item.create",
  item: { type: "function_call_output", call_id, output: result }
}));
ws.send(JSON.stringify({ type: "response.create" }));

Hành động này sẽ kích hoạt mô hình phản hồi bằng lời nói để xác nhận hành động vừa thực hiện.

Phát lại âm thanh: Hàng đợi tuần tự

Các đoạn âm thanh streaming đến có thể không đúng thứ tự và bị chồng lên nhau nếu phát ngay lập tức. Một hàng đợi FIFO đơn giản với cờ isPlaying sẽ giải quyết vấn đề này — mỗi đoạn được giải mã thành AudioBuffer và phát thông qua BufferSourceNode; lệnh gọi lại onended sẽ kích hoạt đoạn tiếp theo.

Thách thức và sự đánh đổi

  • Hủy bỏ ScriptProcessorNode: Nên sử dụng AudioWorklet trong môi trường sản xuất; ScriptProcessorNode vẫn hoạt động nhưng Chrome hiện cảnh báo về việc loại bỏ nó.
  • Không hỗ trợ đa giọng: output_modalities: ["audio"] buộc phải sử dụng một mô hình giọng nói duy nhất; không có cách nào lấy cả văn bản và âm thanh cùng lúc từ cùng một đầu ra mô hình.
  • Không có bản chuyển ngữ streaming đầu vào: Bản chuyển ngữ đầu vào đến dưới dạng input_audio_transcription.delta, không phải stream từng từ một như đầu ra âm thanh.
  • Thời điểm thực thi công cụ: Khi một công cụ thực thi, việc ghi âm sẽ tạm dừng; mô hình sẽ đợi lệnh gọi response.create trước khi tiếp tục.

Dành cho dự án của bạn

Những phần chỉ đặc thù cho ABD là định nghĩa công cụ và ánh xạ executeTool — bạn chỉ cần thay thế chúng bằng bề mặt API của riêng mình. Mọi thứ khác — thu thập âm thanh, chuyển tiếp WebSocket, quản lý phiên, phát lại âm thanh — đều là hạ tầng có thể tái sử dụng.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗