AI-Native Solutions Architect

Multi-Agent Systems: Thiết kế đội ngũ AI chuyên biệt (Coder, Reviewer, Tester)

7/19/2026 · 15p đọc


title: "Multi-Agent Systems: Thiết kế đội ngũ AI chuyên biệt (Coder, Reviewer, Tester)"
series: "AI-Native Solutions Architect: Từ Coder đến Kiến trúc sư AI"
season: "Season 3 — AI Agents & Platform Engineering"
order: 24
audience: "Software Engineer hướng tới Solutions Architect"
reading_time: "14 phút"
tags: ["multi-agent systems", "agent orchestration", "coder agent", "reviewer agent", "tester agent", "self-critique", "ai agents", "solutions architect", "ai-native"]

Multi-Agent Systems: Thiết kế đội ngũ AI chuyên biệt (Coder, Reviewer, Tester)

Bạn giao cho một agent duy nhất: "Viết feature X, review lại cho kỹ, rồi viết test đầy đủ." Agent trả về code, một đoạn "tôi đã review và không thấy vấn đề gì", và vài test case pass ngay từ lần chạy đầu tiên. Nhìn thì đẹp. Nhưng bạn có tin không? Đó chính xác là câu hỏi mà bất kỳ kỹ sư có kinh nghiệm nào cũng nên đặt ra — vì bạn đang chứng kiến một người vừa viết code, vừa tự chấm điểm bài làm của chính mình, vừa tự ra đề thi cho bản thân.

Vấn đề không nằm ở năng lực của model. GPT-4, Claude, hay bất kỳ LLM nào đủ mạnh đều có thể viết code đúng, review sâu, và viết test hợp lý — nếu được hỏi riêng lẻ. Vấn đề nằm ở context và động lực (dù là động lực "giả lập" của một agent). Khi cùng một agent, trong cùng một phiên làm việc, vừa viết ra giải pháp vừa được yêu cầu phản biện chính giải pháp đó, nó thừa hưởng toàn bộ giả định, thiên kiến, và điểm mù đã có sẵn trong lúc viết code. Nó không "quên" được lý do tại sao mình chọn cách làm này để nhìn lại bằng con mắt khác. Đây là hiện tượng quen thuộc trong ngành: code review có giá trị chính vì người review không phải người viết.

Multi-agent system giải quyết vấn đề này không phải bằng cách làm agent "thông minh hơn", mà bằng cách tổ chức lại quy trình: tách vai trò, tách context, tách trách nhiệm — giống hệt cách một team kỹ sư con người vận hành. Bài này đi vào thiết kế cụ thể của một hệ 3 agent (Coder — Reviewer — Tester), cách điều phối chúng, và những cạm bẫy khi số lượng agent giao tiếp qua lại vượt quá lợi ích mang lại.

Vấn đề

Hãy hình dung một pipeline AI-assisted development đơn-agent điển hình:

User prompt → Single Agent → [viết code + tự review + viết test] → Output

Về mặt kỹ thuật, đây là một vòng lặp trong đó agent giữ nguyên context window, giữ nguyên system prompt, và giữ nguyên "trạng thái suy luận" từ bước viết code sang bước review. Ba hệ quả trực tiếp:

1. Thiên kiến xác nhận (confirmation bias) mang tính cấu trúc. Agent đã đầu tư "công sức suy luận" (compute, token, context) vào một giải pháp cụ thể. Khi được yêu cầu review chính giải pháp đó trong cùng phiên, nó có xu hướng tìm lý do để xác nhận giải pháp đúng hơn là tìm lý do để bác bỏ nó — đúng như thiên kiến xác nhận ở con người, chỉ khác là ở đây nó xảy ra vì cùng một context đang được tái sử dụng chứ không có "khoảng cách nhận thức" nào giữa hai vai trò.

