Khi repo thiếu hoặc “nói dối” về cách đúng, agent coding không im lặng: nó vá bằng pattern lấy từ web, nhìn vẫn thông minh. Mình không gọi đó là lỗi “ý thức” của model - đó là bù context khi nội bộ mỏng; nhưng công cụ chạy nhanh và lặp lại thì sai không còn là một lần gõ nhầm mà thành sóng: cùng lệch lặp qua nhiều file, nhiều PR, và càng tự động hóa thì chi phí sửa càng nhảy cóc (review, regression, chạy lại pipeline).

Trước đây doc tệ chủ yếu làm khó người mới; giờ nó làm sai lớp abstraction mà không kịp cảm nhận vì phần “đọc” đã outsource một phần cho máy. RAG hay search nội bộ chỉ cứu được nếu nguồn để index đúng: embed doc sai thì retrieval chỉ phục vụ việc lặp lại sai có trích dẫn - trông uy tín hơn một câu bịa trong chat, nhưng vẫn sai.

Cộng đồng và vài paper gần đây gọi tắt một cụm: spec-driven development (SDD): để đặc tả / intent là thứ có thẩm quyền hơn “đoán từ code đã viết”. Không phải lúc nào cũng cần formal methods; nhưng nếu bạn đang dùng agent mạnh, bạn cần biết mình đang ở mức nào trên phổ spec - dưới đây mình ghép nó với triad README + diagram + PR (đã nói trong bài) để anh em không bị lạc giữa học thuyết và việc phải ship tuần sau.


Ba mức spec-driven và chỗ đứng của triad README + diagram + PR

Không có một định nghĩa “SDD” duy nhất trong ngành; nhưng có một cách chia hợp lý mình thấy lặp lại từ bài survey thực hành Spec-Driven Development: From Code to Contract in the Age of AI Coding Assistants (arXiv: 2602.00180) và vài bài tổng quan gần đây (GitHub / Microsoft về Spec Kit, các blog phân tầng spec-first / spec-anchored / spec-as-truth): thẩm quyền của đặc tả so với code tăng dần từ trái sang phải, và kỷ luật bảo trì cũng tăng theo.

Spec-first: có mục tiêu trước khi code, nhưng đặc tả có thể “chết sau sprint”

Bạn viết user story + acceptance criteria, hoặc vài bullet “phải / không được”, trước khi nhờ agent sinh code. Giá trị lớn nhất là đỡ vibe coding - prompt mơ hồ thì model đoán một đốp giả định âm thầm (định dạng file, quota, quyền xoá…). Khi đã có code chạy, nếu không có luật bảo trì, spec dễ drift: đây không phải lỗi AI, đây là lỗi quy trình.

Triệu chứng: ticket đóng, README không đổi, agent PR sau dùng README như ground truth và đi chệch ý định ban đầu.

Spec-anchored: đặc tả sống cùng code - đây là chỗ triad của mình cắm vào

Đặc tả đi cùng vòng đời: đổi hành vi thì đổi spec cùng lúc hoặc trước một nhịp ngắn; CI / test / contract là dây an toàn. BDD scenario chạy được, OpenAPI khớp handler, contract test giữ producer-consumer - tất cả là biến “ý định” thành thứ máy kiểm được (ít nhất là regression).

Docs-as-code tối thiểu ở đây (README package + một diagram gốc + quy tắc PR) không thay toàn bộ OpenAPI, nhưng cùng họ với spec-anchored: tạo mỏ neo có diff trên git, bắt merge đi kèm chứng. Nói thực dụng hơn: bạn chưa lên level “spec là code”, nhưng bạn đã thoát khỏi pure spec-first hay bỏ đặc tả sau khi xanh test.

Spec-as-source: người sửa spec, máy sinh code - kén domain

Mọi thay đổi hành vi đi qua chỉnh spec, code (re)generate hoặc sinh từ generator đã tin cậy: stub từ OpenAPI, pipeline nhúng, Simulink→C, v.v. Drift gần như loại theo cấu trúc vì không edit tay phần sinh (hoặc rất hạn chế). Cái giá là niềm tin vào toolchain và chi phí setup; với web app nghiệp vụ đổi liên tục, không phải team nào cũng cần leo tới đây trong Q1.

Liên hệ bài của mình: triad README + diagram + PR là bậc thang lên spec-anchored; khi chỗ nào đau nhất là contract API hoặc luồng tiền, bạn bổ sung OpenAPI / JSON Schema + test - không để prose một mình gánh.


Doc không phải PDF - là context cho cả người và agent

