Code With the Grain: Kỹ năng ngăn AI coding agent làm phình to repo của bạn

Được xuất bản vào Sep 22, 2026 bởi Štefan Moravík.
AI Agents Claude Code Developer Tools Code Quality
Trong trang này 11 phút đọc
  1. 1Phiên bản ngắn gọn
  2. 2Kiểu thất bại mà điều này nhắm tới
  3. 3Grain
  4. 4Ponytail
  5. 5Làm cho agent thực sự thực hiện điều đó
  6. 6Chuyện đã xảy ra trong repo, kể cả phần không hiệu quả

Phiên bản ngắn gọn

  • AI coding agent giỏi thêm mã và kém trong việc nhận ra rằng mã đó đã tồn tại. Nếu để mặc, mỗi helper đầu tiên trong repo sẽ có thêm một helper thứ hai, và mọi người đọc đều phải kiểm tra cả hai mới tin được một trong hai.
  • Giải pháp là hai thói quen được viết ra để agent tuân theo. Grain: tìm ví dụ gần nhất đã tồn tại của cùng loại thứ đó và sao chép hình dạng, vị trí và cách đặt tên của nó. Ponytail: leo một cái thang từ “cái này có cần tồn tại không” đến “mức tối thiểu chạy được” và dừng ở nấc đầu tiên đủ vững.
  • Cả hai giờ đã là một kỹ năng công khai, giấy phép MIT: grain-skill trên GitHub . Nó gồm các quy tắc soạn thảo, một lượt review với các thẻ cố định, và dạng của dòng kết thúc buộc agent phải nêu tên thứ nó đã sao chép.
  • Trong repo chính của chúng tôi từ tháng 8: 386 issue và pull request mang một bình luận kế hoạch nêu tên mẫu chuẩn cần sao chép, trong tổng số 1.333 commit đổ vào một cây khoảng 18.000 file. 35 chỗ trong mã mang bình luận ponytail: nêu một sự đơn giản hóa có chủ đích và đường nâng cấp của nó.
  • Phát hiện khó chịu: agent gọi kỹ năng này như một kỹ năng đúng 0 lần. Các quy tắc vẫn hiệu quả vì tôi cũng đặt chúng trong CLAUDE.md của dự án, trong các định nghĩa worker agent và trong checklist review. Hãy đặt quy tắc ở nơi agent vốn đã đọc.

Tôi từng viết về chuyện này một lần và tôi không hài lòng với kết quả, vì nó mô tả nguyên lý chứ không phải cơ chế. Đây là phiên bản có cơ chế.

Kiểu thất bại mà điều này nhắm tới

Ba ví dụ từ backend của chúng tôi, đều là thật, đều đến từ những pull request do agent viết và đều đã vượt qua test.

Một route REST mới lấy thẳng application service từ một factory. Mọi route anh em trong repo đều đi route → controller → service → repository, mỗi chặng đều được inject. Route này rút chuỗi đó thành route → service. Nó chạy được. Nhưng điều đó cũng có nghĩa là chỗ duy nhất diễn ra việc kiểm tra request và định hình response cho tính năng đó đã không tồn tại, và agent tiếp theo chạm vào tính năng này sẽ có hai dạng để chọn.

Một service quá lớn bị tách thành _payloads.py, _protocol.py và _tokens.py, mỗi file là một module toàn các async def trần, với service gọi protocol.discover(...) qua import. Kiểm tra độ dài file chuyển xanh. Đúng thứ mà dependency injection sinh ra để ngăn chặn quay trở lại ngay: một cộng sự được chọn ở thời điểm import, không thể thay thế nếu không patch module, và vắng mặt trong constructor, nơi ghi lại service phụ thuộc vào những gì.

Một normalizer thứ hai. Đã có sẵn một cái. Agent không tìm thấy nó, viết thêm tám dòng gần như làm đúng việc đó, và giờ hai hàm với hành vi khác nhau ở các trường hợp biên đều nhận là chuẩn hóa cùng một field.

