Dùng AI viết code cho side project: viết spec trước khi prompt

Dùng AI viết code hiệu quả hơn nhờ một spec một trang và bảng ví dụ biến thành test. Ví dụ thật với app sổ chi tiêu, có code và 11 test chạy được.

Bạn nhờ AI viết code và nhận về thứ chạy được nhưng sai ý? AI viết code tốt nhất khi bạn đưa cho nó một spec ngắn kèm bảng ví dụ. Bảng ví dụ sẽ trở thành test, thước đo "đúng" cho cả bạn lẫn AI. Hãy cùng mình thử cách này với một side project thật nhé!

Side project của series là gì?

Đây là bài đầu tiên của series Làm side project cùng AI. Trong series này, mình sẽ làm một app nhỏ từ lúc có ý tưởng cho đến khi có người dùng, với sự hỗ trợ của AI ở gần như mọi bước. Mục tiêu không phải là để AI làm hết mọi việc, mà là học cách dùng AI để làm nhanh hơn trong khi vẫn hiểu mình đang làm gì.

Ý tưởng của app rất đơn giản: bạn ghi lại chi tiêu bằng cách nhắn một dòng tin, giống như nhắn tin cho bạn bè. Khi bạn nhắn cà phê 35k, app sẽ ghi lại một khoản chi 35.000đ với ghi chú "cà phê". Khi bạn nhắn /tong, app sẽ trả về tổng chi tiêu trong tháng.

Mình chọn app này vì nó đủ nhỏ để làm xong, đủ thật để chính mình dùng hằng ngày, và có một phần khó vừa sức là đọc số tiền theo đúng cách người Việt hay viết, như "35k", "1tr2" hay "45.000". Các bài viết sau của series sẽ kết nối app với Telegram và thêm một tính năng AI vào chính app.

Chuyện gì xảy ra khi prompt quá chung chung?

Cách tự nhiên nhất là mở công cụ AI lên và gõ một câu như sau:

Prompt mơ hồ
Viết cho mình hàm parse tin nhắn chi tiêu kiểu "cà phê 35k" trong TypeScript.

Bạn sẽ nhận được một hàm chạy được và nhìn khá gọn gàng. Điều khó nhận ra là AI đã phải tự đoán rất nhiều chuyện mà câu prompt không hề nhắc tới.

Chẳng hạn, 1tr2 là 1.200.000 hay 1.000.002, và app có cần hỗ trợ cách viết này không? 45.000 là 45 đồng hay 45 nghìn, khi người Việt dùng dấu chấm để tách hàng nghìn còn phần lớn ngôn ngữ lập trình lại coi dấu chấm là dấu thập phân? Nếu tin nhắn không có số tiền thì hàm trả về null, báo lỗi hay trả về số 0? Còn với cà phê 35k 40k thì hàm lấy số nào?

Mỗi chỗ AI phải đoán là một chỗ app có thể sai mà bạn không hề hay biết. Nếu bạn hỏi lại lần nữa, AI có thể đoán theo một kiểu khác. Bạn có code trong tay nhưng không thể nói chắc rằng code đó đúng, vì chính bạn cũng chưa nói rõ thế nào là đúng.

Để bạn thấy cụ thể, mình đã viết sẵn một phiên bản "tự đoán" trong file src/ban-tu-doan.ts. Đây là kiểu code trông hợp lý mà một prompt mơ hồ rất dễ tạo ra, do mình viết để minh họa chứ không phải code AI viết thật. Hàm parseGuess lấy con số đầu tiên trong câu, nhân với một nghìn nếu thấy chữ k, và nhân với một triệu nếu thấy chữ tr.

Phần dưới của file là một bảng gồm 10 tin nhắn, kèm kết quả mà mình muốn nhận được cho từng tin. Bảng này lấy từ spec ở phần sau của bài, kèm lý do cho từng dòng. Vòng lặp cuối file gọi parseGuess với từng tin nhắn, so với kết quả mong muốn, rồi in ✓ hoặc ✗:

src/ban-tu-doan.ts
export function parseGuess(message: string) {
  const match = message.match(/[\d.,]+/);
  if (!match) return null;
  let amount = parseFloat(match[0]);
  if (/k/i.test(message)) amount *= 1000;
  if (/tr/i.test(message)) amount *= 1_000_000;
  return { note: message.replace(/[\d.,]+\w*/, "").trim(), amount };
}

