Thiết Kế Codebase và Các Mẫu¶
Thiết kế codebase tốt là điều phân biệt một bản mẫu nghiên cứu (research prototype) với phần mềm sản xuất. File này bao gồm cấu trúc dự án, các nguyên tắc mã sạch (clean code), mẫu thiết kế (design patterns) liên quan đến ML, quản lý cấu hình, ghi log (logging), thiết kế API và đóng gói (packaging).
-
Hầu hết mã ML bắt đầu như một Jupyter notebook. Notebook phát triển, được sao chép, sửa đổi, chia sẻ, và cuối cùng trở thành một mớ hỗn độn không thể bảo trì với các biến toàn cục, cell chết và các số ma thuật. Thiết kế codebase là kỷ luật tổ chức mã để nó vẫn có thể hiểu và sửa đổi được khi dự án phát triển.
-
Đây không phải là tuân theo các quy tắc vì mục đích tự thân. Nó là về việc giảm thời gian giữa "tôi muốn thay đổi X" và "X đã được thay đổi và hoạt động." Trong một codebase được thiết kế tốt, thời gian đó là vài phút. Trong một codebase tồi, đó là nhiều ngày khai quật khảo cổ qua mì ống không tài liệu.
Cấu Trúc Dự Án¶
- Một bố cục dự án nhất quán cho phép bất kỳ ai (bao gồm bạn trong tương lai) điều hướng codebase ngay lập tức.
my_project/
├── src/my_project/ # mã nguồn (package có thể import)
│ ├── __init__.py
│ ├── data/ # tải dữ liệu và tiền xử lý
│ │ ├── __init__.py
│ │ ├── dataset.py
│ │ └── transforms.py
│ ├── models/ # kiến trúc mô hình
│ │ ├── __init__.py
│ │ ├── transformer.py
│ │ └── layers.py
│ ├── training/ # vòng lặp huấn luyện, bộ tối ưu
│ │ ├── __init__.py
│ │ ├── trainer.py
│ │ └── losses.py
│ └── utils/ # tiện ích dùng chung
│ ├── __init__.py
│ └── logging.py
├── configs/ # file cấu hình
│ ├── base.yaml
│ └── experiment_1.yaml
├── scripts/ # điểm vào (train, evaluate, serve)
│ ├── train.py
│ ├── evaluate.py
│ └── serve.py
├── tests/ # file kiểm tra (phản chiếu cấu trúc src/)
│ ├── test_dataset.py
│ ├── test_model.py
│ └── test_trainer.py
├── notebooks/ # chỉ để khám phá (không phải mã sản xuất)
├── pyproject.toml # siêu dữ liệu và phụ thuộc của dự án
├── README.md
├── .gitignore
└── Dockerfile
-
Bố cục
src/: đặt mã nguồn dướisrc/my_project/ngăn chặn các import vô tình từ thư mục hiện tại (che giấu các lỗi import có thể xuất hiện trong sản xuất). Cài đặt vớipip install -e .cho phát triển. -
Monorepo so với multi-repo: một monorepo giữ tất cả các dự án liên quan trong một kho lưu trữ (dễ thay đổi xuyên dự án, CI dùng chung). Multi-repo cho mỗi dự án kho lưu trữ riêng (ranh giới sạch hơn, quản lý phiên bản độc lập). Hầu hết các nhóm ML bắt đầu với monorepo và tách sau nếu cần.
-
Script so với thư viện: giữ các điểm vào (
train.py,evaluate.py) trongscripts/. Giữ logic có thể tái sử dụng trongsrc/. Một script huấn luyện nên chỉ khoảng 50 dòng: parse cấu hình, xây dựng dataset, xây dựng mô hình, xây dựng trainer, huấn luyện. Tất cả độ phức tạp nằm trong thư viện.
Nguyên Tắc Mã Sạch¶
- Đặt tên: điều có tác động lớn nhất bạn có thể làm. Một biến tên
xyêu cầu bạn đọc mã xung quanh để hiểu nó. Một biến tênlearning_ratetự giải thích.
# TỆ
def proc(d, n, lr):
for i in range(n):
for k, v in d.items():
v -= lr * g[k]
# TỐT
def update_parameters(parameters, num_steps, learning_rate):
for step in range(num_steps):
for name, param in parameters.items():
param -= learning_rate * gradients[name]
-
Nguyên lý Đơn Trách Nhiệm (Single Responsibility Principle): mỗi hàm/lớp làm một việc. Một hàm tên
load_data_and_train_modelđang làm hai việc và nên được tách. Điều này làm cho mỗi phần có thể kiểm tra độc lập, tái sử dụng và hiểu được. -
DRY (Don't Repeat Yourself) — nhưng không sớm quá. Nếu bạn copy-paste mã ba lần, hãy trích xuất nó thành một hàm. Nhưng đừng tạo một abstraction cho mã bạn mới chỉ dùng một lần. Abstraction sớm (premature abstraction) còn tệ hơn trùng lặp: nó thêm độ phức tạp mà không có lợi ích đã được chứng minh.
# Abstraction quá sớm (một use case, over-engineered)
class AbstractDataTransformPipelineFactory:
...
# Vừa đúng (trực tiếp, rõ ràng, được dùng ở ba chỗ)
def normalise_image(image, mean, std):
return (image - mean) / std
- Số ma thuật (Magic numbers): không bao giờ dùng các giá trị số nguyên không được giải thích.
# TỆ
if len(batch) > 32:
split_batch(batch, 32)
# TỐT
MAX_BATCH_SIZE = 32
if len(batch) > MAX_BATCH_SIZE:
split_batch(batch, MAX_BATCH_SIZE)
- Hàm nên ngắn: nếu một hàm không vừa trên một màn hình (~30 dòng), nó có thể đang làm quá nhiều. Trích xuất các khối logic thành các hàm helper với tên mô tả. Phần thân hàm sau đó đọc như một bản tóm tắt cấp cao.
Mẫu Thiết Kế cho ML¶
-
Mẫu thiết kế (design patterns) là các giải pháp tái sử dụng cho các vấn đề phổ biến. Dưới đây là những mẫu phù hợp nhất với codebase ML:
-
Factory pattern: tạo đối tượng mà không chỉ rõ lớp cụ thể. Hữu ích khi cấu hình của bạn bảo
model: "transformer"và bạn cần khởi tạo đúng lớp:
MODEL_REGISTRY = {
"transformer": TransformerModel,
"cnn": CNNModel,
"mlp": MLPModel,
}
def build_model(config):
model_cls = MODEL_REGISTRY[config["model"]]
return model_cls(**config["model_params"])
-
Điều này tách rời script huấn luyện khỏi các hiện thực mô hình cụ thể. Thêm một mô hình mới nghĩa là thêm một dòng vào registry, không phải sửa đổi vòng lặp huấn luyện.
-
Strategy pattern: hoán đổi thuật toán tại runtime. Hữu ích cho các hàm mất mát, bộ tối ưu, bộ lập lịch:
LOSS_FUNCTIONS = {
"mse": nn.MSELoss,
"cross_entropy": nn.CrossEntropyLoss,
"focal": FocalLoss,
}
loss_fn = LOSS_FUNCTIONS[config["loss"]]()
- Observer pattern (callbacks/hooks): cho phép các module phản ứng với sự kiện mà không có sự ghép nối chặt chẽ. Các framework huấn luyện (PyTorch Lightning, Keras) dùng callbacks rộng rãi:
class EarlyStopping:
def __init__(self, patience=5):
self.patience = patience
self.best_loss = float('inf')
self.counter = 0
def on_epoch_end(self, epoch, val_loss):
if val_loss < self.best_loss:
self.best_loss = val_loss
self.counter = 0
else:
self.counter += 1
if self.counter >= self.patience:
return "stop"
- Tiêm phụ thuộc (Dependency injection): truyền các phụ thuộc vào một hàm/lớp thay vì tạo chúng bên trong. Điều này làm cho việc kiểm tra dễ dàng (tiêm một mock) và cấu hình linh hoạt:
# TỆ: phụ thuộc cứng
class Trainer:
def __init__(self):
self.logger = WandbLogger() # không thể test nếu không có W&B
# TỐT: phụ thuộc được tiêm
class Trainer:
def __init__(self, logger):
self.logger = logger # có thể tiêm bất kỳ logger nào, bao gồm mock
Quản Lý Cấu Hình¶
-
Việc cứng hóa các siêu tham số, đường dẫn file và cài đặt mô hình làm cho các thử nghiệm không thể tái tạo và việc sửa đổi trở nên đau đớn. Ngoại vi hóa cấu hình (Externalise configuration) vào các file.
-
YAML là định dạng phổ biến nhất cho cấu hình ML:
# configs/experiment_1.yaml
model:
name: transformer
d_model: 512
n_heads: 8
n_layers: 6
training:
batch_size: 64
learning_rate: 3e-4
max_epochs: 100
early_stopping_patience: 10
data:
train_path: /data/train.parquet
val_path: /data/val.parquet
max_seq_length: 512
-
Hydra (Facebook) là một framework cấu hình hỗ trợ kết hợp (merge base config với các ghi đè dành riêng cho thử nghiệm), ghi đè dòng lệnh (
python train.py training.lr=1e-3), và multi-run (quét qua các siêu tham số). -
argparse đơn giản hơn cho các script với một vài tham số:
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--lr", type=float, default=3e-4)
parser.add_argument("--batch-size", type=int, default=64)
parser.add_argument("--config", type=str, default="configs/base.yaml")
args = parser.parse_args()
- Thực hành tốt nhất: có một cấu hình cơ sở với tất cả các mặc định, và các cấu hình cho từng thử nghiệm chỉ ghi đè những gì thay đổi. Theo dõi cấu hình của mọi thử nghiệm cùng với kết quả của nó.
Ghi Log (Logging) và Khả Năng Quan Sát¶
- Câu lệnh
printdành cho gỡ lỗi. Logging dành cho sản xuất:
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger.debug("Batch loaded: %d samples", len(batch)) # ồn, để gỡ lỗi
logger.info("Epoch %d: loss=%.4f, lr=%.6f", epoch, loss, lr) # hoạt động bình thường
logger.warning("GPU memory >90%%, consider reducing batch size")
logger.error("Failed to load checkpoint: %s", path) # lỗi có thể phục hồi
logger.critical("CUDA out of memory, aborting") # nghiêm trọng
-
Tại sao không dùng print: logging hỗ trợ các cấp độ (lọc thông báo debug trong sản xuất), định dạng (dấu thời gian, tên module) và các bộ xử lý (handler) (ghi vào file, gửi đến hệ thống giám sát) mà không cần thay đổi các lời gọi logging.
-
Ghi log có cấu trúc (Structured logging) xuất ra các định dạng có thể parse được bằng máy (JSON) cùng với thông điệp có thể đọc được bởi con người. Điều này cho phép tìm kiếm và cảnh báo trên các trường cụ thể:
Thiết Kế API¶
-
Nếu mô hình của bạn sẽ được các dịch vụ khác sử dụng (một ứng dụng web, ứng dụng di động, pipeline ML khác), nó cần một API (Giao diện Lập trình Ứng dụng).
-
API REST sử dụng các phương thức HTTP:
GETđể đọc,POSTđể tạo/dự đoán,PUTđể cập nhật,DELETEđể xóa. Các endpoint tuân theo đặt tên dựa trên tài nguyên:
POST /api/v1/predict # gửi đầu vào, nhận dự đoán
GET /api/v1/models # liệt kê các mô hình có sẵn
GET /api/v1/models/{id} # xem chi tiết mô hình
POST /api/v1/models/{id}/predict # dự đoán với một mô hình cụ thể
- FastAPI là framework Python hàng đầu cho việc phục vụ ML:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class PredictRequest(BaseModel):
text: str
class PredictResponse(BaseModel):
label: str
confidence: float
@app.post("/predict", response_model=PredictResponse)
async def predict(request: PredictRequest):
result = model.predict(request.text)
return PredictResponse(label=result.label, confidence=result.score)
-
FastAPI tự động tạo tài liệu API (Swagger UI tại
/docs), xác thực đầu vào/đầu ra với các mô hình Pydantic và hỗ trợ async cho thông lượng cao. -
gRPC nhanh hơn REST cho giao tiếp nội bộ giữa các dịch vụ. Nó sử dụng Protocol Buffers (tuần tự hóa nhị phân, nhỏ hơn và nhanh hơn JSON) và hỗ trợ streaming. Được sử dụng bởi TensorFlow Serving, Triton Inference Server và nhiều kiến trúc microservice.
Đóng Gói và Phân Phối¶
- Làm cho mã của bạn có thể cài đặt như một package cho phép người khác (và script của chính bạn) import nó một cách sạch sẽ:
# pyproject.toml
[project]
name = "my-ml-project"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"torch>=2.0",
"jax>=0.4",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = ["pytest", "ruff", "mypy"]
[build-system]
requires = ["setuptools>=64"]
build-backend = "setuptools.backends._legacy:_Backend"
-
Cài đặt có thể chỉnh sửa (Editable install) (
-e): các thay đổi đối với mã nguồn của bạn được phản ánh ngay lập tức mà không cần cài đặt lại. Cần thiết trong quá trình phát triển. -
Ghim phụ thuộc (Pinning dependencies):
requirements.txtvới các phiên bản chính xác (torch==2.2.1, không phảitorch>=2.0) đảm bảo khả năng tái tạo. Dùngpip freeze > requirements.txtđể chụp môi trường hiện tại của bạn. Để quản lý phụ thuộc tinh vi hơn, hãy dùnguv,poetryhoặcpip-tools.
Làm Việc với Các Tác Nhân AI Viết Mã¶
-
Các tác nhân AI viết mã (Claude Code, GitHub Copilot, Cursor, v.v.) hiện là một phần của quy trình công việc kỹ thuật chuyên nghiệp. Sử dụng tốt, chúng tăng tốc đáng kể sự phát triển. Sử dụng kém, chúng đưa vào các lỗi tinh vi, làm xói mòn hiểu biết của bạn về codebase của chính mình và tạo ra ảo tưởng về năng suất.
-
Mô hình tinh thần đúng: một tác nhân là một người lập trình cặp nhanh nhưng thiếu kinh nghiệm. Nó có thể viết mã nhanh, biết cú pháp và các mẫu tiêu chuẩn, và đã đọc nhiều tài liệu hơn bạn sẽ bao giờ có thể. Nhưng nó không hiểu hệ thống cụ thể của bạn, các ràng buộc kinh doanh của bạn, các trường hợp biên của bạn, hoặc lý do tại sao đằng sau các quyết định thiết kế của bạn. Bạn là kỹ sư cấp cao; tác nhân là junior. Bạn chỉ đạo, xem xét và chịu trách nhiệm.
Khi Tác Nhân Tỏa Sáng¶
-
Mẫu và khung sườn (Boilerplate and scaffolding): tạo Dockerfile, cấu hình CI, test fixtures, định nghĩa lớp dữ liệu, argparse setup. Chúng tuân theo các mẫu quen thuộc và nhàm chán khi viết tay. Hãy để tác nhân tạo chúng, sau đó review tính đúng đắn.
-
Viết kiểm thử (tests): mô tả hành vi của hàm và tác nhân tạo ra các test case. Nó thường bắt được các trường hợp biên bạn có thể bỏ lỡ (đầu vào rỗng, giá trị âm, Unicode). Luôn đọc các test được tạo ra — chúng xác minh các giả định của bạn, không chỉ mã của bạn.
-
Tái cấu trúc (Refactoring): "trích xuất khối này thành một hàm," "chuyển đổi lớp này sang dùng dataclasses," "thêm type hints vào module này." Các biến đổi cơ học nơi ý định rõ ràng và nguy cơ lỗi tinh vi thấp.
-
Khám phá và tạo mẫu thử (Exploration and prototyping): "viết một script nhanh để benchmark độ trễ suy luận" hoặc "chỉ tôi cách sử dụng API tokeniser của HuggingFace." Tác nhân đưa bạn đến một điểm khởi đầu có thể hoạt động nhanh hơn là đọc tài liệu.
-
Tài liệu và docstrings: tác nhân có thể tạo tài liệu từ cấu trúc mã của bạn. Review tính chính xác, nhưng công việc chân tay được tự động hóa.
-
Hỗ trợ gỡ lỗi: dán một error traceback và yêu cầu chẩn đoán. Tác nhân thường có thể xác định nguyên nhân gốc và đề xuất sửa chữa, đặc biệt cho các vấn đề phổ biến (shape mismatches, lỗi import, CUDA out of memory).
Khi KHÔNG NÊN Dựa Vào Tác Nhân¶
-
Các quyết định kiến trúc mới: nếu bạn đang thiết kế một pipeline huấn luyện mới, tác nhân sẽ cho bạn một câu trả lời chung chung. Nó không biết các ràng buộc dữ liệu của bạn, yêu cầu độ trễ hay chuyên môn của nhóm. Dùng tác nhân để hiện thực thiết kế bạn đã suy nghĩ ra.
-
Mã quan trọng về bảo mật: xác thực (authentication), mã hóa (encryption), làm sạch đầu vào (input sanitisation). Tác nhân có thể tạo mã trông có vẻ đúng nhưng có các lỗ hổng tinh vi (SQL injection, mặc định không an toàn, tấn công timing). Mã bảo mật nên được viết bởi người hiểu mô hình đe dọa và được review bởi người khác.
-
Các vòng lặp nội tại quan trọng về hiệu năng: tác nhân sẽ viết mã đúng nhưng ngây thơ. Đối với GPU kernels, cấu trúc dữ liệu quan trọng về bộ nhớ, hoặc các đường dẫn phục vụ nhạy cảm với độ trễ, bạn cần hiểu các ràng buộc phần cứng (chương 13, chương 16) và tối ưu hóa có chủ đích.
-
Mã bạn không hiểu: nếu tác nhân tạo ra 200 dòng và bạn không thể giải thích mỗi dòng làm gì, đừng commit nó. Bạn giờ đang bảo trì mã bạn không hiểu, và khi nó hỏng (nó sẽ hỏng), bạn không thể gỡ lỗi nó. Đây là chế độ hỏng hóc phổ biến và nguy hiểm nhất.
Kỷ Luật Review¶
-
Luôn đọc từng dòng của mã được tạo trước khi commit. Điều này không phải tùy chọn. Mã của tác nhân là một bản nháp, không phải sản phẩm hoàn chỉnh. Hãy đối xử với nó chính xác như một pull request từ đồng nghiệp: review nó một cách phê phán.
-
Những gì cần kiểm tra:
- Tính đúng đắn: nó có thực sự làm những gì bạn yêu cầu không? Các tác nhân thường giải một vấn đề hơi khác với vấn đề bạn dự định.
- Các trường hợp biên: nó có xử lý đầu vào rỗng, giá trị None, số âm, đầu vào rất lớn không? Các tác nhân thường bỏ qua xử lý trường hợp biên.
- API bị ảo giác: tác nhân có thể gọi các hàm hoặc sử dụng các tham số không tồn tại, đặc biệt cho các thư viện mới hơn hoặc ít phổ biến hơn. Xác minh rằng mọi lời gọi API là thật.
- Kỹ thuật quá mức: các tác nhân có xu hướng tạo ra nhiều mã hơn cần thiết. Một giải pháp 50 dòng cho một vấn đề 10 dòng thêm độ phức tạp mà không có lợi ích. Đơn giản hóa không thương tiếc.
- Bảo mật: bí mật được mã hóa cứng, đầu vào người dùng không được làm sạch, các mặc định không an toàn. Tác nhân không nghĩ theo hướng đối nghịch.
- Tính nhất quán về phong cách: mã được tạo có khớp với các quy ước của dự án bạn không (đặt tên, mẫu, xử lý lỗi)?
Cách Viết Prompt Tốt¶
-
Chất lượng đầu ra của tác nhân tỷ lệ thuận với chất lượng chỉ dẫn của bạn. Các prompt mơ hồ tạo ra mã mơ hồ.
-
Tệ: "viết một data loader"
-
Tốt: "viết một PyTorch DataLoader cho file CSV với các cột 'text' và 'label'. Tokenise văn bản bằng tokeniser HuggingFace 'bert-base-uncased' với max_length=512. Trả về input_ids, attention_mask và label dưới dạng tensor. Xử lý trường hợp CSV có giá trị thiếu trong cột label bằng cách bỏ qua các hàng đó."
-
Cung cấp ngữ cảnh: nói cho tác nhân về cấu trúc dự án của bạn, mã hiện có, các ràng buộc và quy ước. Càng nhiều ngữ cảnh, đầu ra càng tốt.
-
Chỉ rõ ràng buộc: "chỉ sử dụng thư viện chuẩn," "phải hoạt động với Python 3.10," "không sử dụng biến toàn cục," "tuân theo mẫu hiện có trong
src/models/transformer.py." -
Yêu cầu giải thích: "hiện thực X và giải thích các quyết định thiết kế chính." Điều này buộc tác nhân phải nêu rõ lý do của nó, giúp bạn dễ dàng phát hiện các giả định sai lầm.
Sử Dụng Cổng Chất Lượng để Bắt Lỗi Tác Nhân¶
-
Cơ sở hạ tầng chất lượng hiện có của bạn (file 04) bắt lỗi tác nhân cũng tốt như lỗi con người:
- Kiểm tra kiểu (mypy): bắt các chữ ký API bị ảo giác và sự không khớp kiểu.
- Linting (ruff): bắt các import không được sử dụng, biến không được định nghĩa và vi phạm phong cách.
- Kiểm thử (pytest): nếu mã của tác nhân vượt qua bộ kiểm thử của bạn, nó có nhiều khả năng đúng hơn. Nếu bạn không có kiểm thử, hãy viết chúng trước khi yêu cầu tác nhân hiện thực tính năng (phát triển hướng kiểm thử — TDD — hoạt động đặc biệt tốt với các tác nhân).
- Pipeline CI: chạy tất cả các công cụ trên tự động trên mỗi commit.
-
Sự kết hợp của "tác nhân viết mã" + "cổng chất lượng xác minh nó" năng suất hơn một trong hai riêng lẻ. Tác nhân nhanh nhưng cẩu thả; các cổng kỹ lưỡng nhưng không viết mã. Cùng nhau, bạn có được tốc độ và tính đúng đắn.
Cái Bẫy Năng Suất¶
-
Rủi ro lớn nhất của các tác nhân viết mã là ảo tưởng về năng suất. Bạn có thể tạo ra 500 dòng mã trong 10 phút. Nhưng nếu bạn mất 2 giờ gỡ lỗi 500 dòng đó vì bạn không hiểu chúng, bạn đã chậm hơn so với việc tự viết 200 dòng trong 30 phút.
-
Năng suất thực sự với các tác nhân đến từ:
- Giữ quyền kiểm soát: bạn quyết định kiến trúc, tác nhân điền vào hiện thực.
- Hiểu những gì được tạo ra: nếu bạn không thể giải thích nó, hãy viết lại nó hoặc yêu cầu tác nhân đơn giản hóa.
- Đầu tư vào các cổng chất lượng: kiểm thử, kiểu và linting phân bổ chi phí của chúng qua mỗi tương tác với tác nhân.
- Sử dụng tác nhân cho điểm yếu của bạn: nếu bạn giỏi thuật toán nhưng chậm viết test, hãy để tác nhân viết test. Nếu bạn nhanh với mã UI nhưng không quen với truy vấn cơ sở dữ liệu, hãy để tác nhân phác thảo SQL. Chơi theo điểm mạnh của bạn, ủy thác điểm yếu của bạn.
-
Các kỹ sư khai thác được nhiều nhất từ các tác nhân viết mã là những người đã biết viết mã tốt. Tác nhân khuếch đại kỹ năng hiện có của bạn; nó không thay thế nó. Hiểu về cấu trúc dữ liệu, thuật toán, thiết kế hệ thống và kỹ thuật phần mềm (toàn bộ chương này) là điều cho phép bạn chỉ đạo tác nhân hiệu quả và đánh giá đầu ra của nó một cách phê phán.