Subagent trong Claude Code: thiết kế agent chuyên môn hóa cho codebase lớn — hoatq.dev

cat blog/.md

Subagent trong Claude Code: thiết kế agent chuyên môn hóa cho codebase lớn

date: tags: ai, claude-code, developer-tools, architecture, workflow

Khi codebase lớn dần, có một vấn đề mình gặp liên tục với AI assistant: main agent (agent chính) cố làm tất cả nên cửa sổ ngữ cảnh (context window — bộ nhớ ngắn hạn của AI trong một phiên làm việc) ngập ngụa, danh sách công cụ dài cả trăm dòng, và chất lượng phản hồi giảm dần theo từng session. Cùng một agent phải lo review code, tạo migration, chạy Docker, viết test. Giống như bắt một dev fullstack mới vào nghề vừa deploy vừa kiểm tra bảo mật vừa viết unit test — mỗi việc làm được nhưng không việc nào xuất sắc.

Giải pháp là subagent (agent con): thay vì một agent khổng lồ làm tất cả, chia thành nhiều agent chuyên môn, mỗi cái có phạm vi hẹp, danh sách công cụ đúng đủ, và ngữ cảnh riêng. Main agent chỉ đóng vai “project manager”, giao việc (delegate) cho đúng agent con phù hợp. Bài này tổng hợp những gì mình rút ra sau khi thiết kế 12 subagent cho một monorepo (một repo Git chứa nhiều service/app) production.

Subagent khác skill ở chỗ nào

Hai khái niệm dễ nhầm, nên phân biệt trước khi đi sâu.

Skill (đã nói kỹ trong bài cấu trúc SKILL.md) là một đoạn hướng dẫn được đưa vào ngữ cảnh của agent đang chạy. Không có tiến trình riêng, không có bộ nhớ riêng — nó chỉ “dạy” agent hiện tại cách làm một việc.

Subagent thì khác hẳn: là một agent độc lập, có ngữ cảnh riêng, được main agent khởi tạo thông qua công cụ Task với một yêu cầu cụ thể. Chạy xong, nó trả một bản tóm tắt về main agent rồi kết thúc — main agent không thấy được quá trình xử lý chi tiết bên trong.

Nói ngắn gọn: skill dạy main agent làm việc, subagent thay main agent làm việc. Hai thứ bổ sung nhau chứ không thay thế.

Khi nào nên dùng subagent? Mình rút ra ba tình huống rõ ràng. Thứ nhất, khi công việc cần đọc nhiều file để trả lời nhưng main agent không cần nhớ chi tiết — giao đi, lấy tóm tắt về là đủ. Thứ hai, khi công việc có phạm vi hẹp với danh sách công cụ giới hạn — tách ra để hướng dẫn tập trung hơn và giảm rủi ro agent làm bậy. Thứ ba, khi công việc đơn giản có thể chuyển sang model rẻ hơn (Haiku thay vì Sonnet/Opus) để tiết kiệm chi phí.

Cấu trúc AGENT.md

Subagent sống ở .claude/agents/{slug}/AGENT.md. Format tương tự SKILL.md nhưng có thêm vài field quan trọng. Đây là file docker-ops thật đang chạy trong project của mình:

---
name: docker-ops
description: "Run development operations inside Docker containers — build, test, lint, migrate, and debug services correctly using docker compose exec."
model: haiku
tools:
  - Bash
  - Read
  - Grep
  - Glob
maxTurns: 20
---

# Docker Operations Agent

You execute development commands inside Docker containers for backend services.

## Response Language
Always respond in **Vietnamese**.

## Service Map
| Service  | Container | Port |
|----------|-----------|------|
| identity | identity  | 8001 |
| catalog  | catalog   | 8002 |

## Commands Reference
```bash
docker compose exec {service} pytest -x -v
docker compose exec {service} alembic upgrade head
```

Từng field có vai trò riêng, và viết sai bất kỳ field nào cũng làm subagent mất tác dụng.