2. Test được viết để pass, không phải để phát hiện lỗi. Khi cùng một agent vừa viết implementation vừa viết test, nó có xu hướng viết test khớp với chính hành vi nó đã implement — kể cả khi hành vi đó sai so với yêu cầu ban đầu. Đây là lỗi kinh điển: test "xanh" không có nghĩa là code đúng, chỉ có nghĩa là code nhất quán với chính nó.

3. Không có "second opinion" thật sự. Trong review của con người, giá trị lớn nhất đến từ một bộ não khác nhìn vấn đề từ góc độ khác — thiếu domain knowledge chi tiết về cách giải pháp được tạo ra, nhưng bù lại không bị "mắc kẹt" trong logic đã chọn. Một agent tự review thiếu hoàn toàn tính chất này.

Về bản chất, đây là bài toán tổ chức (organizational design), không phải bài toán model capability. Và giải pháp kiến trúc cho bài toán tổ chức luôn là: tách vai trò, giới hạn phạm vi trách nhiệm, và thiết lập cơ chế kiểm tra chéo độc lập.

Kỹ thuật cốt lõi

Nguyên tắc thiết kế: Separation of Concerns ở tầng agent

Multi-agent system áp dụng một nguyên tắc kiến trúc quen thuộc — separation of concerns — nhưng ở tầng "vai trò nhận thức" thay vì tầng module code. Mỗi agent:

  • system prompt riêng định nghĩa vai trò, phạm vi trách nhiệm, và ranh giới không được vượt qua.
  • context riêng (hoặc ít nhất là context được kiểm soát input/output rõ ràng) — không tự động thừa hưởng toàn bộ lịch sử suy luận của agent khác.
  • tiêu chí thành công riêng — Coder được đánh giá bằng "code chạy đúng yêu cầu", Reviewer được đánh giá bằng "tìm ra được bao nhiêu vấn đề thật", Tester được đánh giá bằng "test có phát hiện được lỗi hay không", không phải bằng việc làm hài lòng agent khác.

Điểm mấu chốt: Reviewer agent không nhìn thấy quá trình suy luận của Coder, chỉ nhìn thấy code output cuối cùng cộng với yêu cầu gốc. Điều này mô phỏng chính xác giá trị của code review giữa người: reviewer đánh giá kết quả dựa trên yêu cầu, không bị dẫn dắt bởi lý do "tại sao tôi làm thế này" của người viết.

Sơ đồ luồng phối hợp 3 agent

flowchart TD
    A[User / Task Spec] --> B[Coder Agent]
    B -->|code diff + giải thích ngắn| C[Reviewer Agent]
    C -->|PASS| D[Tester Agent]
    C -->|REJECT + danh sách vấn đề| B
    D -->|test PASS + coverage đạt| E[Hoàn tất - merge/deploy]
    D -->|test FAIL| B
    B -.->|đếm số vòng lặp| F{Vượt max_iterations?}
    C -.->|đếm số vòng lặp| F
    F -->|Có| G[Escalate: chuyển cho con người quyết định]

Ba điểm quan trọng trong sơ đồ này mà nhiều team bỏ sót khi mới triển khai:

  1. Reviewer chỉ nhận code diff + spec gốc, không nhận "lời giải thích của Coder về lý do thiết kế" trừ khi thực sự cần thiết — nếu đưa toàn bộ lý luận của Coder cho Reviewer đọc, Reviewer dễ bị dẫn dắt theo logic của Coder thay vì đánh giá độc lập.
  2. Reviewer PASS trước, Tester mới chạy — thứ tự này có chủ đích: review logic/design/security trước khi tốn compute chạy test, giống pipeline CI thật (lint/review nhanh trước, test tốn thời gian sau).
  3. Test FAIL quay lại Coder, không quay lại Reviewer — vì lỗi lộ ra ở bước test là lỗi implementation, cần Coder sửa trực tiếp; đưa lại cho Reviewer trước sẽ tạo thêm một vòng lặp không cần thiết.

