Skip to content
··11 phút đọc

Mình đã rollback 2.724 dòng code sau khi một thay đổi AI audio làm hỏng production

Mình đã deploy một thay đổi mà lẽ ra là nâng cấp cho hệ thống giọng nói của nền tảng podcast. Sáu ngày và vài commit sau, mình xóa 2.724 dòng code và rollback về phiên bản ổn định. Đây là chuyện đã xảy ra và bài học mình rút ra khi testing thay đổi AI trên production.

Đáng lẽ mình nên viết bài này sớm hơn. Nhưng toàn bộ quá trình tốn nhiều thời gian gỡ rối hơn mình tưởng, và mình muốn chắc chắn nắm rõ câu chuyện trước khi chia sẻ.

Vào ngày 28 tháng 4, 2026, mình deploy một thay đổi mà mình nghĩ là cải tiến cho hệ thống text-to-speech của DIALØGUE. Một voice model khác trông khá hứa hẹn trên lý thuyết. Mình đã nghe — và tự mình nghe thấy — rằng giọng của model hiện tại bị lệch trên các podcast dài. Nên mình đã chuyển sang model mới.

Đến ngày 30 tháng 4, mình đã xóa 2.724 dòng code và rollback về path ổn định của ngày 22 tháng 4.

Một podcast của người dùng thật sự bị kẹt. Hệ thống không thể generate audio được. Nó đã đánh dấu AUDIO_FAILED rồi bắn ra lỗi 409 Conflict khi retry, mà lúc đầu mình tưởng đó chính là bug. Hóa ra đó chỉ là triệu chứng — podcast đã ở trạng thái failed từ trước khi retry diễn ra. Vấn đề thực tế đơn giản và tệ hơn nhiều: một segment bị timeout trên cấu hình mới. Fallback cũng bị timeout. Và rồi hệ thống cứ thế bỏ cuộc.

Tệ hơn nữa, đây là một script dài tới khoảng 65 phút audio. Median podcast trên nền tảng là 26 phút. Mình đã cho một script dài gấp đôi mức đó chạy qua một hệ thống chưa từng generate thành công bất kỳ thứ gì gần bằng kích thước đó. Đó mới là rủi ro thực sự — và thay đổi model chính là thứ phơi bày nó ra.

Mình phải thú thật, đây không phải khoảnh khắc tốt nhất của mình với tư cách một builder. Mình đẩy một thay đổi cấu hình mà không có loại stress testing mà một hệ thống production đáng được hưởng. Đây là chuyện đã xảy ra, thứ gì đã hỏng, và mình học được gì.

DIALØGUE làm gì

Nếu bạn chưa biết, DIALØGUE là một AI podcast generator. Bạn đưa cho nó một chủ đề, một file PDF, hoặc một tập show, và nó tạo ra một podcast hội thoại hai người dẫn. Toàn bộ chạy trên Gemini text-to-speech synthesis của Google.

Audio pipeline hoạt động như sau:

  1. Generate một outline từ source material
  2. Mở rộng outline thành script hoàn chỉnh
  3. Chia script thành các segment
  4. Synthesize audio cho từng segment bằng Gemini TTS
  5. Ghép các segment lại, normalize loudness, và upload file MP3 cuối cùng

Production default là một Gemini TTS model hoạt động tốt cho hầu hết trường hợp. Nhưng script càng dài, bạn càng nghe rõ giọng bị lệch giữa các segment. Host A ở segment một nghe không giống hệt Host A ở segment sáu. Với podcast 25 phút, nó đáng chú ý nhưng bạn quen dần. Với bất kỳ thứ gì gần chạm 40 phút, nó khá tệ.

Mình quyết định thử một cấu hình model khác. Ý tưởng là cải thiện độ nhất quán giọng nói trên các podcast dài hơn.

Mình đã xây gì

