Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 4 — Thiết lập môi trường: dựng pipeline voice agent đầu tiên

Đã đọc 0/9 mục · còn khoảng 4 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Đến lúc chạm tay vào code. Bài này dựng một pipeline cascaded tối giản chạy được: nhận audio → ASR → LLM → TTS → phát audio. Mục tiêu là có bộ khung để bạn cắm engine thật vào.

Chuẩn bị

  • Runtime Python 3.10+ (hoặc Node — nguyên tắc giống nhau).
  • Khóa API cho ASR, LLM, TTS (đặt trong biến môi trường, không hardcode).
  • Thư viện âm thanh để đọc/ghi audio và một transport (WebRTC cho web, hoặc thư viện SIP nếu làm điện thoại).
# .env — không commit file này
ASR_API_KEY=...
LLM_API_KEY=...
TTS_API_KEY=...
LLM_MODEL=your-model-id

Các bước dựng pipeline

  • Lớp nhận audio: mở stream từ mic/WebRTC, cắt thành chunk 20ms, chuẩn hóa 16kHz mono.
  • Lớp ASR streaming: đẩy chunk vào ASR, lắng nghe partial/final, áp endpointing.
  • Lớp LLM: khi có final + endpoint, gọi LLM (streaming) với system prompt + lịch sử hội thoại.
  • Lớp TTS streaming: nhận token LLM theo câu, sinh audio, phát ra.
  • Lớp orchestration: quản lý trạng thái (đang nghe / đang nghĩ / đang nói), xử lý barge-in.
class VoiceAgent:
    def __init__(self, asr, llm, tts):
        self.asr, self.llm, self.tts = asr, llm, tts
        self.history = [{"role": "system", "content": SYSTEM_PROMPT}]
        self.state = "listening"

    async def on_audio(self, chunk):
        result = await self.asr.push(chunk)
        if result.is_final and result.endpoint:
            self.state = "thinking"
            self.history.append({"role": "user", "content": result.text})
            await self.respond()

    async def respond(self):
        self.state = "speaking"
        buf, reply = "", ""
        async for tok in self.llm.stream(self.history):
            buf += tok; reply += tok
            if ends_sentence(buf):
                async for a in self.tts.stream(buf):
                    self.play(a)
                buf = ""
        self.history.append({"role": "assistant", "content": reply})
        self.state = "listening"

Barge-in: cho phép khách ngắt lời

Khi state = "speaking" mà VAD phát hiện khách bắt đầu nói:

  • Dừng phát TTS ngay (xóa buffer audio đang phát).
  • Hủy request LLM/TTS đang chạy (cancel token).
  • Chuyển state về "listening", bắt đầu ASR cho lượt mới.
Thiếu bước hủy → agent vừa nói vừa nghe chồng lên, hội thoại loạn.

System prompt cho voice khác với text

Voice cần prompt hướng dẫn agent:

  • Trả lời ngắn (1-2 câu), vì người nghe không đọc lại được.
  • Không dùng markdown, bullet, emoji (đọc lên nghe kỳ).
  • Đọc số/địa chỉ rõ ràng, xác nhận lại thông tin quan trọng.
  • Xưng hô nhất quán ("em/mình" với khách).
Một system prompt voice mẫu (rút gọn):

Bạn là trợ lý tổng đài. Luôn trả lời bằng tiếng Việt, tối đa 2 câu ngắn.
Không dùng markdown, bullet, emoji. Đọc số điện thoại/tiền/ngày rõ ràng.
Xác nhận lại thông tin quan trọng trước khi thực hiện hành động ghi.
Nếu không nghe rõ, lịch sự hỏi lại. Xưng "em", gọi khách là "mình/anh/chị".

Quản lý lịch sử hội thoại và ngữ cảnh

Voice agent phải giữ lịch sử để hiểu tham chiếu ("đổi sang giờ đó"). Nhưng lịch sử dài làm tăng độ trễ LLM và chi phí. Các bước cân bằng:

  • Giữ đầy đủ vài lượt gần nhất, tóm tắt phần cũ.
  • Ghim các thông tin đã xác nhận (tên, mã đơn) vào một khối "trạng thái" riêng thay vì để trôi trong lịch sử.
  • Xóa các partial/nhiễu ASR khỏi lịch sử, chỉ lưu final đã sạch.

Đo độ trễ ngay từ đầu

Gắn timestamp tại mỗi ranh giới: audio-in, ASR-final, LLM-first-token, TTS-first-audio, audio-out. Ghi hiệu số giữa chúng cho từng lượt. Có dữ liệu này ngay từ bản dựng đầu tiên giúp bạn tối ưu đúng chỗ thay vì đoán mò khi hệ thống chậm.

Checklist chạy được lần đầu

  • [ ] Audio in đã chuẩn hóa 16kHz mono?
  • [ ] ASR trả final + có endpointing?
  • [ ] LLM stream token, TTS phát theo câu?
  • [ ] Barge-in dừng+hủy đúng?
  • [ ] State machine không kẹt ở "thinking"/"speaking"?
  • [ ] Log timestamp mỗi khối để đo trễ?

Sai lầm thường gặp

  • Không quản lý state. Không có state machine, barge-in và lượt nói sẽ chồng chéo.
  • Quên hủy tác vụ khi barge-in. Dừng loa thôi chưa đủ; phải cancel cả LLM và TTS.
  • Prompt kiểu text. Trả lời dài dòng, có bullet → nghe rất tệ qua giọng nói.
  • Hardcode API key. Lộ khóa và không đổi được giữa môi trường dev/prod.
  • Không log timestamp từng khối. Khi trễ, bạn không biết nghẽn ở ASR, LLM hay TTS.
Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi