Agent coding giờ viết code nhanh hơn bất kỳ ai. Vấn đề không còn là tốc độ — mà là làm sao để nó viết đúng thứ mình cần, mà không phải nhồi cả codebase vào context mỗi lần giao việc.

Tôi từng nghĩ Spec-Driven Development (SDD) chỉ là một cái khung quy trình khác — specify, plan, tasks, implement — thêm một lớp thủ tục lên trên thứ mà agent vốn đã làm tốt. Nhưng càng làm việc với nhiều agent trên nhiều connector, nhiều module khác nhau, tôi nhận ra vấn đề thật sự nằm ở chỗ khác: cách giao task cho AI, giao đúng việc mà không load quá nhiều context.

Vibe coding vỡ trận ở đâu

"Vibe coding" — chỉ prompt và để agent tự code — hoạt động tốt với việc nhỏ, tự chứa: thêm dark mode toggle, sửa một hàm, viết test cho một function. Nhưng với việc lớn hơn, prompt bị thiếu đặc tả nghiêm trọng. Agent tự đưa ra giả định hợp lý để lấp chỗ trống, và kết quả là code trôi dạt khỏi ý định ban đầu — vẫn chạy, vẫn pass test, nhưng giải quyết sai vấn đề.

SDD ra đời để chặn đúng lỗ hổng đó: đặc tả (spec) — chứ không phải code — mới là nguồn chân lý. Điểm khác biệt quan trọng so với tài liệu đặc tả truyền thống: spec truyền thống được con người đọc, còn spec trong SDD thực thi như một validation gate. Agent không chỉ đọc spec để hiểu, mà dùng nó để tự kiểm tra output của chính mình.

Một spec tốt thường có 6 thành phần: outcomes, scope boundaries, constraints, prior decisions, task breakdown, và verification criteria. Workflow phổ biến nhất — được GitHub Spec Kit định hình — chia thành 4 giai đoạn: specify → plan → tasks → implement, mỗi giai đoạn dừng lại để verify trước khi sang bước sau.

Nhưng SDD chỉ là cái khung — cắt context mới là việc khó

Đây là chỗ tôi nghĩ nhiều bài viết về SDD bỏ qua. Viết spec chuẩn không tự động giải quyết vấn đề "load bao nhiêu context là đủ".

Agent không đọc context theo kiểu con người ưu tiên thông tin quan trọng — nó xử lý gần như tuyến tính trong context window. Đưa cả schema Oracle DWH 50 bảng khi task chỉ động đến 3 bảng, agent dễ nhầm field hoặc "sáng tác" logic dựa trên bảng không liên quan. Context càng dài, agent càng có xu hướng bám vào phần đầu/cuối, bỏ sót ràng buộc ở giữa — hiện tượng gần giống "lost in the middle" quen thuộc của LLM. Và token dùng để load context thừa không sinh ra giá trị gì, chỉ làm chậm và tốn quota.

Vài nguyên tắc tôi thấy hiệu quả khi cắt task cho agent:

  • Task là đơn vị verify được, không phải đơn vị "hợp lý về mặt người". Một task tốt là task mà bạn (hoặc test) chấm đúng/sai được trong vài phút — ví dụ "implement revocation endpoint theo RFC 7009" có chuẩn để verify rõ ràng, còn "cải thiện bảo mật OAuth" thì không biết khi nào xong.
  • Context tối thiểu = chỉ những gì task đó thực sự chạm vào. Chỉ đưa file/module liên quan, interface mà nó phải tuân theo, và 1-2 ví dụ pattern đã có để agent bám convention — không đưa implementation của module khác.
  • Tách quyết định kiến trúc ra khỏi task thực thi. Những quyết định như chọn số partition Kafka hay chiến lược cache nên chốt trước, ở một phiên riêng tập trung. Nếu để agent vừa quyết kiến trúc vừa code trong cùng một context dài, nó có thể tự đổi ý giữa chừng mà mình không nhận ra.
  • Nói rõ "không được làm gì". Với codebase có nhiều module liên quan nhau, agent có xu hướng "tiện tay" sửa luôn thứ khác nếu thấy nó nằm trong context. Giới hạn phạm vi rõ ràng quan trọng ngang với mô tả việc cần làm.