name là định danh dùng để gọi từ main agent. Nên trùng tên folder, viết theo kiểu kebab-case (các từ nối bằng dấu gạch ngang), và mô tả chức năng rõ: docker-ops, migration-writer, security-auditor. Đừng đặt kiểu helper-1 rồi vài tháng sau nhìn lại không hiểu cái nào là cái nào.

description là trường quan trọng nhất, vì main agent dùng chính nó để quyết định có giao việc cho subagent này không. Viết mơ hồ kiểu “helps with backend stuff” — main agent sẽ không bao giờ chọn. Viết cụ thể như “runs pytest, alembic migrations, ruff check via docker compose exec” — main agent match chính xác mỗi khi user nói “chạy test cho service X”. Coi description như mô tả công việc trong tin tuyển dụng: càng rõ thì càng tuyển đúng người.

model chọn theo độ khó công việc, và đây là nơi tiết kiệm chi phí nhiều nhất. Việc cơ học như chạy command, format output thì Haiku dư sức. Việc cần hiểu code và tổng hợp (viết test, review code) dùng Sonnet. Việc cần suy luận sâu như kiểm tra bảo mật hay thiết kế kiến trúc thì mới cần Opus. Mình có 12 subagent, 8 trong số đó chạy Haiku — mặc định chọn Opus cho mọi thứ là cách đốt tiền nhanh nhất mà kết quả không hơn.

tools là danh sách công cụ subagent được phép dùng, và nguyên tắc là thu hẹp hết mức có thể. Agent docker-ops chỉ cần Bash, Read, Grep, Glob — không cho Write hay Edit vì nó không được phép sửa code. Agent security-auditor thì chỉ cần Read, Grep, Glob — chỉ đọc, không có Bash để tránh nó tự chạy command linh tinh. Danh sách công cụ hẹp có ba lợi ích: hướng dẫn ngắn hơn (ít mô tả công cụ), agent khó làm bậy hơn, và tìm nguyên nhân lỗi dễ hơn khi có sự cố.

maxTurns giới hạn số vòng lặp. Subagent lặp quá nhiều thường là dấu hiệu nó đã lạc đề — đừng tăng maxTurns lên 200 để “cố gắng hơn”, mà giảm xuống và xem tại sao nó không xong. Mình đặt 15-25 cho công việc trung bình, 50+ chỉ cho việc tìm kiếm/khảo sát code sâu.

Phần nội dung viết cho AI, không phải cho người

Phần nội dung Markdown sau phần metadata (frontmatter) không phải tutorial hướng dẫn. Nó là bản mô tả vai trò cho một AI sẽ nhận việc — nên viết như viết bản yêu cầu cho một cộng tác viên: rõ vai trò, rõ kết quả bàn giao, rõ ranh giới không được vượt qua.

Các mục mình luôn có trong AGENT.md:

  • Định dạng phản hồi: ngôn ngữ trả lời, độ dài, có cần liệt kê file đã sửa không. Thiếu phần này thì mỗi subagent trả về một kiểu, main agent khó đọc kết quả.
  • Bản đồ hệ thống: nếu subagent cần biết cấu trúc project, đưa lên đầu dạng bảng (service → container → port, module → trách nhiệm). Một cái bảng 10 dòng cứu được rất nhiều lần subagent đi mò.
  • Lệnh thường dùng: các lệnh hay chạy, kèm template sẵn để subagent copy lại thay vì tự nghĩ ra mỗi lần (dễ sai cú pháp).
  • Ranh giới an toàn: viết rõ những gì không được làm. Ví dụ migration-writer của mình có câu “Never delete columns directly — use expand/contract pattern. Never modify migrations already applied in staging or prod.” Thiếu câu này, nó sẽ tự sáng tạo ra cách thay đổi schema có thể làm hỏng dữ liệu thật.
  • Định dạng kết quả trả về: main agent mong đợi nhận gì. “Return: files changed, tests run pass/fail, errors if any” — giúp main agent đọc kết quả nhất quán giữa các lần gọi.

