Nền tảng experimentation của DoorDash quản lý hơn 60.000 feature flag trên khoảng 623 repo, mỗi tháng thêm gần 2.300 flag mới. Chẳng ai cố tình xóa flag cả. Chúng cứ thế tích tụ tới khi ai đó cuối cùng phải audit lại, thường là ngay trước một đợt migration, hoặc khi review một incident thì phát hiện ra một flag đã rollout 100% cả năm rưỡi nay nhưng vẫn nằm y nguyên trong mọi call site dưới dạng if-statement. Mình từng dọn đúng loại nợ kỹ thuật này bằng tay. Nó tẻ nhạt, rủi ro thấp nhưng không phải bằng không, và là kiểu việc mà ai cũng đồng ý nên tự động hóa nhưng chẳng ai ưu tiên xây dựng công cụ để làm điều đó.
DoorDash xây luôn — và đây mới là phần mình thực sự quan tâm — họ công bố hẳn con số chi phí và tỷ lệ thành công, chứ không chỉ ra một bài launch chung chung. 50 flag lỗi thời, đánh giá từ đầu tới cuối: 45 flag ra được pull request dùng được, trung bình 13.8 phút và 4.79 đô mỗi flag. Đây là lần đầu mình thấy một con số đô-la thật gắn với chuyện “agent AI tự làm trọn một task kỹ thuật”, không phải demo, và nó đáng để mổ xẻ.
Kiến trúc là hai model làm hai việc khác nhau
Hệ thống theo mô hình orchestrator cộng worker. Một orchestrator dùng Claude Sonnet đọc ticket Jira và lấy metadata của flag — việc rẻ, khối lượng lớn, không cần suy luận nhiều. Sau khi flag được scope xong, một agent Claude Opus làm phần xóa thật sự: nó chạy trong một git worktree cô lập riêng (DoorDash chạy tối đa bốn worktree song song mỗi repo, Gradle chạy với --no-daemon để tránh state rò rỉ qua lại giữa các worktree), truy vết call chain, xóa các nhánh code chết, rồi mở PR. Mọi thứ đều có timeout cứng một tiếng.
Đây là quyết định phân tầng chi phí, không phải phân tầng năng lực — Sonnet thừa sức tự đọc metadata một mình, nhưng chẳng có lý do gì phải trả giá Opus chỉ để đọc một ticket Jira. Mình cũng từng ra quyết định y vậy trong pipeline của bọn mình: route theo độ phức tạp của task, chứ không mặc định dùng model mạnh nhất có sẵn cho mọi thứ. Con số ủng hộ cách làm này. Flag đơn giản: 100% thành công, 7.5 phút, 2.69 đô. Flag phức tạp với call chain sâu: 85% thành công, 17.7 phút, 6.20 đô. Đường cong chi phí bám gần như tuyến tính theo độ sâu suy luận, một tính chất khá hay nếu bạn đang cố dự báo ngân sách cho loại việc này ở quy mô lớn.
Phần thực sự khiến chuyện này an toàn để chạy không cần người canh
Con số quan trọng hơn cả 4.79 đô là 95 — mức JaCoCo patch coverage tối thiểu bắt buộc trước khi một PR được mở, cộng thêm việc phải pass build, pass test, và static analysis bằng Detekt. Bốn cổng kiểm tra tất định (deterministic) nằm giữa “agent nghĩ mình xong việc” và “con người nhìn thấy một PR”. Trong số 14 lần cần chỉnh sửa lại trên mẫu 50 flag, 6 lần là do fail coverage và 8 lần là xóa chưa trọn vẹn — cả hai loại lỗi này đều đúng loại mà các cổng kiểm tra được dựng lên để bắt, và chúng đã bắt được.
Kiểu lỗi đáng chú ý ở đây: khi hệ thống làm sai, nó sai theo hướng an toàn, không sai theo hướng nguy hiểm. Bài viết của chính DoorDash nói agent “có xu hướng để sót code chết chưa xóa hết trong các call chain phức tạp, hơn là tạo ra một thay đổi ngữ nghĩa không an toàn” — tức là để sót vài biến không dùng nằm sâu trong call chain, chứ không phải lật một flag về sai default trên production. Đó chính là kiểu lỗi bạn muốn thấy ở một hệ thống tự động đụng vào logic điều kiện đang chạy thật. Một agent thi thoảng để sót code chết chỉ là phiền phức. Một agent thi thoảng đảo ngược tỷ lệ rollout là một incident.
Chi tiết khác giải thích phần lớn tỷ lệ thành công 90%: agent xóa flag không chỉ đọc repo, nó còn truy vấn dữ liệu experimentation trực tiếp qua MCP — tỷ lệ rollout và giá trị target thật đang chạy, chứ không phải suy ra từ nhánh mặc định trong code. Đó là khác biệt giữa “flag trông như đang ở 100% dựa theo code mình đọc được” và “flag thực sự đang ở 100% ngay lúc này”. Suy ra trạng thái flag chỉ từ source code chính là cách dễ nhất để âm thầm thay đổi hành vi production trong khi tưởng mình chỉ đang xóa code chết. Cho agent một nguồn dữ liệu sống thay vì một bản snapshot tĩnh là một lựa chọn kiến trúc nhỏ nhưng loại bỏ được cả một nhóm rủi ro.
Chỗ mình sẽ thực sự áp dụng pattern này
Cách chia Sonnet-làm-orchestrator cộng Opus-làm-worker, được chặn bởi các kiểm tra tất định trước khi bất cứ thứ gì tới tay con người, tổng quát hóa tốt ra ngoài phạm vi feature flag — nâng cấp dependency, migrate API đã deprecated, xóa code chết sau khi một sản phẩm ngừng hoạt động, bất cứ việc gì mang tính “máy móc nhưng cần hiểu call chain, và một câu trả lời sai còn tệ hơn một câu trả lời chậm.” Cái template ở đây không phải là LLM. Nó là: model rẻ lo scope việc, model đắt lo thực thi việc, và output của cả hai model đều không được vào main nếu chưa qua test, coverage, và static analysis.
Điều mình sẽ không làm là bỏ qua cổng coverage để tiết kiệm 6-8% số PR mà nó chặn lại. Con số 4.79 đô đã giả định là cổng đó tồn tại sẵn — bỏ nó đi thì không phải là tiết kiệm chi phí, mà là đẩy chi phí đó sang cho người phát hiện ra coverage bị hụt ba sprint sau.