// [tin nhắn, kết quả mong muốn]; null nghĩa là app phải trả lời "không hiểu"
const table: [string, { note: string; amount: number } | null][] = [
  ["cà phê 35k", { note: "cà phê", amount: 35000 }],
  ["cơm trưa 45.000", { note: "cơm trưa", amount: 45000 }],
  ["xăng 50000", { note: "xăng", amount: 50000 }],
  ["tiền nhà 3tr", { note: "tiền nhà", amount: 3000000 }],
  ["điện thoại 1tr2", { note: "điện thoại", amount: 1200000 }],
  ["trà sữa 32K", { note: "trà sữa", amount: 32000 }],
  ["35k cà phê", { note: "cà phê", amount: 35000 }],
  ["cà phê", null],
  ["cà phê 0k", null],
  ["cà phê 35k 40k", null],
];

let pass = 0;
for (const [message, expected] of table) {
  const got = parseGuess(message);
  const ok = JSON.stringify(got) === JSON.stringify(expected);
  if (ok) pass += 1;
  console.log(`${ok ? "✓" : "✗"} ${message.padEnd(16)} → ${JSON.stringify(got)}`);
}
console.log(`\nĐúng ${pass}/${table.length} dòng của bảng.`);

Khi đọc lướt, bạn sẽ thấy hàm này có vẻ ổn. Nhưng khi chạy node src/ban-tu-doan.ts, kết quả như sau. Mỗi dòng là một tin nhắn và thứ mà hàm trả về, dấu ✗ nghĩa là khác với kết quả mong muốn:

node src/ban-tu-doan.ts
✓ cà phê 35k       → {"note":"cà phê","amount":35000}
✗ cơm trưa 45.000  → {"note":"cơm trưa","amount":45000000}
✓ xăng 50000       → {"note":"xăng","amount":50000}
✓ tiền nhà 3tr     → {"note":"tiền nhà","amount":3000000}
✗ điện thoại 1tr2  → {"note":"điện thoại","amount":1000000}
✗ trà sữa 32K      → {"note":"trà sữa","amount":32000000000}
✓ 35k cà phê       → {"note":"cà phê","amount":35000}
✓ cà phê           → null
✗ cà phê 0k        → {"note":"cà phê","amount":0}
✗ cà phê 35k 40k   → {"note":"cà phê  40k","amount":35000}

Đúng 5/10 dòng của bảng.

Một ly trà sữa bị tính thành 32 tỷ đồng.

Nguyên nhân vừa buồn cười vừa rất thật. Hàm kiểm tra xem trong câu có chữ tr hay không để nhận ra đơn vị triệu, mà "trà sữa" thì có chữ tr. "Cơm trưa" cũng mắc đúng lỗi đó, và còn mắc thêm một lỗi nữa: parseFloat("45.000") coi dấu chấm là dấu thập phân nên trả về 45, sau đó con số này lại bị nhân với một triệu. Ngoài ra, 1tr2 bị mất phần lẻ, 0k vẫn được lưu lại như bình thường, và khi có hai số tiền thì hàm lấy số đầu tiên và để chữ "40k" dính vào phần ghi chú.

Mình không nghĩ lỗi nào ở đây là do AI kém. Mỗi lỗi là một câu hỏi mà prompt bỏ ngỏ, và code đã chọn một đáp án nghe hợp lý nhưng sai với ý người dùng. Muốn AI chọn đúng, bạn phải trả lời những câu hỏi đó trước, và đó chính là việc của spec.

Spec có cần dài không?

Khi nghe đến chữ "spec", nhiều người hình dung ra một tài liệu dài vài chục trang. Với side project, một trang là đủ, miễn là spec trả lời được bốn câu hỏi: app giải quyết vấn đề gì, bản đầu tiên làm những gì, bản đầu tiên không làm những gì, và thế nào thì được coi là xong.

Phần "không làm những gì" là phần hay bị bỏ qua nhất, trong khi chính phần này ngăn cả bạn lẫn AI tự thêm việc không cần thiết. Đây là hai mục ở đầu spec của app sổ chi tiêu, file đầy đủ ở mục sau:

SPEC.md — hai mục Tính năng v1 và Ngoài phạm vi v1
## Tính năng v1
1. Nhắn `cà phê 35k` → lưu một khoản chi: ghi chú "cà phê", số tiền 35.000đ, thời điểm nhắn.
2. Nhắn `/tong` → tổng chi của tháng hiện tại.
3. Tin nhắn không hiểu → trả lời hướng dẫn cách nhắn, **không** lưu gì.