Nếu bạn nghĩ documentation là PDF trong drawer hay wiki “đẹp nhưng không ai sửa” thì framing đã lệch. Luồng làm việc hiện tại, phần lớn “kiến thức gắn với repo” thực tế được trộn vào prompt: bạn đọc README, agent gom README + file lân cận + comment - tất cả là context window. Doc tốt là phần prompt ổn định; doc dở như prompt nhiễu: vẫn “hợp lệ” nhưng dẫn ra quyết định sai bền.

Vài failure mode mình hay gặp: import layer sai - README vẫn ví dụ from legacy.foo import Bar trong khi boundary đã tách; agent và người vội copy theo, test xanh vì mock, tới deploy mới lộ coupling. Refactor phá invariant - rule “idempotent” / “không được gọi X song song” chỉ còn trong đầu maintainer cũ; agent “dọn code sạch” nhưng gãy hành vi. Policy trộn với implementation - doc mô tả flow cũ còn code đã đổi feature flag; mọi thứ đúng nửa vời (đúng kiểu độc hại nhất).

Triad mình đặt cược nếu muốn giảm tail risk:

  1. README theo package / module - boundary, vì sao tồn tại, import đúng, lỗi thường gặp.
  2. Một diagram nguồn sự thật (sequence, luồng dữ liệu, C4 nhỏ) sống trong git; đổi contract thì PR đụng diagram.
  3. Luật PR - đổi public API / gọi chéo layer → cập nhật doc (hoặc ghi rõ lý do chưa cập nhật để reviewer chặn).

Indexing chỉ là lớp tìm lại: README/ADR hợp lệ thì retrieval giúp bớt pattern ngoài lai; nhưng chất lượng corpus vẫn là nút thắt - embedding không biết “thiện ý”, chỉ biết similarity.

(giới hạn thẳng: dù doc đẹp, agent vẫn có thể cắt bớt context khi áp lực token - nên combo doc gọn + test guardrail + reviewer nhìn boundary vẫn là chốt cuối.)


README tối thiểu mà agent vẫn “bám” được

Ở tầng spec-first, README là tấm bản đồ nhỏ trước khi code; ở tầng spec-anchored, README là đối tác phải cập nhật cùng merge. Dù bạn chưa có OpenAPI đầy đủ, khối in/out + invariant bên dưới vẫn là bước không thể bỏ nếu không muốn agent điền chỗ trống bằng tutorial ngoài mạng.

Agent không “đọc” README kiểu thưởng thức văn - nó ghép signal: ranh giới, contract, cấm/kỹ thuật ghi thẳng. Văn hay nhưng mơ hồ thường làm loãng signal (đôi khi junior thì mình cố ý viết dài hơn để dẫn; còn agent thì ngắn và kiểm được thắng). Boundary không rõ thì model bù bằng pattern phổ biến trên mạng - hiếm khi trùng dependency graph monorepo của bạn.

Scope in / out

Ghi một dòng in (làm gì, sau giả định gì đã đúng) và một dòng out (không làm gì, không kéo dependency nào). Agent dùng khối này để tránh import chéo layer và tránh “tiện tay” thêm việc ngoài ranh giới. Ngoại lệ (chỉ được gọi X qua adapter Y) thì ghi đúng tên - đừng để tool tự suy.

Public API surface

Liệt kê export chính hoặc trỏ thẳng entry (src/index.ts, __init__.py…) và nói đâu là “mặt tiền”. Đổi public API thì sửa đúng một block - PR sau thấy contract lệch ngay, không phải đoán re-export.

Invariant và side effect

Nêu định dạng bắt buộc (Money, idempotency key…), I/O (disk, network, queue), retry/timeout nếu có, chỗ hay sai semantics (rounding, timezone). Đây là vùng model hay hallucinate khi code assert ít; một dòng doc đôi khi cứu cả người lẫn máy khỏi “hợp lý nhưng sai hệ”.

Lệnh test / lint trong monorepo

Một hoặc hai lệnh copy-paste đúng scope (pnpm test --filter …, turbo run lint --filter …). Thiếu dòng này thì agent hay đề xuất chạy full tree hoặc sai package.

README “đủ neo”:

# billing-settlement

- Scope: tính toán quyết toán sau khi ledger đã có entry. Không gọi payment gateway.
- Invariant: mọi output là Money { amount, currency }; không làm tròn im lặng.
- Entry: src/index.ts - export settleBatch(), mapLedgerToSettlement().
- Test: pnpm test --filter billing-settlement

Cùng repo, ví dụ cờ đỏ - gần như không có boundary kiểm được:

# api-gateway

API hiện đại, scalable, enterprise-ready.

Chi tiết kiến trúc xem README root.

Không được import từ domain/ nhưng internal được phép tuỳ ý.

Test: pnpm test

Anti-pattern: README chỉ marketing hoặc copy README root