Không cái nào là bug theo nghĩa mà một test bắt được. Mỗi cái là một agent chọn đoạn mã nó có thể sinh ra thay vì đoạn mã đã tồn tại. Làm vậy qua vài trăm pull request thì repo không còn hình dạng nữa.

FlowHunt Logo

Sẵn sàng phát triển doanh nghiệp của bạn?

Bắt đầu dùng thử miễn phí ngay hôm nay và xem kết quả trong vài ngày.

Grain

Mã mới phải không thể phân biệt được với đoạn mã xung quanh nó. Kỹ năng này biến câu đó thành năm bước agent thực hiện trước khi viết.

Tìm mẫu chuẩn trước. Với một endpoint, service, repository, query hook, store slice hay component, hãy định vị cái gần nhất đã tồn tại và sao chép hình dạng, vị trí file và cách đặt tên của nó. Đừng phát minh cấu trúc khi đã có một pattern. Trong repo của chúng tôi, slice branding là mẫu chuẩn được chỉ định cho backend vì nó nhỏ, sạch và trải khắp mọi tầng, và bảng trong kỹ năng nói rõ với từng loại artifact thì cần mở file nào và sao chép cái gì.

Tôn trọng các tầng. Presentation gọi application, application gọi domain, và infrastructure triển khai domain. Một import mới vượt qua các ranh giới đó sẽ trượt kiểm tra kiến trúc. Hãy đọc tài liệu về các tầng, đừng tự suy diễn lại.

Grain cục bộ đối đầu grain của repo. Hãy khớp với các anh em của mình. Nhưng nếu chính các anh em đó lệch khỏi chuỗi chuẩn, hãy theo chuỗi chuẩn và ghi nhận sự lệch thay vì sao chép cái đã trôi dạt. “Nó khớp với năm route còn lại trong file này” là chưa đủ nếu cả sáu đều bỏ qua một tầng mà slice chuẩn có dùng. Bạn đã so sánh quá cục bộ.

Dùng lại helper chuẩn. Nếu một helper, type hay pattern đã có trong repo, hãy dùng nó. Hai normalizer buộc mọi người đọc phải diff cả hai.

Tên mang ý nghĩa. Dùng lại từ vựng đã có trong mã. Một cái tên mới cho một khái niệm đã tồn tại là một bug đang chờ người đọc.

Bài kiểm tra ở cuối là bài tôi đưa cho reviewer: nếu bạn có thể nhận ra agent viết những dòng nào chỉ dựa vào hình dạng của chúng, thì nó đã chống lại grain.

Ponytail

Lười nghĩa là hiệu quả, không phải cẩu thả. Đoạn mã tốt nhất là đoạn mã không bao giờ được viết. Hãy hiểu vấn đề một cách đầy đủ trước, mọi file mà thay đổi chạm tới, luồng thực tế đầu-cuối, rồi leo cái thang này và dừng ở nấc đầu tiên đủ vững.

Thang lười biếng: có cần tồn tại không, đã có trong codebase, thư viện chuẩn, nền tảng gốc, dependency đã cài, một dòng, và chỉ khi đó mới đến đoạn mã tối thiểu chạy được

Có hai quy tắc đi kèm với cái thang.

Sửa bug là sửa nguyên nhân gốc, không phải triệu chứng. Một ticket nêu ra triệu chứng. Hãy grep mọi caller của hàm bạn sắp chạm vào. Một guard trong hàm dùng chung là diff nhỏ hơn một guard ở mọi caller, và chỉ vá đúng đường mà ticket nêu tên sẽ để lại các caller anh em vẫn hỏng.

Giữ diff tối thiểu. Diff là thứ mà review đọc. Không refactor tiện tay, không đổi tên, không định dạng lại, không cải tiến kiểu “nhân đây tôi làm luôn”, không chạm vào những file mà nhiệm vụ không yêu cầu. Mỗi thứ như vậy chôn vùi thay đổi thật. Nếu diff lớn hơn vấn đề, hãy cắt lại. Những cải tiến bạn phát hiện là một issue tiếp theo, không phải commit thêm trên nhánh này.