## Ngoài phạm vi v1
Nhiều người dùng, danh mục, ngân sách, biểu đồ, sửa/xóa khoản chi, đồng tiền khác VND.

Chỉ với một dòng "ngoài phạm vi", spec đã trả lời trước mấy câu hỏi mà AI hay tự quyết, như app có cần đăng nhập không, có chia danh mục không, có hỗ trợ USD không. Câu trả lời cho cả ba câu là không.

Vì sao bảng ví dụ là phần quan trọng nhất?

Theo mình, phần có giá trị nhất của spec là một bảng ví dụ ghi rõ với tin nhắn này thì kết quả phải là gì. Mỗi dòng trả lời đúng một câu hỏi mà prompt mơ hồ đã bỏ ngỏ. Đây là mười dòng của app sổ chi tiêu, mỗi dòng kèm lý do:

  • cà phê 35k thì ghi "cà phê", 35.000đ. Đây là cách viết phổ biến nhất, chữ k nghĩa là nghìn.
  • cơm trưa 45.000 thì ghi 45.000đ, vì người Việt dùng dấu chấm để ngăn hàng nghìn, không phải dấu thập phân.
  • xăng 50000 thì ghi 50.000đ. Số không có đơn vị được hiểu là đồng.
  • tiền nhà 3tr thì ghi 3.000.000đ, vì tr nghĩa là triệu.
  • điện thoại 1tr2 thì ghi 1.200.000đ, đúng cách nói "một triệu hai".
  • trà sữa 32K thì ghi 32.000đ. Chữ K viết hoa vẫn là nghìn, và chữ "tr" trong "trà" không được hiểu là triệu.
  • 35k cà phê thì ghi "cà phê", 35.000đ, vì có người gõ số tiền trước.
  • cà phê thì không ghi gì, vì thiếu số tiền.
  • cà phê 0k thì không ghi gì, vì một khoản chi 0 đồng chỉ là rác trong sổ.
  • cà phê 35k 40k thì không ghi gì, vì có hai số tiền mà app không biết lấy số nào.

Trong file SPEC.md, mười dòng này nằm trong một bảng ba cột là tin nhắn, ghi chú và số tiền, cùng vài quy tắc chung bên dưới. Đây là file spec đầy đủ:

SPEC.md
# Sổ chi tiêu — spec v1

## Vấn đề
Mình muốn ghi lại chi tiêu hằng ngày mà không phải mở app, gõ form. Cách nhanh nhất: nhắn một dòng như nhắn tin cho bạn bè.

## Người dùng
Một người (mình). Chưa cần đăng nhập, chưa cần nhiều người dùng.

## Tính năng v1
1. Nhắn `cà phê 35k` → lưu một khoản chi: ghi chú "cà phê", số tiền 35.000đ, thời điểm nhắn.
2. Nhắn `/tong` → tổng chi của tháng hiện tại.
3. Tin nhắn không hiểu → trả lời hướng dẫn cách nhắn, **không** lưu gì.

## Ngoài phạm vi v1
Nhiều người dùng, danh mục, ngân sách, biểu đồ, sửa/xóa khoản chi, đồng tiền khác VND.

## Quy tắc đọc số tiền
| Tin nhắn | Ghi chú | Số tiền (đồng) |
|---|---|---|
| `cà phê 35k` | cà phê | 35000 |
| `cơm trưa 45.000` | cơm trưa | 45000 |
| `xăng 50000` | xăng | 50000 |
| `tiền nhà 3tr` | tiền nhà | 3000000 |
| `điện thoại 1tr2` | điện thoại | 1200000 |
| `trà sữa 32K` | trà sữa | 32000 |
| `35k cà phê` | cà phê | 35000 |
| `cà phê` | — | không hiểu (thiếu số tiền) |
| `cà phê 0k` | — | không hiểu (số tiền phải > 0) |
| `cà phê 35k 40k` | — | không hiểu (hai số tiền) |

- Số tiền là số nguyên đồng, lớn hơn 0, tối đa 1 tỷ.
- `k` = nghìn, `tr` = triệu, `tr2` = 1,2 triệu; không phân biệt hoa thường.
- Dấu `.` hoặc `,` giữa các nhóm 3 chữ số là dấu phân cách hàng nghìn.
- Ghi chú là phần còn lại sau khi bỏ số tiền, xóa khoảng trắng thừa; không được rỗng.