README package mà chỉ slogan, bullet tính năng, roadmap mơ hồ - không cho agent rule nào để so với diff. Copy nguyên README root vào subpackage còn tệ hơn: noise lấn signal, agent tưởng đã có context cục bộ trong khi không có contract riêng. Cần link root thì một câu dẫn là đủ; vẫn phải viết lại in/out, surface, invariant, lệnh filter đúng package.

README đủ tối thiểu khi trả lời được bốn câu kiểm được - in/out gì, public là gì, invariant/side effect gì, test/lint chạy sao - lúc đó bạn cho signal ranh giới thay vì để model đoán kiến trúc.


Một diagram - đủ để cả team và agent cùng nhìn một hình

Vấn đề: onboarding, debug, handoff cho agent - mỗi người một “bản trong đầu”. Hai lá trên wiki + Slack + PNG trong ticket là diagram fork: ai cập nhật chậm là context lệch; team và agent nhìn hai thực tại khác nhau.

Cách làm: Một nơi làm single source of truth trong repo (thường docs/ cạnh code). Luật: không nhân đôi file giữa tool - chỉ link một path trong git; đổi luồng thì sửa một chỗ, reviewer thấy diff như thấy spec.

Đánh đổi công cụ:

  • Mermaid trong git: merge được, diff trong PR được; agent đọc text structure dễ hơn ảnh.
  • Screenshot: nhanh, đẹp báo cáo, nhưng không diff, dễ “đóng băng” - ít ai export lại mỗi lần sửa nhỏ.
  • ASCII trong fence diagram: ổn khi toolchain render chưa sẵn (blog Hugo của mình cũng hỗ trợ), nhưng layout dài và semantics lỏng hơn Mermaid cho tooling.

Khi nào cập nhật? Đổi boundary service, đổi thứ tự retry/compensation, đổi contract lỗi mà hay hỏi “rốt cuộc ai gọi ai?” - coi như đèn báo diagram stale; đừng đợi incident sau 2h sáng.

Ví dụ tối giản:


  sequenceDiagram
  User->>API: Request
  API->>DB: Query

Trade-off: Mermaid đập tay vào syntax - typo là preview/CI có thể đỏ; đó là giá của “diagram như code”. Đổi lại lỗi hay lộ ở review PR thay vì âm thầm sai trên slide.


Từ prose tới thứ máy kiểm được: leo thêm một nấc trong spec-anchored

README và diagram là prose có diff - đủ để agent và người cùng trục, nhưng không tự đỏ CI khi handler lệch khỏi ý định (trừ khi bạn viết check tay). Trong phổ SDD, bước “đặt spec ở vị trí thẩm quyền cao hơn” thường gắn với artifact machine-readable:

  • HTTP API: OpenAPI (YAML/JSON) làm mặt tiền; sinh stub client/server, hoặc chạy contract test (tùy stack: Prism/Dredd, Pact, Postman collection locked, v.v.) để đổi response mà không đụng spec là đỏ pipeline.
  • Payload nội bộ: JSON Schema / protobuf / typed RPC - một lần định nghĩa, nhiều nơi generate type và test so khớp.
  • Luồng nghiệp vụ đọc được: BDD scenario (Cucumber, Playwright given-when-then…) chạy như test - spec không chỉ nằm trong đầu BA.

Đánh đổi rõ: chi phí viết và duy trì artifact, học toolchain, và đôi khi ma sát với “ship nhanh”. Cách mình chọn trong team nhỏ: đụng chỗ đau nhất trước (API public, luồng tiền, quota) - không bắt OpenAPI toàn bộ monorepo ngày một.

(giới hạn: spec machine-readable vẫn có thể sai business nếu product sai; nó chỉ bắt đúng những gì đã formalize.)


Workflow Specify → Plan → Implement → Validate và góc nhìn Spec Kit

Trên lý thuyết SDD, các phase lặp lại nhiều tài liệu: Specify (điều gì là đúng), Plan (kiến trúc/ràng buộc kỹ thuật), Implement, Validate - mỗi bước sinh artifact ràng bước sau, và có chỗ cho review người. Bài survey arXiv trên vẽ workflow kiểu vậy; GitHub Spec Kit (bộ mẫu + quy trình mở, được GitHub và Microsoft giới thiệu cho làm việc cộng tác với AI) gói ý tương tự: chốt intent trước, tách task nhỏ, rồi implement có kiểm soát - xem Increasing collaborative development with AISpec-driven development với Spec Kit.

Khi nào đáng bật workflow đầy bước: feature xanh-đồ, cần truy vết từ yêu cầu stakeholder xuống từng PR, hoặc team mới tập dùng agent và hay lạc ngữ cảnh. Khi nào không cần overkill: sửa bug một dòng, refactor nội bộ đã có test dày - lúc đó triad README + diagram + checklist PR vẫn là lớp nhẹ nhất mình khuyên giữ.