Orchestration: ai điều phối, điều phối bằng gì?

Có hai mô hình orchestration phổ biến, mỗi mô hình có đánh đổi khác nhau:

Mô hình Cách vận hành Ưu điểm Nhược điểm
Centralized (Orchestrator agent/script) Một agent hoặc script điều phối gọi tuần tự Coder → Reviewer → Tester, quản lý state và vòng lặp Dễ debug, dễ giới hạn số vòng lặp, dễ log audit trail Orchestrator trở thành single point of failure về logic
Decentralized (agent-to-agent handoff) Agent tự quyết định "chuyển việc cho ai tiếp theo" dựa trên kết quả của chính nó Linh hoạt hơn với case phức tạp, ít code điều phối Khó kiểm soát vòng lặp, dễ xảy ra hội thoại lan man giữa các agent

Với hầu hết bài toán engineering thực tế (không phải nghiên cứu agent tự trị), centralized orchestration bằng code thường xuyên là lựa chọn đúng — vì bạn cần kiểm soát được: số vòng lặp tối đa, timeout, chi phí token, và có một điểm duy nhất để log/audit toàn bộ quy trình. Orchestrator không cần là một LLM — nó có thể là một state machine thuần túy (Python/TypeScript) gọi API của từng agent theo thứ tự đã định nghĩa.

Thực hành

1. System prompt cho từng agent — định nghĩa vai trò và ranh giới

Đây là phần quan trọng nhất: ranh giới trách nhiệm phải được viết rõ ràng đến mức agent không thể làm việc ngoài phạm vi của mình, kể cả khi được yêu cầu.

Coder Agent — system prompt:

Bạn là Coder Agent trong một pipeline multi-agent (Coder → Reviewer → Tester).

VAI TRÒ: Viết code implement đúng theo spec được giao. CHỈ viết code, không tự
đánh giá chất lượng, không tự viết test, không tự quyết định code đã "đủ tốt".

INPUT bạn sẽ nhận:
- Spec/yêu cầu tính năng (từ user hoặc từ Reviewer nếu là vòng sửa lỗi)
- [Nếu là vòng sửa lỗi] Danh sách vấn đề cụ thể mà Reviewer hoặc Tester đã chỉ ra

OUTPUT bắt buộc (JSON):
{
  "code_diff": "<diff hoặc full file content>",
  "files_changed": ["path/to/file1.ts", "path/to/file2.ts"],
  "assumptions": ["giả định bạn đã đưa ra khi spec không rõ ràng"],
  "known_limitations": ["giới hạn bạn biết nhưng không xử lý trong phạm vi này"]
}

RÀNG BUỘC:
- KHÔNG viết phần "Tôi đã kiểm tra kỹ và code này không có bug" — đó là việc
  của Reviewer Agent, không phải của bạn.
- KHÔNG viết test case — đó là việc của Tester Agent.
- Nếu spec mơ hồ, liệt kê rõ trong "assumptions", KHÔNG tự suy diễn im lặng.
- Nếu nhận lại feedback từ Reviewer/Tester, sửa ĐÚNG vấn đề được chỉ ra,
  không refactor lan man ngoài phạm vi feedback.
- Tối đa 1 lần sửa cho mỗi vòng phản hồi — không tự ý sửa thêm chỗ khác.

Reviewer Agent — system prompt:

Bạn là Reviewer Agent trong một pipeline multi-agent (Coder → Reviewer → Tester).

VAI TRÒ: Phản biện ĐỘC LẬP đối với code do Coder Agent tạo ra. Bạn KHÔNG được
xem là "trợ lý hoàn thiện" cho Coder — vai trò của bạn là tìm lý do CODE NÀY
KHÔNG NÊN được merge, không phải tìm lý do nó ổn.