## Tiêu chí hoàn thành
- Mọi dòng trong bảng trên có test và test qua.
- Không có thư viện ngoài cho phần đọc số tiền.

Dấu "—" trong cột ghi chú nghĩa là không có ghi chú nào được lưu, vì cả tin nhắn bị từ chối. Viết bảng này mất khoảng mười lăm phút, và đó là mười lăm phút bạn phải suy nghĩ về sản phẩm chứ không phải về code. Khi viết đến dòng cà phê 35k 40k, bạn buộc phải chọn giữa việc lấy số đầu tiên, cộng hai số lại hay báo là không hiểu. Mình chọn báo không hiểu, vì ghi sai số tiền gây hại nhiều hơn so với việc bắt người dùng nhắn lại. Đây là một quyết định về sản phẩm, và mình không muốn AI quyết định thay.

Ngoài ra, mỗi dòng trong bảng có thể chuyển thẳng thành một test, nên bạn không cần nghĩ thêm test nào khác. Bạn cũng nên chú ý đến các dòng "không hiểu", vì đó là nơi lỗi hay ẩn nấp nhất. Người mới thường chỉ nghĩ đến trường hợp mọi thứ đều đúng, còn bảng ví dụ buộc bạn phải nghĩ cả đến trường hợp sai.

Prompt sẽ thay đổi thế nào khi đã có spec?

Khi đã có spec, prompt trở nên ngắn hơn nhưng rõ ràng hơn hẳn:

Prompt kèm spec
Đọc SPEC.md, mục "Quy tắc đọc số tiền".
1. Viết test bằng node:test cho từng dòng trong bảng (file src/parse-expense.test.ts).
2. Viết hàm parseExpense(message: string): { note: string; amount: number } | null
   trong src/parse-expense.ts để mọi test qua. Không dùng thư viện ngoài.
3. Chạy `node --test src/parse-expense.test.ts` và dán kết quả.
Nếu spec có chỗ chưa rõ, hỏi lại trước khi viết code.

So với câu prompt đầu tiên, prompt này chỉ cho AI đúng nguồn thông tin là file SPEC.md thay vì để AI dựa vào trí nhớ. Prompt yêu cầu viết test trước khi viết code, nên test không bị viết theo cách mà AI cài đặt. Prompt cũng đặt sẵn các ràng buộc như tên hàm, vị trí file và không dùng thư viện ngoài. Cuối cùng, prompt đòi hỏi bằng chứng là kết quả chạy test, chứ không chấp nhận một câu "xong rồi ạ".

Dòng cuối cùng, "nếu chưa rõ thì hỏi lại", là thói quen mà mình khuyên bạn nên giữ. Tài liệu hướng dẫn viết prompt của Anthropic (Prompting best practices) có một hình ảnh rất dễ nhớ: hãy coi AI như một nhân viên mới rất giỏi nhưng chưa biết gì về dự án của bạn. Họ cũng gợi ý một cách kiểm tra đơn giản là đưa prompt cho một đồng nghiệp không biết gì về việc này, và nếu người đó thấy bối rối thì AI cũng sẽ bối rối.

Kết quả là một hàm và 11 test

Code dưới đây được viết bằng Claude Code từ đúng spec và prompt ở trên, sau đó được chạy test thật. Project chỉ có ba file trong src: ban-tu-doan.ts ở phần trên, file test và file hàm bên dưới, cộng với SPEC.md ở thư mục gốc.

Đây là file test. Mảng ok chứa các tin nhắn đọc được, mỗi phần tử gồm tin nhắn, ghi chú mong muốn và số tiền mong muốn. Mảng ở vòng lặp thứ hai chứa các tin nhắn mà hàm phải trả về null. Mỗi phần tử trở thành một test riêng:

src/parse-expense.test.ts
import assert from "node:assert/strict";
import { test } from "node:test";

import { parseExpense } from "./parse-expense.ts";

const ok: [string, string, number][] = [
  ["cà phê 35k", "cà phê", 35000],
  ["cơm trưa 45.000", "cơm trưa", 45000],
  ["xăng 50000", "xăng", 50000],
  ["tiền nhà 3tr", "tiền nhà", 3000000],
  ["điện thoại 1tr2", "điện thoại", 1200000],
  ["trà sữa 32K", "trà sữa", 32000],
  ["35k cà phê", "cà phê", 35000],
];