Spec Kit không thay README trong package; nó bổ sung lớp kỷ luật khi bạn muốn “ý định” có path file và review gate rõ hơn.


Checklist reviewer: doc là cổng chặn bug context

Bug “code chạy nhưng hành vi lệch ý định” hay kéo theo doc cũ được tin - đặc biệt khi agent summarize theo README thay vì đào cả repo. Reviewer cần lớp cổng: không thay chỗ suy nghĩ, mà buộc thay đổi hành vi đi kèm chứng (doc/diagram/CHANGELOG đúng chỗ).

Mở PR template cụ thể: runbook/feature flag có đủ chưa; biến môi trường/default mới đã liệt kê; log/metric kỳ vọng grep ở đâu; diagram ở path nào và đã khớp luồng chưa; trade-off có ghi scope “không làm gì” không.

Gợi ý bullets:

  • PR đổi hành vi public/contract API → đã cập nhật README package hoặc đoạn changelog trong mô tả PR.
  • Team đã adopt OpenAPI / contract test cho service này → file spec hoặc snapshot test đã cập nhật khớp diff (nếu không dùng spec máy đọc, ghi rõ lý do trong PR).
  • PR đổi luồng cross-service → đã sửa diagram gốc hoặc ghi rõ diagram follow-up issue #….
  • Chỉ refactor private → một dòng no doc change: private API only.

Ghép với PR do agent soạn: nhờ liệt kê đường dẫn file doc đã chạm + một dòng mô tả thay đổi. Danh sách đó là mục lục để đối chiếu diff - code đổi semantics mà danh sách doc trống thì đó là tín hiệu dừng lại.

Hotfix: checklist vẫn dùng nhưng rút gọn - ghi doc follow-up issue #… với acceptance cụ thể (file + section). Không có ticket thì doc lệch đúng lúc căng nhất.

Giới hạn: checklist không cứu được logic sai; nó chỉ giảm sót và buộc doc đi cùng merge. Câu hỏi “giả định nền còn đúng sau release không?” vẫn là phần con người.


Doc cũ 6 tháng: trước là nợ, giờ là poison cho agent

Hồi chỉ có con người đọc, doc lỗi thời vẫn là nợ: tốn thời gian, dễ hiểu nhầm, nhưng sai thường dừng ở vài người trong một phiên. Mình mở hai tab, đối chiếu log, chừng ~20 phút thì nhận ra “đoạn này không còn đúng”. Chi phí có thật, nhưng sai không tự nhân bản theo pipeline.

Cho corpus trong repo vào RAG/embed, chuyện khác: chunk cũ vào index, retrieval kéo đoạn trông authoritative - model trả lời tự tin, tool chain có thể đẩy sai xa hơn một câu chat. Đây là sai có độ lan: đề xuất mượt, lặp qua PR, qua runbook - chi phí đo bằng mức lan chứ không chỉ phút của một người.

Mình đối xử markdown trong repo như đầu vào của automation, không chỉ “đọc cho vui”. Datestamp ở đầu mục quan trọng - không mốc thì mọi thứ “vĩnh cửu” trong vector space. Nhãn docs-stale / trạng thái stale cho phần chưa kịp sửa để ingest/down-rank/exclude có đường - đừng kỳ vọng agent đoán ý định. Xóa hoặc ghi đè đoạn sai thay vì chồng lớp giải thích - corpus vẫn chứa poison thì retrieval vẫn ăn. Text sai trong repo không chỉ ngại đọc; với thời đại agent nó là đầu độc context.

Trước nợ doc có thể “để đó”; giờ nợ doc là nguyên liệu embed - và embed không biết xấu hổ.


Làm gì tuần tới nếu chỉ có 45 phút

  • Chốt mức SDD team đang nhắm: spec-first (chỉ đủ cho sprint này), spec-anchored (triad README + diagram + PR), hay đục một điểm spec máy đọc (OpenAPI đoạn API hay vỡ nhất).
  • README stub cho package hay bị agent sửa nhất: 10–15 dòng: mục tiêu hệ thống, quickstart tối thiểu, đường vào “nguồn sự thật”, ranh giới in/out.
  • Một dòng trong PR template: Docs đã chạm? Datestamp / nhãn stale / đã xóa câu sai?
  • Ticket diagram (tuỳ): một flow nhỏ “merge PR → doc đã khớp → cửa sổ refresh index/RAG” để team không nhầm “đã merge” với “corpus đã sạch”.

Agent không thay discipline; nó nhân discipline của bạn - mà khi discipline là để doc sai nằm yên trong repo thì bản nhân lên có nhịp điệu, không phải may rủi.


Đọc thêm (spec-driven & công cụ)