INPUT bạn sẽ nhận:
- Spec/yêu cầu tính năng gốc (không phải diễn giải của Coder)
- code_diff từ Coder Agent
- KHÔNG nhận "assumptions" hay lý giải thiết kế của Coder trừ khi bạn chủ động
  hỏi — mặc định bạn chỉ đánh giá code so với spec.

CHECKLIST bắt buộc phải kiểm tra (không được bỏ qua mục nào):
1. Code có implement ĐÚNG và ĐỦ yêu cầu trong spec không? (không thiếu case,
   không thừa scope)
2. Edge case: input rỗng/null, giá trị biên, concurrency, race condition
3. Security: injection, validate input, auth/authorization, secret leak
4. Error handling: exception được catch đúng chỗ, không nuốt lỗi âm thầm
5. Performance: N+1 query, vòng lặp lồng nhau không cần thiết, memory leak
6. Convention: có tuân theo coding convention của dự án không (xem CLAUDE.md
   nếu có)

OUTPUT bắt buộc (JSON):
{
  "verdict": "PASS" | "REJECT",
  "issues": [
    {"severity": "critical|major|minor", "location": "file:line",
     "description": "...", "suggested_fix": "..."}
  ],
  "reasoning": "giải thích ngắn gọn vì sao PASS hoặc REJECT"
}

RÀNG BUỘC:
- KHÔNG tự sửa code. Bạn chỉ mô tả vấn đề, Coder Agent là người sửa.
- KHÔNG PASS chỉ vì "code trông ổn" — phải đối chiếu từng mục trong checklist.
- Nếu có ít nhất 1 issue mức "critical", verdict BẮT BUỘC là REJECT.
- Tối đa liệt kê issue thật sự quan trọng, không liệt kê nitpick văn phong
  để tránh vòng lặp không hội tụ.

Tester Agent — system prompt:

Bạn là Tester Agent trong một pipeline multi-agent (Coder → Reviewer → Tester).
Bạn CHỈ được kích hoạt sau khi Reviewer Agent đã trả về verdict "PASS".

VAI TRÒ: Viết và chạy test để XÁC NHẬN code hoạt động đúng theo spec — không
phải để xác nhận code khớp với cách nó được implement.

INPUT bạn sẽ nhận:
- Spec/yêu cầu tính năng gốc
- code_diff đã được Reviewer PASS
- KHÔNG nhận nội dung review của Reviewer (tránh bị dẫn dắt test theo đúng
  những gì Reviewer đã kiểm tra, bỏ sót góc nhìn khác)

QUY TẮC VIẾT TEST:
- Test phải dựa trên SPEC (behavior mong đợi), KHÔNG dựa trên cách code hiện
  tại được viết. Nếu code làm sai so với spec, test PHẢI fail để lộ ra điều đó.
- Bắt buộc có: happy path, ít nhất 2 edge case, ít nhất 1 negative case (input
  sai/không hợp lệ).
- KHÔNG viết test chỉ để "cho pass" — nếu nghi ngờ hành vi hiện tại sai, viết
  test theo đúng spec và để nó fail, kèm giải thích rõ trong output.

OUTPUT bắt buộc (JSON):
{
  "test_file": "đường dẫn file test",
  "test_code": "...",
  "run_result": "PASS" | "FAIL",
  "failures": [{"test_name": "...", "expected": "...", "actual": "...",
                "likely_cause": "..."}],
  "coverage_note": "phần nào của spec chưa được test, nếu có"
}

2. Orchestration logic — giới hạn vòng lặp bằng code, không phải bằng lời hứa của agent

Đây là phần bắt buộc phải làm bằng code xác định (deterministic), không giao phó cho agent tự "biết dừng đúng lúc":

// orchestrator.ts — điều phối Coder -> Reviewer -> Tester với giới hạn vòng lặp
interface AgentResult {
  role: 'coder' | 'reviewer' | 'tester';
  payload: any;
}

const MAX_CODER_REVIEWER_ITERATIONS = 3; // giới hạn vòng Coder <-> Reviewer
const MAX_CODER_TESTER_ITERATIONS = 2;   // giới hạn vòng Coder <-> Tester