Và các giới hạn, vì “lazy” là từ dễ bị hiểu sai nhất. Không bao giờ đơn giản hóa để bỏ đi việc kiểm tra đầu vào ở ranh giới tin cậy, xử lý lỗi nhằm ngăn mất dữ liệu, bảo mật, những điều cơ bản về khả năng tiếp cận, hay bất cứ thứ gì người dùng yêu cầu. Không bao giờ lười trong việc hiểu: cái thang rút ngắn giải pháp, không bao giờ rút ngắn việc đọc. Một diff nhỏ ở sai chỗ là một bug thứ hai. Logic không tầm thường phải để lại một kiểm tra chạy được, thứ nhỏ nhất sẽ fail nếu logic hỏng.

Khi agent cố ý dừng ở một nấc và biết giới hạn trần, nó nói rõ điều đó trong mã:

# ponytail: khóa toàn cục, khóa theo từng tài khoản nếu thông lượng quan trọng

Hiện có 35 chỗ như vậy trong repo của chúng tôi. Mỗi chỗ là một quyết định mà người đọc tương lai nếu không có sẽ phải tự dựng lại, và là một dấu mốc cho biết cần nhìn vào đâu khi chạm giới hạn trần.

Làm cho agent thực sự thực hiện điều đó

Một nguyên lý trong tài liệu không phải là một hành vi. Ba cơ chế đã biến hai điều này thành hành vi, và tôi sẽ xếp hạng chúng theo thứ tự sau.

Bản brief mang theo mẫu chuẩn. Trong mô hình kiến trúc sư , mô hình chính viết một bản brief cho một worker, và trường Design của bản brief nêu pattern gần nhất đã tồn tại cần sao chép cùng thay đổi tối thiểu ở nguyên nhân gốc. Worker không phải tự đi tìm grain. Nó được đưa sẵn. Trong repo của chúng tôi, đó chính là 491 bình luận kế hoạch trên 386 issue và pull request: một bước lập kế hoạch nêu rõ cần sao chép cái gì trước khi bất kỳ ai viết.

Agent kết thúc bằng thứ nó đã sao chép. Báo cáo của mọi người triển khai kết thúc bằng một dòng, mirrored: <path>, hoặc nêu pattern mới và lý do không có cái anh em nào phù hợp. Việc này rẻ và là trợ giúp review tốt nhất mà tôi từng thêm vào. Một dòng mirrored: trỏ tới sai loại file cho bạn biết diff sai trước cả khi bạn mở nó. Một dòng thiếu cho bạn biết agent đã không tìm.

Review thực thi cùng một chuẩn, theo một thứ tự cố định. Kỹ năng review của chúng tôi có hai lượt với các thẻ cố định, và chính các thẻ này khiến các phát hiện có thể so sánh được giữa các reviewer và giữa các agent.

Hai lượt review: lượt cấu trúc gắn thẻ diverge, duplicate, layer, misplaced, reach-in, inconsistent, own và chain; lượt cắt gắn thẻ delete, stdlib, native, yagni và shrink. Cấu trúc chạy trước vì tính nhất quán thắng việc cắt bớt

Cấu trúc trước. Đoạn này có lệch khỏi mẫu chuẩn gần nhất, có trùng lặp một helper đã có, có vượt qua một tầng, có nằm sai chỗ, có thò vào nội bộ của module khác, có dùng tên không nhất quán, có tự nhận sở hữu thứ lẽ ra phải inject, hay có phá vỡ chuỗi gọi chuẩn không? Rồi đến lượt cắt. Cái gì có thể bị xóa, thay bằng thư viện chuẩn, thay bằng một tính năng nền tảng, bỏ đi vì chỉ là phỏng đoán, hoặc thu nhỏ lại?

Thứ tự này là một quy tắc, không phải sở thích. Tính nhất quán thắng việc cắt bớt. Một cú cắt để lại đoạn mã lệch khỏi các anh em của nó không phải là thắng lợi, vì người đọc giờ phải học hai hình dạng để tiết kiệm tám dòng. Chạy cấu trúc trước và lượt cắt chỉ chạm vào đoạn mã vốn đã khớp.