Main agent gọi subagent như thế nào

Việc gọi subagent đi qua công cụ Task. Main agent quyết định ba thứ trước khi khởi tạo: có subagent nào phù hợp với việc này không (so khớp qua description), cần cung cấp bao nhiêu thông tin nền (chỉ đủ để subagent làm việc, không thừa), và mong đợi kết quả dạng gì.

Yêu cầu gửi cho subagent phải đầy đủ, tự chứa vì subagent không thấy được cuộc trò chuyện trước đó của main agent. Một ví dụ yêu cầu tốt:

subagent_type: docker-ops
description: Run identity tests
prompt: |
  Chạy pytest cho service identity và báo cáo:
  - Tổng số test pass/fail/skip
  - Các failure mới (so với lần chạy gần nhất)
  - Thời gian chạy
  Trả lời ngắn dưới 150 từ.

Yêu cầu tệ là “run the tests” — subagent chạy xong không biết báo cáo kiểu gì, main agent nhận về một đống log thô. Yêu cầu tốt thì ngữ cảnh đủ, phạm vi rõ, định dạng kết quả chỉ định sẵn. Cách làm giống hệt như giao việc cho nhân viên thực tập — càng mơ hồ thì càng phải sửa nhiều lần.

Ví dụ thực tế: từ user request đến subagent chạy

Để hình dung rõ hơn, đây là một flow hoàn chỉnh từ lúc user nhập yêu cầu đến lúc subagent trả kết quả về.

Bước 1 — User nói với main agent:

“Chạy test cho service identity và catalog xem có gì fail không, song song để nhanh.”

Bước 2 — Main agent đọc description của các subagent có sẵn, thấy docker-ops khớp (“runs pytest, alembic migrations… via docker compose exec”), quyết định spawn 2 subagent song song trong cùng một message:

// Main agent gọi Task tool 2 lần trong 1 message để chạy parallel
Task({
  subagent_type: "docker-ops",
  description: "Run identity tests",
  prompt: `Chạy pytest cho service identity. Báo cáo:
- Tổng pass/fail/skip
- Tên test fail (nếu có) + 1 dòng error
- Thời gian chạy
Trả lời ngắn dưới 150 từ.`
})

Task({
  subagent_type: "docker-ops",
  description: "Run catalog tests",
  prompt: `Chạy pytest cho service catalog. Báo cáo:
- Tổng pass/fail/skip
- Tên test fail (nếu có) + 1 dòng error
- Thời gian chạy
Trả lời ngắn dưới 150 từ.`
})

Bước 3 — Mỗi subagent docker-ops chạy độc lập trong context riêng: đọc AGENT.md để biết mapping service → container, chạy docker compose exec identity pytest -x -v, đọc output, tóm tắt theo định dạng yêu cầu.

Bước 4 — Main agent nhận về 2 bản tóm tắt (mỗi cái khoảng 100-150 từ), gộp lại trả cho user. Toàn bộ log thô của pytest không vào context của main agent — chỉ tóm tắt vào.

Điểm quan trọng để ý: main agent chỉ tốn ~300 từ context cho cả 2 service, thay vì hàng nghìn dòng log nếu tự chạy. Đây mới là lý do thực sự subagent có giá trị — không phải vì “chia việc”, mà vì bảo vệ context của main agent để session dài không bị nhiễu.

Danh sách subagent đáng thử đầu tiên

Project mình có 12 subagent, nhưng không phải cái nào cũng đáng làm ngay từ đầu. Đây là nhóm mình khuyến nghị thử đầu tiên cho bất kỳ codebase vừa đến lớn nào:

Tên subagentModelChức năng chính
docker-opsHaikuChạy test, lint, migration bên trong Docker container
codebase-explorerSonnetTìm file, tra cứu từ khóa, tổng hợp cấu trúc code
migration-writerSonnetTạo migration Alembic/Prisma an toàn (theo expand/contract)
test-writerSonnetViết unit/integration test theo pattern có sẵn
security-auditorOpusĐọc code tìm lỗ hổng (SQL injection, lỗ hổng xác thực, secret bị lộ)
log-analyzerHaikuĐọc log Docker, grep lỗi, lần theo một request qua các service
schema-syncSonnetKiểm tra schema Pydantic, ORM, migration, type TypeScript khớp nhau

Cách đặt tên mình dùng là <miền>-<vai trò> cho dễ nhớ: docker-ops (operations — “vận hành” — trên Docker), migration-writer (chuyên viết migration), security-auditor (chuyên audit bảo mật). Đọc tên là biết ngay làm gì, không cần mở file ra đọc.

Làm 7 cái này trước, chạy vài tuần, cái nào không được gọi thường xuyên thì xóa đừng giữ. Subagent thừa không vô hại — nó làm main agent chọn kém chính xác hơn, vì có quá nhiều “ứng viên” cho cùng một việc, và đôi khi chọn nhầm cái không phù hợp.

Những bẫy mình từng dính

Quên đưa đường dẫn file trong yêu cầu. Main agent biết file cần xem nhưng yêu cầu chỉ viết “check auth code”. Subagent phải tự đi tìm, tốn vài vòng lặp chỉ để tìm ra file main agent đã biết. Luôn đưa đường dẫn cụ thể khi đã có sẵn.

Description trùng lặp. Hai subagent có mô tả na ná nhau thì main agent chọn nhầm thường xuyên. Mỗi subagent nên phụ trách một phần không giao nhau; nếu thấy trùng thì gộp lại hoặc viết lại description cho rạch ròi.

Yêu cầu chỉ có “làm gì”, thiếu “để làm gì”. Yêu cầu kiểu “check file X” — subagent báo cáo chính xác về file X nhưng kết quả thiếu bối cảnh. Thêm mục đích vào: “Check file X vì đang debug lỗi đăng nhập, cần biết có lỗ hổng phân quyền không”. Subagent hiểu mục đích thì tự biết đào sâu chỗ cần thiết.

Tin tóm tắt của subagent mà không xác minh. Bản tóm tắt mô tả cái subagent định làm, không phải cái nó đã làm xong. Khi subagent có quyền Write/Edit, main agent luôn phải kiểm tra diff thực tế trước khi báo cáo hoàn thành cho user — mình đã ăn quả đắng này vài lần.

Không tận dụng chạy song song. Khi có nhiều subagent làm việc độc lập (ví dụ review 3 file cùng lúc), khởi tạo song song trong cùng một message thay vì lần lượt. Chờ 5 subagent × 30 giây chạy nối đuôi khác hẳn với 30 giây tổng khi chạy song song. Tiết kiệm rất nhiều thời gian cho quy trình lớn.

Lợi ích thực tế đo được

Trước khi có subagent, main agent của mình thường tốn 40-60% ngữ cảnh chỉ để khảo sát codebase mỗi session — vì monorepo có 9 service và main agent phải biết hết cấu trúc mới làm được việc. Sau khi tách codebase-explorer ra chạy độc lập, main agent chỉ nhận bản tóm tắt 200 từ rồi tiếp tục làm việc với ngữ cảnh sạch. Kết quả: agent ít “quên” yêu cầu ban đầu hơn, và chi phí mỗi session giảm khoảng 30% vì phần lớn việc khảo sát đã chuyển sang Haiku thay vì Sonnet.

Subagent không phải “thuốc tiên” trị được mọi vấn đề. Với project nhỏ (dưới 5.000 dòng code), chi phí thiết lập và bảo trì subagent lớn hơn lợi ích thu được — cứ để main agent làm hết cho gọn. Nhưng từ mốc khoảng 30.000 dòng code trở lên, đây là khác biệt giữa một AI “giúp được một chút” và một AI thực sự hữu dụng trên codebase của bạn.

// reactions


cat comments.log


hoatq@dev : ~/blog $