async function runMultiAgentPipeline(spec: string) {
  let coderOutput = await callCoderAgent({ spec });
  let reviewIteration = 0;

  // Vòng lặp Coder <-> Reviewer, có trần cứng
  while (reviewIteration < MAX_CODER_REVIEWER_ITERATIONS) {
    const review = await callReviewerAgent({ spec, code: coderOutput.code_diff });

    if (review.verdict === 'PASS') break;

    reviewIteration++;
    if (reviewIteration >= MAX_CODER_REVIEWER_ITERATIONS) {
      return escalateToHuman({
        reason: 'review_not_converged',
        spec, lastCode: coderOutput, lastReview: review,
      });
    }

    coderOutput = await callCoderAgent({
      spec,
      feedback: review.issues, // chỉ đưa issues, không đưa toàn bộ context cũ
    });
  }

  // Sau khi Reviewer PASS, chuyển Tester
  let testIteration = 0;
  while (testIteration < MAX_CODER_TESTER_ITERATIONS) {
    const testResult = await callTesterAgent({ spec, code: coderOutput.code_diff });

    if (testResult.run_result === 'PASS') {
      return { status: 'DONE', code: coderOutput, test: testResult };
    }

    testIteration++;
    if (testIteration >= MAX_CODER_TESTER_ITERATIONS) {
      return escalateToHuman({
        reason: 'test_not_converged',
        spec, lastCode: coderOutput, lastTest: testResult,
      });
    }

    coderOutput = await callCoderAgent({
      spec,
      testFailures: testResult.failures,
    });

    // Sau khi Coder sửa vì test fail, PHẢI quay lại Reviewer trước khi test lại
    // (tránh bug mới lọt qua review) — gọi đệ quy hoặc quay lại vòng review ở trên
    const reReview = await callReviewerAgent({ spec, code: coderOutput.code_diff });
    if (reReview.verdict !== 'PASS') {
      return escalateToHuman({
        reason: 'fix_introduced_new_review_issue',
        spec, lastCode: coderOutput, lastReview: reReview,
      });
    }
  }
}

function escalateToHuman(context: Record<string, unknown>) {
  // Log đầy đủ context, gửi notification, dừng pipeline tự động
  console.warn('[ESCALATION]', context.reason);
  return { status: 'NEEDS_HUMAN_REVIEW', ...context };
}

Ba điểm thiết kế đáng chú ý trong đoạn code trên:

  • Trần vòng lặp là số cứng, kiểm tra ở tầng orchestrator, không phải instruction "đừng lặp quá 3 lần" trong prompt — vì agent có thể không tuân thủ giới hạn mềm, nhưng code while với counter thì không có ngoại lệ.
  • Escalate to human là một trạng thái kết thúc hợp lệ, không phải lỗi hệ thống — đây chính là cách xử lý "vòng lặp bất đồng không hội tụ" được nêu trong phần cạm bẫy.
  • Khi Coder sửa lỗi vì Tester fail, phải quay lại Reviewer một lần nữa trước khi test lại — vì fix cho một bug có thể vô tình tạo ra vấn đề mới mà chỉ Reviewer mới soi ra (ví dụ sửa logic nhưng quên xử lý lại edge case đã PASS trước đó).

3. Bảng phân định trách nhiệm — dùng làm checklist thiết kế

Khía cạnh Coder Agent Reviewer Agent Tester Agent
Input chính Spec + feedback (nếu có) Spec + code diff (không nhận lý luận của Coder) Spec + code đã PASS review (không nhận nội dung review)
Được quyền quyết định Cách implement PASS/REJECT, mức độ nghiêm trọng của issue PASS/FAIL dựa trên kết quả chạy test thật
Không được làm Tự đánh giá chất lượng, tự viết test Tự sửa code Viết test khớp theo code thay vì theo spec
Tiêu chí thành công Review PASS + Test PASS Tìm ra vấn đề thật (không nitpick vô ích) Test có khả năng phát hiện lỗi thật (không chỉ để xanh)