Chuyện đã xảy ra trong repo, kể cả phần không hiệu quả

Từ ngày 1 tháng 8, repo chính của chúng tôi đã nhận 1.333 commit, phần lớn do agent viết và qua review. 386 issue và pull request trong giai đoạn đó mang một bình luận kế hoạch nêu mẫu chuẩn cần sao chép, tổng cộng 491 bình luận như vậy. Chuỗi chuẩn là hình dạng mặc định của một endpoint mới, và hai route cũ bỏ qua controller được liệt kê trong tài liệu như một sự lệch đang được loại bỏ dần, không phải một phong cách thứ hai.

35 bình luận ponytail: là con số nhỏ hơn tôi tưởng và tôi cho là đúng. Hầu hết các đơn giản hóa không đáng để lại bình luận. Những cái đáng, là những cái có một giới hạn trần.

Giờ đến phần tôi không muốn báo cáo. Tôi đã phát hành grain và ponytail như một kỹ năng của Claude Code: một mô tả trong chỉ mục kỹ năng, một phần thân tải theo nhu cầu, và một dòng trong CLAUDE.md nói rằng phải gọi nó trước khi viết mã. Qua năm tuần transcript, agent gọi nó theo tên đúng 0 lần. Dòng kết thúc mirrored: xuất hiện trong bảy phản hồi.

Các quy tắc vẫn được tiếp nhận. Chúng được tiếp nhận vì cùng đoạn văn đó cũng nằm ở ba nơi agent đọc mà không cần chọn: CLAUDE.md của dự án, thứ nằm trong context ngay từ lượt đầu; các định nghĩa worker agent, nơi ghi “sao chép hình dạng, vị trí và cách đặt tên của ví dụ gần nhất đã tồn tại” và “chọn giải pháp lười nhất mà thực sự chạy được” như những quy tắc, chứ không phải một kỹ năng để cân nhắc; và checklist review, nơi các thẻ khiến một pull request vi phạm sẽ trượt. Phần thân của kỹ năng trở thành tài liệu tham chiếu chuẩn mà ba nơi đó trỏ tới. Như một yếu tố kích hoạt, nó không làm gì cả.

Vậy bài học tôi muốn truyền lại cho bất kỳ ai đóng gói quy ước cho agent là thế này. Kỹ năng là chỗ tốt cho phiên bản dài của một quy tắc. Nó là chỗ tồi cho chính quy tắc đó, vì agent phải quyết định tải nó và phần lớn sẽ không tải. Hãy đặt phiên bản một đoạn vào file luôn nằm trong context, đặt phiên bản mệnh lệnh vào định nghĩa của agent viết mã, và đặt phần kiểm tra vào review chặn merge. Rồi trỏ cả ba vào kỹ năng để lấy chi tiết.

Bản công khai tại github.com/LivinTribunal/grain-skill được bố trí theo cách đó: một SKILL.md ngắn với các quy tắc agent có thể giữ trong đầu, tài liệu đầy đủ với cái thang và các anti-pattern, các thẻ review, và một README nói rõ đoạn nào nên đặt vào CLAUDE.md của bạn và đoạn nào vào định nghĩa implementer của bạn. Hãy thay ví dụ mẫu bằng mẫu chuẩn của chính bạn và thế là xong.

Câu hỏi thường gặp

Štefan là một kỹ sư AI và phần mềm đang xây dựng FlowHunt. Ngoài sản phẩm itself, anh thiết kế các quy trình kỹ thuật phần mềm agentic cho các nhà phát triển giúp giảm chi phí phát triển đồng thời nâng cao chất lượng mã.

Štefan Moravík
Štefan Moravík
Kỹ sư AI & Phần mềm

Trao cho agent của bạn những quy ước mà đội của bạn vốn đã tuân theo

FlowHunt cho phép bạn gắn các quy tắc, ví dụ và bước review mà agent cần tuân theo vào mọi workflow, để kết quả của nó khớp với đoạn mã mà nó được đưa vào.