Khoảng một tuần cuối tháng 4, mình push một loạt commit để thử cấu hình model khác. Đây không phải thay đổi một dòng code đơn giản. Mình xây cả một sự dịch chuyển kiến trúc:

  • Chuyển TTS model default sang cấu hình mới
  • Thêm fallback chain — nếu model chính bị timeout, fallback về model production ổn định
  • Xây hệ thống QA cấp chunk: chia transcript thành đơn vị nhỏ hơn, synthesize từng cái, validate chất lượng audio bằng ffmpeg analysis
  • Thêm workflow progress tracking để UI có thể hiển thị trạng thái synthesis theo từng segment
  • Củng cố retry logic — ba lần thử mỗi chunk, exponential backoff
  • Thêm long-form audio quality gate chặn COMPLETE trừ khi file MP3 lắp ráp cuối cùng vượt qua audio QA

Ý tưởng là chuyển từ "generate audio theo segment và mong nó nghe nhất quán" sang "chunk transcript, QA từng chunk, retry phần hỏng, validate file cuối."

Đến ngày 28 tháng 4, code được deploy lên production. Unit test pass. Integration test pass. Thứ mình không có — và lẽ ra phải có — là một load test chạy full export 50+ phút trên cấu hình mới. Mình đã bỏ qua test quan trọng nhất.

Thứ gì đã hỏng

Gần như ngay lập tức, một podcast production bị lỗi.

Đó là một tập dài — phân tích capex Big Tech dài khoảng 65 phút audio. Hệ thống bước vào phần audio và trả về AUDIO_FAILED.

Lúc đầu, mình thấy lỗi 409 Conflict và tưởng orchestrator có bug. Hóa ra 409 là triệu chứng phụ — một retry attempt sau khi podcast đã bị đánh dấu failed. Lỗi đầu tiên đơn giản hơn nhiều.

Segment 0 bị timeout trên cấu hình model mới. Timeout đặt ở 60 giây. Fallback model — cùng model mà stable path dùng — cũng bị timeout. Hệ thống đánh dấu podcast là failed và chuyển sang bước tiếp theo.

Fallback lẽ ra phải hoạt động. Nhưng fallback được gọi với cùng budget timeout 60 giây, trên cùng segment lớn, trong cùng request context đã tiêu tốn thời gian cho primary call thất bại. Còn stable path gọi cùng model đó từ đầu với đầy đủ timeout available và xử lý từng segment riêng lẻ. Cùng model, điều kiện khác nhau — đó là lý do stable version xử lý ổn còn fallback thì không.

Mình disable chunk QA system trên production bằng một dòng code. Cũng không fix được timeout. Cấu hình mới vẫn bị timeout trên những segment mà stable model xử lý bình thường.

Chà, đây là kiểu khoảnh khắc bạn phải chọn lựa: tiếp tục debug path mới, hay quay lại thứ đang hoạt động.

Rollback

Ngày 30 tháng 4, mình đưa ra quyết định.

Commit rollback (4a5bfc8) xóa 2.724 dòng code:

  • Toàn bộ module TTS chunker
  • Audio quality analysis gates
  • Workflow progress tracking
  • Bộ test suite chunk QA retry
  • Cấu hình fallback model chain

Và thêm 321 dòng để giữ lại những phần quan trọng — localized voice prompt phrasing, frontend compatibility, và regression coverage cho stable path.

Đó là rất nhiều dòng code để xóa. Thành thật mà nói, nó cảm giác như thừa nhận thất bại. Nhưng không phải. Đó là chọn path đúng về lâu dài thay vì thứ trông giống tiến bộ nhưng chưa sẵn sàng.

Sau khi rollback, mình:

  1. Rebuild shared base Docker image
  2. Redeploy service generate-speech
  3. Reset podcast bị failed về bước script approval
  4. Nhấn "Generate Audio" qua UI bình thường

Kết quả:

  • Model: Gemini TTS (cấu hình production ổn định)
  • Segments: 6 trên 6 hoàn thành
  • Duration: 1.527 giây — khoảng 25 phút
  • Final MP3: 30,5 MB
  • Status: COMPLETE

Podcast bị kẹt hai ngày đã hoàn thành trong khoảng 11 phút sau rollback. Tập podcast của người dùng được ship — trễ hai ngày so với dự kiến, nhưng hoàn chỉnh.