for (const [message, note, amount] of ok) {
  test(`đọc được "${message}"`, () => {
    assert.deepEqual(parseExpense(message), { note, amount });
  });
}

for (const message of ["cà phê", "cà phê 0k", "cà phê 35k 40k", "35k"]) {
  test(`không hiểu "${message}"`, () => {
    assert.equal(parseExpense(message), null);
  });
}

Bạn có thể thấy dòng cuối có thêm "35k", một trường hợp không nằm trong bảng, tức tin nhắn chỉ có số tiền mà không có ghi chú. Phần quy tắc của spec có câu "ghi chú không được rỗng" nên test này vẫn hợp lệ, nhưng ở lần sửa spec tiếp theo, mình sẽ thêm hẳn trường hợp này vào bảng để không bị sót.

Còn đây là hàm đọc tin nhắn. Hàm tìm mọi cụm số tiền trong tin nhắn bằng một biểu thức chính quy. Nếu tìm được đúng một cụm, hàm đổi cụm đó thành số đồng, rồi lấy phần chữ còn lại làm ghi chú. Phần giải thích từng đoạn nằm ở mục sau:

src/parse-expense.ts
export type Expense = { note: string; amount: number };

const MAX_AMOUNT = 1_000_000_000;
// 35k · 32K · 3tr · 1tr2 · 45.000 · 50000 (một "token" số tiền, đứng riêng)
const AMOUNT = /(?<![\p{L}\d])(\d{1,3}(?:[.,]\d{3})+|\d+)(k|tr(\d)?)?(?![\p{L}\d])/giu;

function toAmount(digits: string, unit: string | undefined, decimal: string | undefined): number {
  const base = Number(digits.replace(/[.,]/g, ""));
  const lower = unit?.toLowerCase();
  if (lower === "k") return base * 1_000;
  if (lower?.startsWith("tr")) return base * 1_000_000 + Number(decimal ?? 0) * 100_000;
  return base;
}

export function parseExpense(message: string): Expense | null {
  const matches = [...message.matchAll(AMOUNT)];
  if (matches.length !== 1) return null; // thiếu số tiền, hoặc có nhiều hơn một

  const [token, digits, unit, decimal] = matches[0];
  const amount = toAmount(digits, unit, decimal);
  if (amount <= 0 || amount > MAX_AMOUNT) return null;

  const note = message.replace(token, " ").replace(/\s+/g, " ").trim();
  if (!note) return null;
  return { note, amount };
}

Để chạy test, bạn cần Node.js 22.18 trở lên và không phải cài thêm gì. Mỗi dòng ok là một test qua, hai dòng cuối là tổng kết. Nếu chạy trong terminal, Node có thể hiển thị dấu ✔ thay cho chữ ok, nhưng nội dung giống nhau:

node --test src/parse-expense.test.ts
ok 1 - đọc được "cà phê 35k"
ok 2 - đọc được "cơm trưa 45.000"
ok 3 - đọc được "xăng 50000"
ok 4 - đọc được "tiền nhà 3tr"
ok 5 - đọc được "điện thoại 1tr2"
ok 6 - đọc được "trà sữa 32K"
ok 7 - đọc được "35k cà phê"
ok 8 - không hiểu "cà phê"
ok 9 - không hiểu "cà phê 0k"
ok 10 - không hiểu "cà phê 35k 40k"
ok 11 - không hiểu "35k"
# pass 11
# fail 0

Test chạy qua có nghĩa là bạn đã hiểu code chưa?

Việc 11 test chạy qua cho biết hàm đã làm đúng những gì spec yêu cầu. Tuy nhiên, điều đó không có nghĩa là bạn đã hiểu hàm. Trước khi chấp nhận code do AI viết, mình luôn bắt bản thân phải giải thích được từng đoạn, và nếu chưa giải thích được thì mình hỏi lại AI cho đến khi hiểu.

Ví dụ, cặp (?<![\p{L}\d]) ở đầu và (?![\p{L}\d]) ở cuối biểu thức chính quy dùng để làm gì? Đây là điều kiện "ký tự đứng trước và đứng sau không được là chữ cái hay chữ số". Nhờ điều kiện này, những con số dính liền với chữ, như abc123 hay gạo 35kg, sẽ không bị đọc thành số tiền. Ký hiệu \p{L} khớp với mọi chữ cái, kể cả chữ có dấu, và phải có cờ u thì mới dùng được.

