Spec-driven development: viết spec trước để AI agent code đúng tính năng lớn
Prompt một dòng thì AI làm tốt việc nhỏ, nhưng với tính năng lớn nó tự đoán thay bạn. Bài này giải thích spec-driven development, vòng lặp spec, plan, tasks, implement, cách làm với Claude Code và GitHub Spec Kit, kèm một SPEC.md mẫu.
Bạn gõ vào Claude Code: "thêm tính năng lưu bài viết để đọc sau". Hai mươi phút sau, agent báo xong. Bạn mở diff: nó tạo bảng mới với cấu trúc bạn không muốn, lưu tạm vào localStorage cho người chưa đăng nhập (bạn chưa bao giờ yêu cầu), quên luôn trang xem lại danh sách, và sửa thêm vài file không liên quan. Bạn sửa lần một, lần hai. Đến lần thứ ba, agent bắt đầu làm hỏng chỗ đã sửa trước đó.
Model không kém. Vấn đề là câu yêu cầu một dòng để lại hàng chục quyết định chưa ai đưa ra, và agent phải tự đoán từng cái. Spec-driven development (SDD) là cách xử lý chuyện này: đưa ra các quyết định đó trên giấy trước, rồi mới cho AI code.
Vì sao "vibe prompting" gãy ở tính năng lớn
Với việc nhỏ, prompt ngắn là đủ. "Đổi màu nút submit sang đỏ" không có gì để đoán. Nhưng một tính năng thật luôn kéo theo các câu hỏi ẩn: người chưa đăng nhập thì sao, lưu hai lần thì sao, lỗi mạng thì UI hiển thị gì, dữ liệu nằm ở bảng nào, cái gì không làm.
Khi bạn không trả lời, agent sẽ tự chọn, thường là lựa chọn phổ biến nhất nó từng thấy chứ không phải lựa chọn hợp với dự án của bạn. Bài giới thiệu Spec Kit trên blog GitHub (9/2025) tóm gọn điều này: model "exceptional at pattern completion, but not at mind reading" (Den Delimarsky, GitHub).
Cái giá thứ hai nằm ở context. Mỗi lần bạn sửa hướng, lượt sửa đó nằm lại trong lịch sử hội thoại. Docs Claude Code khuyên nếu đã sửa cùng một lỗi hơn hai lần thì nên xóa context và bắt đầu lại với prompt tốt hơn (mình giải thích kỹ cơ chế này trong bài Context engineering là gì). Spec chính là "prompt tốt hơn" đó, được viết trước thay vì sau ba lần thất bại.
Đây cũng là ranh giới với vibe coding: vibe coding chấp nhận để AI quyết, còn SDD giữ quyền quyết định về phía bạn và giao phần gõ code cho AI.
Spec-driven development là gì
SDD là cách làm trong đó đặc tả (spec) là thứ bạn viết và duyệt đầu tiên, code được sinh ra từ spec và được kiểm tra ngược lại theo spec. Nguyên tắc cốt lõi mà Spec Kit nhấn mạnh: tách phần "what" ổn định (làm gì, vì sao) khỏi phần "how" linh hoạt (làm bằng công nghệ gì).
Birgitta Böckeler (Thoughtworks) phân biệt ba mức độ trong một bài phân tích các công cụ SDD trên martinfowler.com:
| Mức | Spec sống bao lâu | Ví dụ |
|---|---|---|
| Spec-first | Viết trước khi code, xong task có thể bỏ | SPEC.md dùng một lần trong Claude Code |
| Spec-anchored | Giữ lại sau khi xong, dùng tiếp khi sửa tính năng | Thư mục specs/ commit cùng code |
| Spec-as-source | Người chỉ sửa spec, code sinh lại từ spec | Hướng đi thử nghiệm, chưa phổ biến |
Với phần lớn team, spec-first là điểm khởi đầu hợp lý. Spec-anchored đáng cân nhắc khi tính năng sẽ còn thay đổi nhiều lần.
Vòng lặp: spec → plan → tasks → implement → verify
Công cụ khác nhau đặt tên khác nhau, nhưng vòng lặp gần như giống nhau:
| Bước | Trả lời câu hỏi | Ai quyết | Output |
|---|---|---|---|
| Spec | Làm gì, cho ai, ngoài phạm vi là gì | Bạn (AI hỏi để khai thác) | spec.md / SPEC.md |
| Plan | Làm bằng cách nào trong codebase này | AI đề xuất, bạn duyệt | plan.md hoặc plan trong plan mode |
| Tasks | Chia thành các bước nhỏ, thứ tự ra sao | AI | tasks.md |
| Implement | Code từng bước | AI | Diff |
| Verify | Code có đúng spec không | Test + bạn | Test pass, review |
Hai điểm quan trọng nhất là duyệt spec và duyệt plan. Sửa một dòng trong spec rẻ hơn nhiều so với sửa một loạt file code đã sinh sai. Bước tasks và implement thì AI làm khá tốt nếu hai bước đầu chắc chắn.
Cách 1: làm gọn với Claude Code, không cần cài thêm gì
Docs best practices của Claude Code mô tả một quy trình nhẹ, thực chất là SDD thu gọn, gồm ba phần.
Để AI phỏng vấn bạn và viết SPEC.md
Thay vì tự nghĩ hết mọi trường hợp, bắt đầu bằng một prompt ngắn và yêu cầu Claude phỏng vấn bạn qua tool AskUserQuestion. Prompt dưới đây dịch và rút gọn từ ví dụ trong docs:
Mình muốn làm [mô tả ngắn tính năng]. Hãy phỏng vấn mình chi tiết bằng tool AskUserQuestion.
Hỏi về cách implement, UI/UX, edge case, rủi ro và tradeoff. Đừng hỏi câu hiển nhiên,
đào vào những phần khó mà mình có thể chưa nghĩ tới.
Hỏi tới khi đủ, rồi viết spec hoàn chỉnh vào SPEC.md.Theo docs, spec hữu ích nhất là spec tự đứng được: nêu tên file và interface liên quan, ghi rõ cái gì ngoài phạm vi, và kết thúc bằng một bước kiểm chứng end-to-end.
Implement trong một phiên mới
Viết xong spec, mở phiên mới (hoặc /clear) để implement. Phiên phỏng vấn đã đầy những câu hỏi qua lại; phiên mới chỉ có context sạch cộng với SPEC.md. Bước này hay bị bỏ qua.
Plan mode trước khi code
Trong phiên mới, bật plan mode rồi yêu cầu Claude đọc SPEC.md và code liên quan để lập kế hoạch:
- Nhấn
Shift+Tabtới khi thanh trạng thái hiện⏸ plan mode on, hoặc thêm/plantrước một prompt, hoặc chạyclaude --permission-mode plan. - Trong plan mode, Claude đọc file và chạy lệnh để khám phá nhưng không sửa source.
- Nhấn
Ctrl+Gđể mở plan trong editor và sửa trực tiếp. - Khi duyệt, bạn chọn tiếp tục ở auto mode, tự duyệt từng edit, hoặc "No, keep planning" để yêu cầu sửa plan.
- Nếu bật setting
showClearContextOnPlanAccept, khi duyệt plan sẽ có thêm lựa chọn duyệt và xóa context lập kế hoạch, để phần implement chạy với context sạch.
Cursor có Plan Mode tương tự (cũng xoay bằng Shift+Tab, plan là file Markdown sửa được). Cách cấu hình CLAUDE.md, subagent và hook để hỗ trợ quy trình này có ở bài Claude Code nâng cao.
Ví dụ SPEC.md cho một tính năng Next.js
Dưới đây là spec cho tính năng "lưu bài viết" trong một blog Next.js App Router, đúng tình huống ở đầu bài. Tên file và hàm là giả định cho ví dụ, bạn thay bằng tên thật trong dự án:
# SPEC: Lưu bài viết để đọc sau
## Mục tiêu
Người dùng đã đăng nhập lưu được một bài blog và xem lại danh sách bài đã lưu tại /saved.
## Ngoài phạm vi
- Không có thư mục hay tag cho bài đã lưu
- Không lưu tạm cho người chưa đăng nhập (không dùng localStorage)
- Không gửi email hay thông báo
## Hành vi
1. Nút "Lưu" hiển thị cuối mỗi trang /blog/[slug].
2. Chưa đăng nhập: bấm nút thì chuyển tới /login?next=/blog/[slug]. Trang login chỉ chấp nhận next là đường dẫn nội bộ (bắt đầu bằng "/", không phải "//").
3. Đã đăng nhập: bấm thì nút đổi trạng thái ngay (optimistic); bấm lần nữa để bỏ lưu.
4. Lưu thất bại: nút quay về trạng thái cũ, hiện toast "Không lưu được, thử lại".
5. /saved: bài lưu gần nhất lên đầu, 20 bài mỗi trang; danh sách rỗng thì hiện link về /blog.
6. Bài đã gỡ (unpublished) không hiện trong /saved, nhưng bản ghi lưu vẫn giữ.
## Dữ liệu
- Bảng mới Bookmark(userId, postId, createdAt), unique (userId, postId).
- Request lưu trùng (double-click, hai tab) không báo lỗi, không tạo bản ghi trùng.
## Ràng buộc kỹ thuật
- Mutation bằng Server Action trong src/app/blog/[slug]/actions.ts
- Lấy user bằng getCurrentUser() trong src/lib/auth.ts; KHÔNG tin userId gửi từ client
- Action kiểm tra postId tồn tại và đang publish trước khi lưu; không tin postId từ client
- UI dùng useOptimistic, theo pattern của src/components/LikeButton.tsx
- Tạo migration mới, không sửa migration cũ
## Kiểm chứng
- Unit test (Vitest) cho action: lưu, bỏ lưu, lưu trùng, chưa đăng nhập
- E2E (Playwright): đăng nhập → lưu bài → thấy bài ở /saved → bỏ lưu → bài biến mất
- pnpm lint && pnpm tsc --noEmit && pnpm test đều pass
## Câu hỏi còn mở
- Có giới hạn số bài được lưu không? (tạm thời: không giới hạn)Để ý vài điểm. Mục Ngoài phạm vi chặn đúng những thứ agent hay "tặng thêm". Mục Hành vi viết thành các câu kiểm tra được, không phải mô tả chung chung. Dòng "KHÔNG tin userId gửi từ client" và quy tắc chỉ redirect nội bộ là loại quyết định bảo mật mà agent có thể bỏ sót nếu không ai nói. Mục Kiểm chứng cho agent một vòng lặp tự kiểm tra: docs Claude Code nhấn mạnh rằng không có check để chạy thì "trông như xong" là tín hiệu duy nhất agent có.
Mục Ràng buộc kỹ thuật ở đây chỉ ghi những pattern đã có sẵn trong repo, đúng như docs Claude Code khuyên nêu tên file và interface. Nếu dùng Spec Kit, phần này nên để dành cho bước /speckit-plan, còn spec chỉ giữ what và why.
Cách 2: GitHub Spec Kit cho quy trình đầy đủ
Spec Kit là toolkit mã nguồn mở (MIT) của GitHub, biến vòng lặp trên thành các skill chạy trong agent. Bản 1.0 phát hành ngày 21/8/2026, bản mới nhất lúc viết bài là 1.1.1 (6/10/2026). Dự án ra bản mới gần như mỗi tuần, nên kiểm tra README trước khi làm theo bất kỳ hướng dẫn nào, kể cả bài này.
Cài đặt
Cần Python 3.11+, uv và một coding agent được hỗ trợ:
uv tool install specify-cli
# dự án mới
specify init my-project --integration claude
# dự án đã có code: commit hoặc stash trước, rồi chạy ở thư mục gốc
specify init --here --force --integration claude--integration chọn agent. Spec Kit hỗ trợ hơn 50 agent, trong đó có claude, copilot, codex, cursor-agent, gemini, agy (Antigravity), kiro-cli và nhiều agent khác. Với dự án đã có code, --force có thể ghi đè file ở các đường dẫn Spec Kit quản lý, vì vậy docs khuyên tạo một baseline có thể review (commit, tạo nhánh) trước.
Các bước
Với Claude Code, Copilot và Codex, các bước là skill gọi trong chat của agent, không phải lệnh terminal. Cú pháp phụ thuộc agent: Claude Code và Copilot gõ /speckit-specify, Codex gõ $speckit-specify, còn tài liệu tham chiếu viết dạng /speckit.specify. Nhiều hướng dẫn cũ trên mạng chỉ dùng dạng có dấu chấm, nên nếu gõ không ra lệnh thì kiểm tra lại cú pháp.
| Skill | Việc | Bắt buộc? |
|---|---|---|
/speckit-constitution | Nguyên tắc chung của dự án, mọi bước sau đối chiếu theo | Một lần mỗi dự án |
/speckit-specify | Viết spec: what và why, chưa nói tech stack | Có |
/speckit-clarify | Hỏi tối đa 5 câu mỗi lần chạy về chỗ mơ hồ, ghi lại vào spec; chạy lại được | Nên có |
/speckit-plan | Plan kỹ thuật: stack, kiến trúc | Có |
/speckit-checklist | "Unit test cho requirements": spec đã đủ rõ chưa | Tùy |
/speckit-tasks | Chia tasks.md theo phase, đánh dấu task chạy song song | Có |
/speckit-analyze | Kiểm tra mâu thuẫn giữa spec, plan, tasks (chỉ đọc) | Nên có |
/speckit-implement | Thực thi các task | Có |
/speckit-converge | So code với spec, thêm task còn thiếu vào tasks.md | Có |
Sau /speckit-implement, bạn chạy /speckit-converge. Nếu còn thiếu, nó chỉ thêm task mới vào tasks.md (không sửa code), bạn implement tiếp và converge lại tới khi báo Converged. Với tính năng lớn, docs khuyên implement từng phase một thay vì chạy hết một lần để agent không bị quá tải context.
Constitution nên ghi những điều đã đúng trong repo, không phải mong ước. Ví dụ cho dự án Next.js:
/speckit-constitution TypeScript strict, không dùng any. Mọi mutation đi qua Server Action
và kiểm tra quyền ở server. Mỗi tính năng có unit test Vitest và ít nhất một E2E Playwright.
Không thêm dependency mới khi chưa ghi lý do trong plan.File sinh ra nằm ở .specify/memory/constitution.md và mỗi tính năng một thư mục trong specs/ (gồm spec.md, plan.md, tasks.md, checklists/requirements.md). Tạo nhánh git cho từng tính năng là tùy chọn, qua extension git.
Kiro specs: cùng ý tưởng, tích hợp sẵn trong Kiro
Kiro (AWS) cũng xây quanh spec: mỗi spec gồm requirements.md (hoặc bugfix.md cho spec sửa lỗi), design.md và tasks.md. Requirements viết theo dạng EARS, kiểu WHEN <sự kiện> THE SYSTEM SHALL <hành vi>, ví dụ khi người dùng gửi form sai dữ liệu thì hệ thống phải hiện lỗi cạnh từng trường. Kiro có hai biến thể Requirements-First và Design-First, cùng chế độ Quick Spec chạy cả ba bước không dừng duyệt, dành cho tính năng đã hiểu rõ. Nếu team bạn không dùng Kiro, cú pháp EARS vẫn đáng mượn để viết mục Hành vi trong SPEC.md cho dễ kiểm tra.
Cách review một spec
Spec do AI viết cũng cần review như code. Checklist mình dùng:
- Có mục Ngoài phạm vi chưa? Không có thì agent sẽ tự mở rộng.
- Mỗi hành vi có kiểm tra được không? "Trải nghiệm mượt" không test được; "nút đổi trạng thái ngay khi bấm" thì được.
- Trường hợp lỗi và người dùng chưa đăng nhập đã được nêu chưa?
- Quyền và bảo mật: ai được làm gì, dữ liệu nào không tin từ client?
- Có nêu file, pattern có sẵn để theo không? Không nêu thì agent có thể viết lại thứ đã có.
- Có bước kiểm chứng end-to-end không?
- Còn câu hỏi mở nào không? Spec Kit đánh dấu bằng
[NEEDS CLARIFICATION]; trong SPEC.md tự viết, gom vào một mục riêng và trả lời trước khi implement. - Spec có lẫn "how" vào "what" không? Nếu spec đã chọn sẵn thư viện mà không có lý do, bước plan mất chỗ để đề xuất.
Khi nào SDD là thừa
SDD có giá thật: thời gian viết, thời gian đọc cả đống Markdown. Trong bài phân tích tháng 10/2025 (khi Spec Kit còn ở bản 0.x), Böckeler nhận xét các workflow này có thể là "overkill" cho bug nhỏ, file Markdown sinh ra dài và mệt để review, và agent đôi khi vẫn bỏ qua chỉ dẫn dù spec rất kỹ. Các bản mới đã thêm bước converge để bắt phần bị sót, nhưng spec vẫn không thay được việc đọc diff.
| Tình huống | Nên dùng |
|---|---|
| Sửa typo, đổi tên biến, thêm log | Prompt thẳng |
| Bug rõ nguyên nhân, sửa một vài chỗ | Prompt cụ thể + test tái hiện lỗi |
| Thay đổi nhiều file, bạn chắc cách làm | Plan mode là đủ |
| Tính năng mới có nhiều quyết định sản phẩm | SPEC.md + phiên mới + plan mode |
| Tính năng lớn, nhiều người, cần lưu lại lý do | Spec Kit hoặc Kiro, giữ spec trong repo |
| Prototype thử ý tưởng, sẵn sàng vứt | Không cần spec, cứ thử |
Quy tắc gọn từ docs Claude Code: nếu bạn mô tả được diff trong một câu, bỏ qua bước plan. Mình thêm một vế: nếu bạn không trả lời được "cái gì ngoài phạm vi", đó là lúc cần spec.
Sai lầm thường gặp
- Để AI viết spec rồi duyệt lướt. Spec sai thì mọi bước sau sai theo, và sai một cách rất tự tin. Đọc spec kỹ hơn đọc code.
- Implement ngay trong phiên vừa phỏng vấn. Context đã đầy hội thoại. Mở phiên mới.
- Spec quá dài. Spec không phải tài liệu thiết kế trăm trang. Nếu một tính năng cần spec quá dài, có lẽ nên chia thành nhiều tính năng.
- Code thay đổi, spec đứng yên. Nếu giữ spec trong repo, thống nhất cách cập nhật; spec lỗi thời còn tệ hơn không có spec vì nó gây hiểu nhầm.
- Constitution toàn khẩu hiệu. "Code phải sạch" không giúp gì. Chỉ ghi quy tắc cụ thể, kiểm tra được.
- Tin rằng có spec là xong. Vẫn cần test, vẫn cần review diff. Spec giảm số lần đoán sai, không loại bỏ nó.
Tóm lại
Spec-driven development không phải phương pháp mới lạ: đó là việc nghĩ trước khi code, áp dụng cho một đồng đội làm rất nhanh nhưng không đọc được suy nghĩ của bạn. Với phần lớn tính năng, bản nhẹ là đủ: để AI phỏng vấn, viết SPEC.md, mở phiên mới, plan mode, implement kèm test. Khi team lớn hơn hoặc cần lưu lại lý do quyết định, Spec Kit cho bạn quy trình đầy đủ với constitution, clarify, analyze và converge. Nếu muốn nhìn toàn cảnh một ngày làm việc với AI, xem thêm AI workflow lập trình.
Viết được một spec tốt đòi hỏi bạn biết trước những chỗ dễ sai: state, quyền, lỗi mạng, dữ liệu trùng. Đó là kiến thức từ việc tự xây ứng dụng thật. Nếu bạn muốn có nền đó cho frontend, khóa React PRO của HoleTex dạy React và Next.js tới mức bạn tự viết được spec, đọc được plan và biết khi nào AI đang đi sai hướng.
Bài liên quan
- Context engineering là gì: quản lý ngữ cảnh để AI agent code đúng
- Claude Code nâng cao: CLAUDE.md, subagents, skills, hooks và MCP
- AI workflow lập trình: một ngày code với AI trông thế nào
- Prompt cho lập trình: cách viết prompt để AI code đúng ý
- Vibe coding là gì
Nguồn tham khảo: Best practices for Claude Code (code.claude.com), Permission modes, plan mode (code.claude.com), Spec Kit (github.com), Spec Kit releases (github.com), Spec Kit quickstart (github.github.io), Agentic SDD reference (github.github.io), Spec Kit integrations (github.github.io), Adopting Spec Kit in an existing project (github.github.io), Spec-driven development with AI (github.blog), Understanding spec-driven development: Kiro, spec-kit, and Tessl (martinfowler.com), Kiro specs (kiro.dev), Kiro feature specs (kiro.dev), Cursor Plan Mode (cursor.com), Next.js Server Actions (nextjs.org). Cập nhật 2026-10-08.