Đế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.
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).
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