Vì sao 1tr2 lại ra 1.200.000? Phần tr(\d)? lấy chữ số đứng sau "tr" làm hàng trăm nghìn, nên kết quả là 1 × 1.000.000 + 2 × 100.000. Còn việc kiểm tra matches.length !== 1 thay vì lấy số đầu tiên là vì spec quy định rằng hai số tiền thì "không hiểu", nghĩa là quyết định đã đưa ra ở bảng ví dụ giờ đã được thể hiện trong code.

Câu hỏi cuối cùng mới là câu quan trọng nhất: những cách viết nào chưa có trong bảng? Mình viết thêm một file nhỏ, gọi parseExpense với bốn tin nhắn không có trong bảng và in thứ hàm trả về:

src/thu-them.ts
import { parseExpense } from "./parse-expense.ts";

for (const message of ["táo 2kg 35k", "gạo 35kg", "1.5tr laptop", "cà phê 35 nghìn"]) {
  console.log(message.padEnd(16), "→", parseExpense(message));
}
node src/thu-them.ts
táo 2kg 35k      → { note: 'táo 2kg', amount: 35000 }
gạo 35kg         → null
1.5tr laptop     → null
cà phê 35 nghìn  → { note: 'cà phê nghìn', amount: 35 }

Ba dòng đầu cho kết quả hợp lý.

táo 2kg 35k ghi đúng 35.000đ, vì "2kg" dính chữ nên không bị coi là số tiền. gạo 35kg bị từ chối vì không có số tiền nào. 1.5tr laptop bị từ chối vì cách viết "1.5tr" không có trong spec, và từ chối vẫn an toàn hơn đoán. Dòng cuối cùng là một lỗi thật: người dùng viết "35 nghìn" nhưng app lại ghi 35 đồng với ghi chú "cà phê nghìn", và không hề báo lỗi gì. 11 test vẫn chạy qua, vì trong bảng không có dòng nào chứa chữ "nghìn".

Test chỉ bảo vệ những trường hợp mà bạn đã nghĩ tới, còn những trường hợp khác thì bạn phải tự thử. Việc đọc code và thử thêm như vậy mất khoảng mười phút, và theo mình đó chính là khác biệt giữa "để AI viết hộ" và "viết cùng AI". Bài một workflow dùng AI mà vẫn giữ được tư duy giải thích kỹ hơn vì sao bạn không nên bỏ qua bước này.

Khi spec phải thay đổi thì làm thế nào?

Lỗi "35 nghìn" ở trên chính là lúc spec cần thay đổi, vì bạn phải quyết định app có hiểu các chữ "nghìn", "ngàn", "triệu" hay chưa. Thứ tự sửa mà mình hay làm là sửa spec trước, bằng cách thêm dòng cà phê 35 nghìn → 35000 vào bảng, hoặc → không hiểu nếu bạn quyết định chưa hỗ trợ cách viết này. Sau đó mình thêm test cho dòng mới và chạy để thấy test này thất bại. Cuối cùng, mình mới nhờ AI sửa code cho đến khi mọi test đều chạy qua, kể cả 11 test cũ.

Khi làm theo thứ tự này, spec luôn là bản mô tả chính xác nhất của app, còn 11 test cũ sẽ đảm bảo rằng bản sửa mới không làm hỏng những thứ đang chạy tốt.

Thói quen viết spec trước cũng có ích khi bạn đi phỏng vấn, nhất là khi gặp câu hỏi "bạn dùng AI khi lập trình như thế nào", câu hỏi này có trong ngân hàng câu hỏi phỏng vấn.

Nên làm việc với AI ở đâu?

Khi đã có spec, câu hỏi tiếp theo là bạn nên làm việc với AI ở đâu: trong trình duyệt, trong trình soạn code như Cursor hay GitHub Copilot, hay ngay trong terminal như Claude Code. Mỗi cách phù hợp với một kiểu làm việc khác nhau.

Bài viết tiếp theo của series sẽ dùng chính spec này để so sánh các công cụ trên một việc cụ thể là thêm lệnh /tong. Bạn có thể theo dõi cả series ở Làm side project cùng AI.

Nhận bài tiếp theo qua email.

Một email mỗi tuần, chỉ khi có điều đáng đọc. Không spam.