Xây dựng AI Giọng Nói Thực Thời: Giải Pháp Hủy Âm Thanh và Quản Lý Phiên Làm Việc

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

Khi phát triển GoNoGo.team, một nền tảng AI kiểm tra ý tưởng khởi nghiệp qua giọng nói, tác giả nhận ra rào cản lớn nhất không phải là trí tuệ nhân tạo mà là vấn đề hủy âm thanh (echo) và feedback loop. Bài viết chia sẻ chi tiết các kỹ thuật như sử dụng cổng RMS hai tầng, cơ chế "cooldown" 1.5 giây và phục hồi phiên làm việc khi mất kết nối mạng.

Xây dựng AI Giọng Nói Thực Thời: Giải Pháp Hủy Âm Thanh và Quản Lý Phiên Làm Việc

Khi tôi bắt đầu phát triển GoNoGo.team — một nền tảng cho phép các đại lý AI phỏng vấn trực tiếp qua giọng nói để kiểm tra tính khả thi của ý tưởng khởi nghiệp — tôi nghĩ rào cản lớn nhất sẽ nằm ở trí tuệ nhân tạo. Đó là việc phân luồng nhiều đại lý, hay 40+ công cụ gọi hàm (function-calling).

Tôi đã lầm.

Thách thức thực sự nằm ở vấn đề hủy âm thanh (echo). Cụ thể: làm sao để ngăn một đại lý AI nghe chính mình nói, hoảng loạn và ngắt quãng câu chuyện của chính mình?

Sau hơn 500 phiên hội thoại và quá nhiều đêm thức trắng nhìn các sóng âm RMS, đây là những gì tôi đã học được.


Thiết lập: Đường ống "Giọng nói thành Giọng nói"

GoNoGo chạy trên API Gemini 2.5 Flash Live — một đường ống xử lý âm thanh trực tiếp (speech-to-speech). Không có bước chuyển đổi sang văn bản trung gian, không có lớp tổng hợp giọng nói (TTS) được gắn thêm sau đó. Âm thanh vào, âm thanh ra. Đơn giản.

Điều này cực kỳ quan trọng vì nó thay đổi mọi thứ về cách bạn xử lý âm thanh trên phía khách hàng. Bạn không làm việc với các bộ đệm văn bản. Bạn làm việc với dữ liệu âm thanh thô (PCM), đầu vào 16kHz từ mic trình duyệt, đầu ra 24kHz từ giọng nói của đại lý. Mã hóa Base64 qua WebSocket.

Mặt thu âm trên trình duyệt có thể nhìn thấy như sau:

// ScriptProcessorNode trong trình duyệt — các mảnh 512 mẫu (~32ms mỗi mảnh)
const scriptProcessor = audioContext.createScriptProcessor(512, 1, 1);

scriptProcessor.onaudioprocess = (event) => {
  const inputBuffer = event.inputBuffer.getChannelData(0);

  // Tính toán RMS để phát hiện giọng nói (VAD)
  const rms = Math.sqrt(
    inputBuffer.reduce((sum, sample) => sum + sample * sample, 0) / inputBuffer.length
  );

  // Ngưỡng VAD: 0.05 RMS
  if (rms < VAD_THRESHOLD) return;

  // Chuyển đổi Float32 PCM sang Int16
  const int16Buffer = new Int16Array(inputBuffer.length);
  for (let i = 0; i < inputBuffer.length; i++) {
    int16Buffer[i] = Math.max(-32768, Math.min(32767, inputBuffer[i] * 32768));
  }

  // Mã hóa Base64 và gửi qua WebSocket
  const base64Audio = btoa(String.fromCharCode(...new Uint8Array(int16Buffer.buffer)));
  ws.send(JSON.stringify({ type: 'audio_chunk', data: base64Audio }));
};

Đơn giản thôi. Chỉ đến khi AI bắt đầu nói.


Vấn đề Echo (Tại sao AEC của trình duyệt không đủ)

Các trình duyệt có tích hợp sẵn khả năng hủy âm thanh (Acoustic Echo Cancellation - AEC). Bạn chỉ cần bật nó khi gọi getUserMedia:

const stream = await navigator.mediaDevices.getUserMedia({
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    autoGainControl: true
  }
});

Cách này hoạt động rất tốt cho các cuộc gọi video giữa con người. Nó được thiết kế cho việc đó. Tuy nhiên, nó có một giả định cốt lõi baked vào: "Âm thanh bên đầu xa" đến qua thẻ <audio> hoặc Web Audio API mà trình duyệt biết về.

Khi bạn phát các mảnh PCM 24kHz từ WebSocket, giải mã thủ công và lên lịch qua các bộ đệm AudioContext? AEC của trình duyệt không biết âm thanh đó tồn tại. Nó không thể hủy những gì nó không thấy.

Kết quả là: đại lý AI bắt đầu nói, mic thu lại âm thanh phát ra, đại lý nghe thấy chính mình. Trong trường hợp tốt nhất, nó bị nhầm lẫn và lặp lại một câu. Trong trường hợp tồi tệ nhất — và điều này xảy ra liên tục trong các bản dựng đầu tiên — bạn tạo ra một vòng lặp phản hồi nơi đại lý ngắt câu của chính mình, nghe thấy sự ngắt quãng đó, cố gắng phản hồi lại, nghe thấy điều đó, và cả phiên làm việc sụp đổ.

Tôi gọi những lỗi này là disconnect mã 1011, đó là mã đóng kết nối WebSocket mà tôi liên tục thấy trong nhật ký.


Cổng RMS Hai Tầng

Giải pháp là một cổng RMS hai tầng (two-tier RMS gate) trên mặt thu âm. Ý tưởng đơn giản: đo mức độ to của những gì mic đang thu, và nếu có vẻ như chỉ là loa đang phát lại, hãy không gửi nó.

Tuy nhiên, "đơn giản" ẩn chứa rất nhiều trường hợp ngoại lệ.

Tầng 1: Loại bỏ hoàn toàn trong khi đại lý đang nói

Trong khi đại lý đang nói, tôi theo dõi trạng thái đó trên phía máy chủ và gửi nó về phía khách hàng. Trong thời gian này, âm thanh đầu vào bị loại bỏ hoàn toàn — không gửi mảnh nào cho Gemini.

let agentSpeaking = false;
let cooldownTimer: ReturnType | null = null;
const COOLDOWN_MS = 1500;
const COOLDOWN_THRESHOLD = 0.03; // Ngưỡng cao hơn trong thời gian chờ
const NORMAL_THRESHOLD = 0.05;   // Ngưỡng VAD bình thường

// Gọi khi luồng âm thanh của đại lý bắt đầu/dừng
function setAgentSpeakingState(speaking: boolean) {
  if (speaking) {
    agentSpeaking = true;
    if (cooldownTimer) clearTimeout(cooldownTimer);
  } else {
    agentSpeaking = false;
    // Bắt đầu thời gian chờ
    cooldownTimer = setTimeout(() => {
      cooldownTimer = null;
    }, COOLDOWN_MS);
  }
}

function shouldSendAudioChunk(rms: number): boolean {
  if (agentSpeaking) return false; // Loại bỏ cứng

  if (cooldownTimer !== null) {
    // Trong thời gian chờ: dùng ngưỡng cao hơn
    return rms > COOLDOWN_THRESHOLD;
  }

  return rms > NORMAL_THRESHOLD;
}

Tầng 2: Thời gian chờ 1.5 giây

Đây là phần tôi mất nhiều thời gian nhất để tìm ra. Khi đại lý ngừng nói, vẫn còn sự cộng hưởng của loa trong phòng. Mức độ RMS của âm thanh thu lại không giảm xuống ngay lập tức — nó giảm dần. Nhiễu nền trong một văn phòng gia đình thông thường ở mức 0.01–0.02 RMS. Nhưng trong 1-2 giây sau khi phát âm thanh ngừng, bạn đang thấy 0.025–0.04 RMS — cao hơn ngưỡng VAD bình thường.

Thời gian chờ sử dụng một ngưỡng cao hơn (0.03 so với 0.05) trong 1.5 giây sau khi đại lý nói xong. Điều này giúp bắt được sự giảm dần mà không cắt đứt người sáng lập nếu họ bắt đầu trả lời ngay lập tức.

Ngưỡng này được tinh chỉnh empirically (thực nghiệm) chứ không phải bằng lý thuyết. Tôi đã dành hàng ngày nghe lại các phiên làm việc để đo chính xác tốc độ mà sự cộng hưởng của phòng giảm ở các bộ mic khác nhau.


Phục hồi Phiên Làm Việc: Nửa Mặt Của Vấn Đề

Hủy âm thanh giải quyết vấn đề chất lượng. Phục hồi phiên làm việc giải quyết vấn đề độ tin cậy.

Các phiên làm việc Gemini Live thường xuyên bị mất kết nối. Lỗi mạng, chuyển đổi di động, hoặc Chrome quyết định làm việc quá mạnh mẽ với bộ nhớ — kết nối thất bại. Vào những ngày đầu, mất kết nối nghĩa là bắt đầu lại toàn bộ buổi phỏng vấn 30 phút. Người sáng lập sẽ bỏ cuộc. Tôi hoàn toàn hiểu điều đó.

Giải pháp: Lưu session handle trong Firestore và phục hồi khi kết nối lại.