Một điều về con số 65 phút: đó là ước lượng dựa trên độ dài script thô, không phải output cuối cùng. Sau khi reset, script đi qua pipeline rút ngắn bình thường, và audio lắp ráp cuối cùng ra khoảng 25 phút. Script gốc còn dài hơn nữa — đó cũng là một phần lý do nó bị lỗi.

Dữ liệu

Trước khi quyết định làm gì tiếp theo, mình muốn có số liệu cụ thể. Không phải cảm tính — mà là dữ liệu. Mình query production database để lấy thông tin về duration podcast.

CohortCountMedian Durationp90Max
30 ngày gần nhất826 phút31 phút34 phút
90 ngày gần nhất7227 phút36 phút45 phút
Tất cả12026 phút30 phút45 phút

Vậy median podcast khoảng 26 phút. p90 khoảng 34-36 phút. Podcast dài nhất từng hoàn thành là 45 phút.

Và đây là dữ liệu khó chịu hơn — những podcast đạt đến giai đoạn audio, phân theo estimated duration:

Estimated DurationReached AudioCompletedFailedIn Progress
Dưới 15 phút181701
15-30 phút363123
30-40 phút242301
40-50 phút3120
50+ phút2020

"In Progress" bao gồm podcast đã bắt đầu nhưng không bao giờ hoàn thành — người dùng bỏ dở, cancel giữa chừng, hoặc để pending. Hai podcast thử nghiệm ở mức 50+ phút. Cả hai đều failed ở giai đoạn audio. Không cái nào hoàn thành.

Chưa có podcast production hoàn thành nào đạt 50 phút. Hệ thống hoạt động tốt cho khoảng 15-40 phút. Trên 40 phút, rủi ro tăng mạnh. Và trên 50 phút, về cơ bản là vùng chưa được kiểm chứng.

Bài học mình rút ra

1. Thay đổi cấu hình cần stress testing cấp production

Mình phải thành thật về điều này. Cấu hình model mới có lẽ hoạt động ổn với test prompt ngắn. Nhưng trên production, với script thật và ràng buộc timeout thật, nó failed trên những segment mà cấu hình ổn định xử lý không vấn đề.

Mình không benchmark đàng hoàng trước khi deploy. Một test prompt ngắn sẽ thành công. Một podcast 65 phút với nhiều segment dưới timeout 60 giây là bối cảnh hoàn toàn khác. Khoảng cách giữa hai kịch bản đó chính là nơi sự cố production tồn tại.

Mình có thể sai về bản thân cấu hình — có lẽ nó chỉ cần tuning thêm hoặc timeout dài hơn. Nhưng điểm chính vẫn đứng vững: mình thay đổi một core production dependency mà không có stress testing mà một hệ thống người dùng thực sự dựa vào đáng được hưởng.

2. Giọng không nhất quán mới là vấn đề thực, không phải timeout

Timeout là thất bại nhìn thấy được. Nhưng lý do mình thử cấu hình khác ngay từ đầu là giọng không nhất quán. Ngay cả trên stable model, giọng cũng thay đổi giữa các lần synthesis. Host A ở segment một không giống hệt Host A ở segment sáu.

Với podcast ngắn, điều này hầu như không đáng chú ý. Với podcast dài hơn, nó tích lũy. Và với podcast 50+ phút — mà, nhắc lại, chưa ai hoàn thành trên production — chắc chắn sẽ rất rõ ràng.

Cách tiếp cận chunked đang cố giải quyết điều này bằng cách làm mỗi chunk nhỏ hơn và kiểm soát hơn. Đó là hướng đi đúng, mình nghĩ. Chỉ là implementation chưa sẵn sàng cho production.

3. Mình cần telemetry trước sự cố, không phải sau

Khi lỗi xảy ra, mình không thể map TTS cost hay performance tới podcast ID cụ thể. Log không có entry hữu ích. Workflow event không có record TTS generation nào.

Mình phải chẩn đoán lỗi từ status field của podcast, lỗi 409 khó hiểu, và reproduce timeout behavior trên local. Cuối cùng cũng xong, nhưng đó không phải trải nghiệm debugging mình muốn lặp lại.

