AI coding agent viết code chạy được rất nhanh, nhưng nếu bạn không nói rõ kiến trúc (architecture) nào đang áp dụng, nó sẽ âm thầm phá vỡ toàn bộ ranh giới kiến trúc mà team bạn dày công xây dựng.
AI Vi Phạm Ràng Buộc Kiến Trúc Như Thế Nào Theo Mặc Định
Một AI coding agent (như Claude Code, Cursor, GitHub Copilot) được huấn luyện trên hàng triệu repository công khai, phần lớn trong số đó không tuân theo bất kỳ kiến trúc nghiêm ngặt nào. Khi bạn yêu cầu nó "thêm một API endpoint để reset password", nó sẽ mặc định chọn cách làm phổ biến nhất trong dữ liệu huấn luyện — thường là nhét toàn bộ logic validation, truy vấn database, gửi email, và xử lý lỗi thẳng vào trong controller. Đây không phải vì model "lười" hay "kém", mà vì đó là pattern có xác suất xuất hiện cao nhất khi không có ràng buộc nào được nêu ra.
Hệ quả cụ thể mà bạn sẽ gặp: business logic bị viết trực tiếp trong controller thay vì đưa xuống service/use-case layer; agent bỏ qua repository layer và gọi thẳng ORM hoặc raw SQL trong tầng application; import xuyên qua ranh giới module một cách tùy tiện — ví dụ tầng domain lại import một class từ tầng infrastructure vì "cho nhanh"; hướng phụ thuộc (dependency direction) bị đảo ngược, khiến core business logic phụ thuộc ngược vào chi tiết kỹ thuật (framework, database driver) thay vì điều ngược lại như dependency inversion (đảo ngược phụ thuộc) yêu cầu. Ngoài ra, agent còn có xu hướng chọn thư viện hoặc pattern phổ biến nhất trên GitHub thay vì thứ mà codebase của bạn đã chuẩn hóa — ví dụ dùng axios trong khi cả dự án đã thống nhất dùng một HTTP client wrapper nội bộ.
Vấn đề nghiêm trọng hơn ở chỗ code do AI viết ra thường "đọc rất mượt" và pass được test đơn giản, nên vi phạm kiến trúc dễ lọt qua review nếu reviewer không tỉnh táo. Về lâu dài, một codebase từng có kiến trúc rõ ràng có thể xói mòn (erode) chỉ sau vài chục pull request được AI hỗ trợ mà không có ràng buộc tường minh. Vì vậy, trách nhiệm của kỹ sư senior không phải là hy vọng AI "tự hiểu" kiến trúc, mà là chủ động mã hóa (encode) ràng buộc đó vào prompt, vào file cấu hình, và vào quy trình kiểm tra.
Mẹo: Trước khi giao một task lớn cho AI agent, hãy tự hỏi "nếu tôi không nói gì về kiến trúc, nó sẽ viết code kiểu generic nào?" — nếu câu trả lời khác với chuẩn dự án, đó chính là chỗ bạn cần ràng buộc rõ trong prompt.
Nêu Rõ Ràng Buộc Kiến Trúc Trong Prompt
Cách hiệu quả nhất và rẻ nhất để giảm vi phạm kiến trúc là nói thẳng ra trong prompt — đừng giả định agent "biết" pattern dự án bạn đang dùng chỉ vì nó đọc qua vài file. Bạn cần chỉ rõ tên các layer (domain, application, infrastructure, presentation...), hướng phụ thuộc được phép, và quan trọng không kém: agent KHÔNG được phép import từ đâu.
Một prompt yếu thường chỉ nói: "Thêm chức năng gửi thông báo khi user đăng ký." Một prompt mạnh sẽ như sau:
Add a notification-on-signup feature. This codebase follows hexagonal
architecture (ports and adapters) with three layers:
- domain/ — pure business entities and value objects, zero external
dependencies, no imports from application/ or infrastructure/
- application/ — use cases and ports (interfaces), may import from
domain/, must NOT import concrete classes from infrastructure/
- infrastructure/ — adapters implementing ports from application/,
may import from application/ and domain/
Dependency direction: infrastructure -> application -> domain only.
Never the reverse.
Before writing any file, list each new file you will create and state
which layer it belongs to and which layers it is allowed to import from.
Then wait for my confirmation before writing code.
Kỹ thuật "yêu cầu agent liệt kê layer trước khi viết code" cực kỳ hiệu quả vì nó buộc model phải suy luận tường minh (explicit reasoning) về vị trí file thay vì viết code theo phản xạ thống kê. Nếu agent trả lời sai layer, bạn sửa ngay từ bước lên kế hoạch — rẻ hơn nhiều so với việc phải refactor lại sau khi code đã viết xong. Bạn cũng nên cho một ví dụ cụ thể về layering đúng đã có sẵn trong codebase (ví dụ trỏ tới application/use-cases/CreateOrder.ts) để agent có "khuôn mẫu" tham chiếu thay vì tưởng tượng.
Một điểm quan trọng: đừng chỉ liệt kê rule một lần rồi thôi — với các task dài, nhắc lại ràng buộc ở mỗi bước con (sub-step), vì context window dài có thể khiến agent "quên" chỉ dẫn ban đầu, đặc biệt khi phải đọc thêm nhiều file khác trong quá trình thực thi.
Mẹo: Luôn yêu cầu agent liệt kê danh sách file dự kiến tạo + layer tương ứng TRƯỚC khi viết code — chặn sai sót kiến trúc ở giai đoạn rẻ nhất.
Yêu Cầu Áp Dụng Design Pattern Cụ Thể
Nếu bạn prompt "hãy làm code sạch, tuân theo SOLID", agent sẽ hiểu theo cách rất mơ hồ và tự diễn giải theo phong cách phổ biến nhất nó từng thấy — điều này gần như vô nghĩa với một codebase có chuẩn riêng. Thay vào đó, hãy gọi tên chính xác design pattern (mẫu thiết kế) bạn muốn: Repository, Factory, Strategy, Adapter, Observer, CQRS (Command Query Responsibility Segregation), Decorator, v.v. Việc gọi tên chính xác giúp agent kích hoạt đúng "khuôn mẫu" đã học từ hàng nghìn ví dụ pattern đó trong dữ liệu huấn luyện, thay vì đoán mò.
Ngoài tên pattern, hãy kèm theo lý do ngắn gọn vì sao pattern đó phù hợp với bài toán, và tốt nhất là cung cấp sẵn một interface hoặc code skeleton để "neo" (anchor) implementation vào đúng hình dạng bạn muốn. Ví dụ:
Implement payment processing using the Strategy pattern because we need
to support Stripe, PayPal, and bank transfer today, with more providers
likely to be added later without touching the checkout use case.
Anchor to this interface (already defined in application/ports):
interface PaymentStrategy {
charge(amount: Money, method: PaymentMethod): Promise<PaymentResult>;
}
Implement three concrete strategies in infrastructure/payment/, and a
PaymentStrategyFactory in application/ that selects the right strategy
based on PaymentMethod.type. Do not use if/else chains in the checkout
use case to branch on payment type — that defeats the purpose of the
pattern.
After implementing, explain in 3-4 sentences how your implementation
satisfies the Strategy pattern's intent (interchangeable algorithms,
selected at runtime, without modifying the client).
Yêu cầu agent tự giải thích implementation của nó thỏa mãn "intent" của pattern như thế nào là một bước kiểm tra rẻ nhưng cực kỳ hữu ích — nếu agent giải thích lúng túng hoặc mâu thuẫn với code nó vừa viết, đó là dấu hiệu rõ ràng implementation chưa đúng pattern, dù code vẫn chạy được. Tránh các prompt mơ hồ kiểu "làm cho nó extensible" hay "áp dụng best practice" — những cụm từ này không neo được vào một cấu trúc code cụ thể nào và AI sẽ tự diễn giải theo hướng dễ nhất.
Mẹo: Luôn kèm interface hoặc skeleton có sẵn khi yêu cầu áp dụng pattern — pattern không có "khuôn" để neo vào rất dễ bị agent triển khai nửa vời hoặc trộn lẫn với pattern khác.
Dùng CLAUDE.md Để Mã Hóa Quy Tắc Kiến Trúc
Nhắc lại ràng buộc kiến trúc trong từng prompt là cần thiết cho task phức tạp, nhưng sẽ rất tốn công và dễ bị bỏ sót nếu phải làm thủ công mỗi lần. Giải pháp bền vững là viết các quy tắc đó một lần vào file cấu hình cấp dự án — CLAUDE.md (hoặc tương đương như AGENTS.md, project instructions tùy công cụ) — để mọi phiên làm việc, mọi thành viên trong team, mọi agent trong tương lai đều tự động kế thừa cùng một bộ luật kiến trúc mà không cần gõ lại.
Một section kiến trúc tốt trong CLAUDE.md thường bao gồm: tên và mục đích của từng layer, hướng phụ thuộc được phép (thường vẽ dạng mũi tên một chiều), quy ước đặt tên file/folder theo layer, danh sách import bị cấm tuyệt đối, và tham chiếu tới các ADR (Architecture Decision Record — tài liệu ghi lại quyết định kiến trúc quan trọng kèm bối cảnh/lý do) liên quan. Ví dụ một đoạn CLAUDE.md thực tế:
## Architecture: Hexagonal (Ports and Adapters)
Layers and allowed imports:
- domain/ -> imports nothing outside domain/
- application/ -> imports domain/ only
- infrastructure/ -> imports application/, domain/
- presentation/ -> imports application/ only, never infrastructure/ directly
Rules:
- Never import a concrete infrastructure class into domain/ or application/.
- All external I/O (DB, HTTP, queue) must go through a port defined in
application/ports/ and implemented in infrastructure/.
- New use case? Create file in application/use-cases/, one file per use case.
- See docs/adr/0007-hexagonal-architecture.md for the full rationale.
Điểm mấu chốt: CLAUDE.md nên súc tích và có thể kiểm chứng được (enforceable), không nên viết theo kiểu khát vọng chung chung như "chúng ta hướng tới clean architecture" mà không định nghĩa rõ layer nào import layer nào. Một quy tắc mơ hồ thì AI (và cả con người) đều không tuân theo được. Khi kiến trúc thay đổi — ví dụ tách thêm một bounded context (ngữ cảnh giới hạn, khái niệm từ Domain-Driven Design để phân chia các mô hình nghiệp vụ độc lập) mới — hãy cập nhật CLAUDE.md ngay lập tức, đừng để nó lạc hậu so với thực tế codebase, vì một file rule sai còn nguy hiểm hơn không có rule.
Mẹo: Viết CLAUDE.md dưới dạng quy tắc "được phép / không được phép" cụ thể kèm ví dụ đường dẫn file thật, tránh câu chữ trừu tượng như "code phải sạch và dễ bảo trì".
Kiểm Chứng Output Của AI Theo Nguyên Tắc Kiến Trúc
Prompt tốt và CLAUDE.md rõ ràng giảm đáng kể vi phạm kiến trúc, nhưng không loại bỏ hoàn toàn — bạn vẫn cần một lớp kiểm chứng độc lập với chính AI. Công cụ phân tích hướng phụ thuộc tĩnh (static dependency-direction linter) là tuyến phòng thủ đầu tiên: dependency-cruiser cho JavaScript/TypeScript, ArchUnit cho Java/Kotlin, hay các eslint plugin về import boundaries (như eslint-plugin-boundaries) cho phép bạn định nghĩa rule y hệt những gì viết trong CLAUDE.md nhưng dưới dạng máy có thể thực thi — ví dụ chặn cứng mọi import từ domain/ sang infrastructure/ ngay ở build time.
Bên cạnh công cụ tự động, một checklist review tập trung vào vi phạm layering nên là một phần bắt buộc của code review, không phải review chung chung. Vài câu hỏi cụ thể reviewer nên tự hỏi: file này có import đúng layer không? Use case có gọi thẳng ORM thay vì qua port không? Domain entity có biết gì về HTTP request hay database row không? Một kỹ thuật hay bị bỏ qua nhưng rất hiệu quả là yêu cầu chính AI agent tự phê bình (self-critique) output của nó dựa trên kiến trúc đã nêu, ví dụ:
Review the code you just wrote against the hexagonal architecture rules
in CLAUDE.md. List any file that imports across a forbidden boundary,
and any business logic that leaked into infrastructure/ or presentation/.
If none found, say so explicitly and explain why each file is compliant.
Agent tự phê bình không thay thế được review của con người, nhưng bắt được một tỷ lệ đáng kể lỗi rõ ràng với chi phí gần như bằng không. Lớp phòng thủ tiếp theo là CI: build phải fail nếu dependency-cruiser hoặc ArchUnit test phát hiện vi phạm — biến rule kiến trúc từ "gợi ý" thành "cổng chặn cứng" (hard gate), không phụ thuộc vào việc reviewer có tỉnh táo lúc đó hay không. Cuối cùng, review của con người senior engineer luôn là lớp gác cổng cuối cùng, vì linter chỉ bắt được vi phạm cấu trúc (structural), còn vi phạm về ý nghĩa/tinh thần kiến trúc (ví dụ dùng đúng layer nhưng sai design pattern) thì vẫn cần con người đánh giá.
Mẹo: Thêm dependency-cruiser hoặc ArchUnit vào CI ngay từ đầu dự án — chi phí thiết lập rẻ hơn rất nhiều so với việc gỡ rối một codebase đã bị AI "làm bẩn" kiến trúc sau vài tháng.
Thực Hành: Triển Khai Một Tính Năng Theo Hexagonal Architecture
Phần này đi qua toàn bộ quy trình thêm tính năng "password reset" (đặt lại mật khẩu) vào một codebase theo hexagonal architecture (ports and adapters), từ định nghĩa port cho tới khi verify không có vi phạm dependency direction (hướng phụ thuộc).
Bước 1 — Định nghĩa domain port. Trước khi để AI viết bất kỳ dòng code nào, tự tay (hoặc cùng AI) định nghĩa interface (port) mà use case sẽ phụ thuộc vào, đặt trong application/ports/:
// application/ports/NotificationSender.ts
export interface NotificationSender {
sendPasswordResetEmail(to: string, resetToken: string): Promise<void>;
}
Việc bạn tự định nghĩa port trước, thay vì để AI tự bịa ra trong lúc viết use case, đảm bảo interface phản ánh đúng nhu cầu nghiệp vụ chứ không phải nhu cầu kỹ thuật của một provider email cụ thể nào.
Bước 2 — Prompt agent triển khai use case ở application layer:
Implement the RequestPasswordReset use case in application/use-cases/.
It must depend only on:
- domain/entities/User.ts
- application/ports/NotificationSender.ts (interface, not implementation)
- application/ports/UserRepository.ts (interface, not implementation)
Do NOT import any class from infrastructure/. Do NOT construct a
concrete NotificationSender or UserRepository inside the use case —
they must be injected via the constructor.
Flow: find user by email, generate a reset token, persist it via
UserRepository, call NotificationSender.sendPasswordResetEmail.
Output sai điển hình mà bạn nên cảnh giác: agent import thẳng infrastructure/email/SendgridClient.ts vào use case "cho tiện", hoặc new SendgridClient() ngay trong constructor thay vì nhận qua dependency injection. Đây là dấu hiệu agent đã bỏ qua ranh giới layer dù bạn đã nói rõ — cần yêu cầu sửa lại ngay, không merge.
Bước 3 — Prompt agent viết infrastructure adapter:
Implement infrastructure/notifications/SendgridNotificationSender.ts
that implements the NotificationSender port from application/ports/.
Use the Sendgrid SDK here — this is the only place in the codebase
allowed to reference Sendgrid directly.
Bước 4 — Wiring dependency injection. Ở composition root (thường là file khởi tạo app, ví dụ main.ts hoặc một DI container), prompt agent nối các adapter cụ thể vào use case:
In the composition root (src/bootstrap.ts), wire
SendgridNotificationSender and PostgresUserRepository into
RequestPasswordReset via constructor injection. This is the only file
allowed to import both application/ and infrastructure/ together.
Bước 5 — Verify không có domain layer import infrastructure. Chạy dependency-cruiser (hoặc tương đương) để xác nhận:
npx depcruise application domain --config .dependency-cruiser.js
Nếu có vi phạm, dependency-cruiser sẽ liệt kê chính xác file và import lỗi — đưa kết quả này ngược lại cho AI agent để nó tự sửa:
dependency-cruiser reports application/use-cases/RequestPasswordReset.ts
imports infrastructure/notifications/SendgridNotificationSender.ts
directly. Fix this by depending only on the NotificationSender
interface and moving the concrete import to the composition root.
Quy trình 5 bước này — port trước, use case sau, adapter sau cùng, wiring riêng, verify bằng công cụ — là khuôn mẫu bạn có thể lặp lại cho bất kỳ tính năng nào trong kiến trúc hexagonal, và nó biến việc "giữ kiến trúc sạch khi dùng AI" từ một nỗ lực ý chí thành một quy trình có thể lặp lại và kiểm chứng được.
Mẹo: Luôn chạy dependency-cruiser (hoặc ArchUnit) ngay sau khi AI báo "xong việc" — đừng đợi tới lúc pull request được review mới phát hiện vi phạm.
Những điểm chính
- AI mặc định chọn pattern phổ biến nhất trong dữ liệu huấn luyện, không phải kiến trúc dự án bạn — im lặng đồng nghĩa với rủi ro vi phạm layering.
- Nêu rõ tên layer, hướng phụ thuộc, và import bị cấm ngay trong prompt; yêu cầu agent liệt kê layer trước khi viết code.
- Gọi tên chính xác design pattern (Repository, Strategy, Factory, CQRS...) kèm skeleton/interface để neo implementation, tránh yêu cầu mơ hồ kiểu "code sạch".
- Mã hóa quy tắc kiến trúc vào CLAUDE.md một lần để mọi phiên AI trong tương lai tự động kế thừa; viết rule dạng "được phép/không được phép" cụ thể, tham chiếu ADR.
- Kết hợp linter hướng phụ thuộc (dependency-cruiser, ArchUnit), self-critique của AI, CI gate, và review con người thành nhiều lớp kiểm chứng.
- Với hexagonal architecture: định nghĩa port trước, prompt use case ở application layer, prompt adapter ở infrastructure layer, wiring DI ở composition root, rồi verify bằng công cụ tự động.