# Backend FastAPI - Quản lý phiên
from google.genai.live import AsyncSession
from firebase_admin import firestore

async def get_or_create_session(
    project_id: str,
    user_id: str
) -> tuple[AsyncSession, bool]:
    db = firestore.client()
    session_ref = db.collection('sessions').document(f'{user_id}_{project_id}')
    session_doc = session_ref.get()

    if session_doc.exists:
        session_data = session_doc.to_dict()
        handle = session_data.get('resumption_handle')

        if handle:
            try:
                # Cố gắng phục hồi — Gemini sẽ quay lại chính xác nơi nó dừng lại
                session = await resume_gemini_session(handle)
                return session, True  # resumed=True
            except Exception:
                pass  # Thử phiên mới

    # Tạo phiên mới
    session = await create_gemini_session(project_id)
    session_ref.set({
        'created_at': firestore.SERVER_TIMESTAMP,
        'project_id': project_id
    })
    return session, False  # resumed=False

Khi phiên làm việc được phục hồi, Gemini khôi phục toàn bộ ngữ cảnh — mọi kết quả gọi hàm công cụ, mọi dữ liệu nghiên cứu thị trường, mọi nhân vật trong nhóm tập trung tổng hợp. Người sáng lập kết nối lại và đại lý nói: "Xin lỗi vì điều đó, chúng ta đang ở đâu vậy?" và thực sự biết mình đang nói về gì.


Vấn Đề Âm Thanh Điền

Một điều nữa mà ít người nhắc đến: bạn nên phát gì trong khi AI đang suy nghĩ?

Gemini 2.5 Flash rất nhanh. 300-500ms end-to-end là thực sự nhanh. Nhưng khi đại lý đang thực thi một công cụ — quét trang web đối thủ với Playwright, chạy thu thập dữ liệu Reddit, tính toán kinh tế đơn vị — bạn có thể có khoảng trống 3-8 giây.

Sự im lặng trong một cuộc trò chuyện giọng nói cảm thấy bị hỏng. Người dùng thường nghi ngờ kết nối đã bị mất.

Giải pháp: Âm thanh bổ trợ (filler audio) đã được tính toán trước. Các cụm từ ngắn như "xin hãy đợi một chút" hoặc "tôi sẽ tra cứu điều đó" trong 17 ngôn ngữ, lưu dưới dạng các mảnh PCM, được phát khi thời gian thực thi công cụ vượt quá khoảng 800ms. Đại lý được kích hoạt bằng tín hiệu văn bản (không dùng proactive_audio, vì nó có một regression gây ra lỗi phát lại kép — bị tắt hoàn toàn, hãy dùng tín hiệu văn bản thay thế).

Nghe có vẻ đơn giản. Nó đã loại bỏ khoảng 40% các tin nhắn hỗ trợ "ứng dụng bị hỏng".


Những Gì Tôi Sẽ Làm Khác

  1. Bắt đầu với cổng echo, không phải logic AI: Tôi đã dành hàng tuần xây dựng việc phân luồng đa đại lý đẹp đẽ trước khi có thể demo một cách đáng tin cậy. Thứ tự sai.
  2. Cố định các giá trị RMS từ ngày đầu tiên: Ghi nhật ký chúng. Mỗi phiên. Bạn không thể tinh chỉnh điều gì bạn không thấy.
  3. Test trên phần cứng kém: Thiết lập phát triển của tôi có mic tốt và khoảng cách vật lý từ loa. Hầu hết người dùng có mic laptop cách loa laptop chỉ 30cm. Hãy xây dựng cho điều đó.
  4. Di động là một hành tinh khác: iOS Safari xử lý vòng đời AudioContext theo những cách sẽ khiến bạn tự hỏi về sự nghiệp của mình. Nhưng đó là chủ đề cho một bài viết khác.

Kết Quả

Sau khi giải quyết các vấn đề này — cổng RMS hai tầng, thời gian chờ 1.5s, phục hồi phiên làm việc, âm thanh bổ trợ — GoNoGo chạy các phiên hội thoại giọng nói 15-45 phút với người sáng lập thật sự, trên 21 ngôn ngữ, với 3 đại lý AI chuyển giao cho nhau giữa cuộc trò chuyện. Các lỗi disconnect mã 1011 gần như biến mất.

Hạ tầng giọng nói trở nên vô hình, đó chính là điều nó nên làm.

Nếu bạn đang xây dựng bất cứ thứ gì với mic trình duyệt + âm thanh AI thực thời: thách thức lớn nhất của bạn là gì? Tôi thực sự tò mò liệu vấn đề echo có phải là phổ quát hay tôi đã làm điều gì sai rất nhiều trong những ngày đầu.

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 ↗