Cạm bẫy thường gặp

1. Chi phí token và độ trễ tăng phi tuyến so với lợi ích. Mỗi vòng Coder ↔ Reviewer ↔ Tester đều tốn một lượt gọi LLM đầy đủ context (spec + code + lịch sử feedback). Với task đơn giản (sửa một dòng config, đổi tên biến), chạy full pipeline 3 agent có thể tốn gấp 5-10 lần token và thời gian so với để một agent làm thẳng. Quy tắc thực dụng: chỉ áp dụng multi-agent review đầy đủ cho thay đổi có rủi ro thật (business logic, security-sensitive, thay đổi schema/API contract); với thay đổi nhỏ, một agent + lint/test tự động là đủ. Đừng dùng multi-agent như búa tạ cho mọi việc.

2. Vòng lặp bất đồng không hội tụ (non-converging disagreement loop). Reviewer liên tục REJECT với lý do tinh vi hơn mỗi lần, Coder liên tục "sửa" nhưng không đúng trọng tâm vấn đề, hoặc tệ hơn — Reviewer và Tester đưa ra yêu cầu mâu thuẫn nhau (Reviewer muốn thêm validation chặt, Tester viết test kỳ vọng behavior lỏng hơn). Nếu không có trần vòng lặp cứng (như MAX_CODER_REVIEWER_ITERATIONS ở trên) và cơ chế escalate rõ ràng, hệ thống có thể chạy vô hạn, đốt token, và không bao giờ ra kết quả. Luôn thiết kế "lối thoát" — escalate cho con người — như một trạng thái kết thúc hợp lệ, không phải một trường hợp ngoại lệ hiếm gặp cần xử lý sau.

3. Rò rỉ context giữa các agent làm mất tính độc lập của review. Nếu vô tình để Reviewer Agent nhìn thấy toàn bộ "chain of thought" hoặc lời giải thích chi tiết của Coder Agent (thay vì chỉ code diff + spec), Reviewer sẽ bị dẫn dắt theo logic của Coder và mất đi giá trị cốt lõi của việc tách vai trò — quay lại đúng vấn đề ban đầu mà kiến trúc này được sinh ra để giải quyết. Kiểm soát chặt input/output schema giữa các agent (như JSON contract trong phần Thực hành) là cách đảm bảo ranh giới này không bị xói mòn theo thời gian khi team thêm field "cho tiện" vào payload.

🧭 Góc nhìn Solutions Architect
Nếu phải trả lời "tại sao cần 3 agent thay vì 1 agent giỏi hơn", câu trả lời của bạn có dừng ở "để tăng độ chính xác" hay đi xa hơn tới "để tái tạo tính độc lập nhận thức mà review có giá trị nhờ vào"? Bạn đã đo được ngưỡng chi phí (token, latency) mà tại đó multi-agent review không còn đáng làm so với một agent + test suite tự động chưa? Và quan trọng nhất: hệ thống của bạn có một đường thoát rõ ràng khi các agent bất đồng mãi không hội tụ, hay nó sẽ lặp tới khi hết ngân sách token?

🔗 Bài viết liên quan

  • [Bài 23] Tool Use & Function Calling — nền tảng để mỗi agent trong hệ thống này gọi được tool thật (chạy test, đọc file, gọi linter).
  • [Bài 22] Framework cho Agents — các framework (LangChain, CrewAI, AutoGen) cung cấp sẵn primitive để dựng orchestration multi-agent thay vì tự viết state machine từ đầu.

Bài trước: Tool Use & Function Calling · Bài tiếp theo: Deploying AI Agents

Multi-Agent Systems: Thiết kế đội ngũ AI chuyên biệt (Coder, Reviewer, Tester)