Kiến trúc sạch là thứ làm cho việc cắt context trở nên khả thi

Đây là điểm tôi nghĩ quan trọng nhất, và thường bị tách rời khỏi câu chuyện SDD: Clean Architecture không chỉ giúp con người maintain code dễ hơn — nó là ranh giới context tự nhiên cho agent.

Nếu hệ thống tách rõ domain / use-case / infrastructure, thì khi giao task, ta tự động biết cần load gì:

  • Task chạm domain layer → chỉ cần entity và business rule, không cần biết ORM dùng gì.
  • Task chạm infrastructure (một adapter cụ thể) → cần interface của use-case, không cần business logic phía sau nó.

Nói cách khác: kiến trúc tốt là điều kiện để "cắt context tối thiểu" trở nên khả thi. Nếu business logic lẫn trong view, SQL raw string rải khắp nơi, thì dù spec viết chuẩn đến đâu cũng không cắt được context nhỏ — vì mọi thứ phụ thuộc chéo nhau, muốn hiểu một phần buộc phải load gần hết.

Với kiến trúc có ranh giới rõ, spec cũng tự nhiên map vào boundary có sẵn: spec của một use-case chính là input port + output port + business rule, không cần mô tả implementation. Đây là lúc spec-as-contract thực sự phát huy tác dụng, thay vì chỉ là tài liệu mô tả tính năng.

Một sai lầm phổ biến khi mới áp SDD: viết spec theo kiểu tả tính năng end-to-end — vẫn tư duy feature-first chứ chưa phải layer-first. Kết quả là spec dài, context để implement dài theo, và agent vẫn phải đoán ranh giới vì spec không khớp với architecture boundary có sẵn.

Cách khắc phục: vỡ một feature lớn thành nhiều spec nhỏ theo layer — spec cho domain rule (không nhắc gì tới UI hay giao thức truyền tải), spec cho port/interface giữa các layer, spec cho adapter implementation (chỉ chạm infra, không lặp lại business rule). Mỗi spec như vậy tự nhiên có context riêng, nhỏ, và verify được độc lập.

Vai trò kiến trúc sư đang dịch chuyển

Cái thay đổi thực sự không phải "giờ phải prompt giỏi hơn", mà là trách nhiệm dịch chuyển từ viết code đúng sang thiết kế ranh giới đúng. Vẽ sai boundary — business logic lẫn với concern hạ tầng — thì mọi agent giao task trong khu vực đó sau này đều phải load context bẩn, và không ai sửa được tận gốc vì đó không phải bug, mà là nợ kiến trúc.

Kiến trúc sư giờ thiết kế cho hai người đọc: con người và agent. Một interface rõ ràng, một use-case có input/output tường minh — không chỉ giúp người mới onboard nhanh, mà giúp agent load đúng contract mà không cần đọc cả implementation phía sau.

Và không phải chỗ nào cũng cần boundary chặt như nhau. Với phần logic bảo mật lõi — nơi một lỗi có thể lan ra toàn hệ thống — boundary cần rất nghiêm ngặt, spec và test phải chặt chẽ. Với phần ít rủi ro hơn (UI, reporting), có thể giao nguyên khối cho agent tự do hơn mà không sao. Biết chỗ nào cần siết, chỗ nào có thể lỏng — đó mới là phần khó thật sự, và không có công cụ SDD nào làm thay được.

Kết

Spec-Driven Development giải quyết đúng một nửa vấn đề: nó buộc ý định phải tường minh trước khi code được viết. Nửa còn lại — cắt context sao cho agent load đúng vừa đủ — chỉ giải được nếu kiến trúc bên dưới đã có ranh giới rõ ràng. Hai thứ này không tách rời: spec tốt mà kiến trúc bùng nhùng thì context vẫn phình to; kiến trúc sạch mà không có spec thì ranh giới rõ cũng không đủ để agent hiểu ý định.

Nếu đang cân nhắc áp SDD cho một hệ thống cũ, tôi nghĩ việc nên làm trước tiên không phải là viết spec, mà là nhìn lại xem ranh giới layer đã đủ rõ để cắt task nhỏ chưa. Nếu chưa, viết spec kỹ đến đâu cũng chỉ là vá tạm.