Mình thêm TTS cost telemetry sau đó — model sử dụng, fallback model, retry count, status mỗi attempt, ký tự transcript, audio bytes output, audio duration. Đáng lẽ cái này phải tồn tại trước sự cố. Theo kinh nghiệm của mình, chuyện này luôn xảy ra. Bạn xây observability sau đám cháy, không phải trước.

4. Rollback không phải là thất bại

Xóa 2.724 dòng code cảm giác khá tệ. Mình sẽ không giả vờ ngược lại. Bạn dành một tuần xây thứ gì đó, tự hào về kiến trúc, rồi phá hết vì nó chưa sẵn sàng.

Nhưng đó là quyết định đúng. Chunk QA system là thiết kế tốt. Nó sẽ quay lại — dưới dạng thay đổi nhỏ hơn, cô lập hơn, với canary validation đàng hoàng. Chỉ là không phải như một phần của thay đổi cấu hình model. Và không phải trong khi setup mới vẫn bị timeout trên những segment mà stable path xử lý ngon lành.

5. Tập 50 phút là một sản phẩm khác

Điều này làm mình ngạc nhiên. Mình tưởng hệ thống xử lý được script dài bất kỳ. Dữ liệu nói ngược lại.

Nếu ai đó thực sự muốn podcast 50 phút, đó là một generation profile khác. Có lẽ cần script được rút gọn xuống 45 phút hoặc ít hơn trước khi generate audio. Một manual review gate trước TTS. Budget timeout mạnh hơn cho mỗi segment. Có thể cả chiến lược synthesis khác hoàn toàn.

Tối ưu default path cho khoảng 15-40 phút là quyết định đúng. Tập 50+ phút nên được xem như exceptional path, không phải norm. Mình nghĩ đó là quyết định product, không chỉ engineering.

Mọi thứ đang ở đâu

Production đã quay lại cấu hình TTS ổn định. Nó không hoàn hảo. Giọng không nhất quán trên podcast dài vẫn còn, và mình nghe thấy nó. Nhưng đủ ổn định để nền tảng hoạt động cho đại đa số trường hợp.

Nếu bạn muốn nghe DIALOGUE tạo ra gì, bạn có thể tự thử tại https://podcast.chandlernguyen.com.

Incident report đã được commit vào repository. Rollback được document với SQL shape chính xác để reset podcast failed. Telemetry giờ đã có sẵn cho lần thử tiếp theo.

Và bài học rõ ràng hơn mình muốn:

Trong production AI, cái giá của việc thay đổi core dependency mà không stress testing đàng hoàng không phải là một thí nghiệm thất bại. Mà là một trải nghiệm người dùng thất bại.

Mình sẽ thử lại với các cấu hình model khác. Nhưng lần sau, với telemetry tốt hơn, một canary deployment path, và stress test chạy full export 50+ phút trước khi bất kỳ thứ gì chạm production.

Trước khi thay đổi production AI dependency, đây là checklist mình lẽ ra phải tuân thủ:

  1. Stress test job tệ nhất. Chạy workload dài nhất, nặng nhất bạn kỳ vọng — không chỉ test prompt ngắn. Nếu hệ thống xử lý podcast 40 phút, hãy test với podcast 50 phút.
  2. Log per-attempt telemetry ngay từ đầu. Tên model, fallback model, retry count, timeout, ký tự transcript, audio bytes output, audio duration. Đáng lẽ phải có trước sự cố. Luôn vậy.
  3. Canary một job thật. Trước khi bật cho tất cả mọi người, chạy một production job thật end to end trên cấu hình mới và verify output.
  4. Verify fallback dưới cùng budget timeout. Fallback dùng chung timeout với primary không phải là fallback — đó là cơ hội thứ hai để failed dưới cùng điều kiện.
  5. Định nghĩa rollback SQL và runbook trước khi deploy. Biết chính xác cách reset job bị kẹt. Document các command. Nếu không có cái này, bạn sẽ improvising trong sự cố.

Nếu bạn xây hệ thống AI production, pattern của bạn để testing thay đổi cấu hình model mà không làm hỏng trải nghiệm người dùng là gì? Mình thực sự muốn biết. Bạn đã từng có khoảnh khắc "thay đổi một thứ và mọi thứ vỡ" tương tự chưa?

Thân,

Chandler

Đọc tiếp