Hầu như ngày nào mình cũng chạy một đội subagent nhỏ — một agent tổng hợp digest, một agent viết blog, một agent deploy, cùng vài task research one-off spin up lên, làm việc rồi biến mất. Lần đầu tiên mình thực sự cộng tổng token bill cho một task fan-out “đơn giản” — năm agent tóm tắt năm nguồn, rồi một agent thứ sáu gộp kết quả lại — mình đã không vui chút nào. Phần lớn chi phí đó không phải là reasoning. Đó là năm agent mỗi con đọc lại context mà nó không cần, và bước gộp đọc lại toàn bộ hai lần. Đó chính là “chảy máu token”, và hóa ra nó đến từ đúng hai nguồn, cả hai đều có thể sửa mà không cần đụng vào model.
Hai chỗ rò rỉ
Hệ thống multi-agent lãng phí token theo hai cách cụ thể, cộng dồn với nhau:
- Kích hoạt song song không có cấu trúc — mọi agent trong graph đều bật lên bất kể input của nó đã sẵn sàng hay chưa, khiến các agent đang rảnh vẫn tốn token để poll lại hoặc đọc lại context cũ chỉ để nhận ra là chưa có gì để làm.
- Chia sẻ context không giới hạn — pattern mặc định là “đưa toàn bộ conversation tích lũy cho mọi agent”, trong khi phần lớn agent chỉ cần hai ba thông tin liên quan tới subtask cụ thể của nó.
Cả hai đều không phải là vấn đề của model. Đó là vấn đề orchestration — và orchestration là thứ một tech lead thực sự kiểm soát được, khác với giá model.
Kích hoạt theo phase (phase-scheduled activation)
Một pattern nghiên cứu gần đây mình bắt đầu mượn dùng — phase-scheduled multi-agent systems (PSMAS) — coi việc kích hoạt agent là bài toán lập lịch thay vì “spawn hết rồi để nó tự lo”. Mỗi agent được gán một phase dựa trên đồ thị phụ thuộc của task: agent nào có input chưa sẵn sàng cho tới stage 3 thì không thức dậy và poll trong stage 1, 2 — nó nhận một bản tóm tắt context nén và ở trạng thái idle. Số liệu công bố cho pattern này là giảm trung bình 27.3% token, trong khi hiệu năng task vẫn nằm trong khoảng 2 điểm phần trăm so với baseline kích hoạt toàn bộ — một sự đánh đổi thực sự tốt, vì cái baseline “kích hoạt toàn bộ” đó chính là mặc định lãng phí mà phần lớn chúng ta ship đầu tiên.
Trong thực tế, sự khác biệt trông như sau:
// naive: mọi agent chạy ngay lập tức, tất cả đều đọc chung một context đang phình to
const results = await Promise.all(agents.map(a => a.run(fullContext)))
so với:
// phase-scheduled: agent chỉ thức dậy khi dependency được resolve,
// và chỉ nhận đúng lát context mà phase của nó cần
const results = await pipeline(
stages,
(input, stage) => agent(stage.prompt, { context: stage.contextSlice(input) })
)
Phiên bản thứ hai không hề nhiều code hơn. Nó là cùng một primitive orchestration mà phần lớn framework agent đã có sẵn — pipeline thay vì Promise.all — được dùng có chủ đích thay vì dùng theo mặc định.
Context pruning: cắt ở đâu, khi nào
Chỗ rò rỉ thứ hai — mọi agent kế thừa toàn bộ context — cần một chính sách pruning, không chỉ là một prompt khởi đầu nhỏ hơn. Pattern đã chứng minh hiệu quả trong thực tế: kích hoạt pruning tự động khi context chạm khoảng 80–95% ngân sách, chứ không phải ở một mốc token cố định, vì “ngân sách” thay đổi theo model và theo lượng headroom bước tiếp theo cần. Có hai cơ chế riêng biệt làm hai việc khác nhau ở đây, và rất hay bị nhầm lẫn với nhau:
- Truncation và summarization kiểm soát cái gì rời khỏi context window — gộp lại các lượt cũ khi chúng không còn actionable.
- Pruning kiểm soát cái gì đi vào context ngay từ đầu — lọc output thô của một tool xuống còn đúng những field bước tiếp theo thực sự dùng, trước khi nó được append vào.
Cái thứ hai mới là nơi phần lớn khoản tiết kiệm thực sự nằm ở đó, vì tool output gần như luôn là nguồn đóng góp lớn nhất vào context phình to — một danh sách thư mục, một API response đầy đủ, một trang log — và 90% payload đó không bao giờ được agent nhận nó đọc tới. Một bộ lọc relevance nhẹ giữa “tool chạy xong” và “tool output đi vào context” tự trả giá trị của nó ngay lập tức:
async function toolResult(raw, task) {
const relevant = await scoreRelevance(raw, task) // pass rẻ, dùng model nhỏ hoặc heuristic
return relevant.filter(f => f.score > THRESHOLD).map(f => f.field)
}
Đây là một model nhỏ hơn, thậm chí một pass regex/heuristic, làm nhiệm vụ sàng lọc trước khi model đắt tiền thực sự nhìn thấy payload — cùng bản năng như một cache đặt trước một database chậm, chỉ khác là áp dụng cho context thay vì cho data.
Trên đội agent của mình trông như thế nào
Workflow mình dùng để chạy task digest và blog đã dùng fan-out kiểu pipeline thay vì parallel() kiểu barrier chính vì lý do này: một barrier bắt mọi stage phải chờ agent chậm nhất của mọi stage khác, nghĩa là context của mọi agent vẫn mở và tiếp tục phình to suốt thời gian mọi người khác hoàn thành. Pipeline cho phép từng item chảy qua các stage một cách độc lập — agent A có thể đang tóm tắt trong khi agent B vẫn đang fetch, và không agent nào giữ context mở mà nó không cần. Chỉ dành true barrier cho một hai chỗ thực sự cần context xuyên item (chủ yếu là dedup trước bước gộp cuối) thay vì dùng nó làm mặc định — điều này đáng giá hơn bất kỳ trick prompt-engineering nào mình từng thử.
Checklist
Trước khi scale một pipeline multi-agent, có ba câu hỏi đáng hỏi, theo đúng thứ tự:
- Mọi agent có cần active ngay bây giờ không? Nếu không, hãy phase nó — agent đang idle nên nhận một bản tóm tắt, không phải một bản replay đầy đủ.
- Mọi agent có cần toàn bộ context, hay chỉ một lát? Mặc định dùng một lát; chỉ mở rộng khi agent thực sự fail vì thiếu thông tin.
- Cái barrier đó có thực sự cần thiết không? Nếu một lệnh
parallel()tồn tại chỉ vì đó là pattern đầu tiên bạn nghĩ tới, thay nó bằng pipeline và đo lại sự khác biệt.
Không cái nào trong số này cần một model khác hay một ngân sách lớn hơn. Nó chỉ cần đối xử với context như bất kỳ tài nguyên khan hiếm, dùng chung nào khác — có scheduler, có eviction policy, chứ không phải một mặc định ăn-thoải-mái.