Tôi từng nghĩ thêm agent vào hệ thống là thêm capability. Một agent giải quyết được 70% task. Hai agent thì phải được 90%. Năm agent thì gần như không giới hạn.

Logic đó sai hoàn toàn. Tôi phát hiện ra điều này khi một pipeline 4 agent mất 3 phút để trả lời một câu hỏi mà single agent làm được trong 8 giây. Kết quả giống nhau. Chỉ tốn 12× compute.

Multi-agent không phải phép nhân capability. Đó là một architectural choice với trade-off rõ ràng. Bài này đi qua khi nào nên chọn, ba pattern phổ biến nhất, và pitfall mà gần như ai cũng gặp khi scale lên.

Khi nào single agent là đủ

Trước khi bàn về multi-agent, cần nói thẳng: phần lớn agent use case không cần nhiều agent.

Single agent đủ khi:

  • Task có thể decompose thành steps tuần tự với context chung
  • Tool set không quá lớn (dưới 15-20 tools là rule of thumb)
  • Không có yêu cầu parallelism thật sự (chờ API call A xong mới có thể gọi B)
  • Context window không phải bottleneck

Multi-agent bắt đầu có ý nghĩa khi:

  • Task có sub-problems độc lập nhau hoàn toàn (có thể chạy parallel)
  • Mỗi sub-domain cần expertise khác nhau, prompt/persona khác nhau
  • Một agent cần review, critique, hoặc verify output của agent khác
  • Context window của single agent không đủ chứa toàn bộ state

Nói cách khác: multi-agent giải quyết vấn đề parallelism, specialization, và quality checking. Không phải để “làm cho mạnh hơn” theo nghĩa chung chung.

Đọc thêm: Agent là gì: LLM cộng tools cộng memory cộng loop nếu chưa rõ mental model của single agent trước khi đọc tiếp.

Pattern 1: Supervisor

Mô tả: Một agent điều phối (supervisor) nhận task từ user, phân tích, giao cho sub-agent chuyên môn, tổng hợp kết quả.

Dùng khi: Task có nhiều loại công việc rõ ràng, mỗi loại cần skill khác nhau. Ví dụ: “Viết báo cáo kỹ thuật” cần một agent nghiên cứu data, một agent viết văn, một agent review grammar.

Cấu trúc:

User
  |
  v
Supervisor Agent  <-- quyết định phân công, tổng hợp
  |     |     |
  v     v     v
Sub A  Sub B  Sub C  <-- chuyên môn hóa

Code skeleton (Anthropic SDK):

import anthropic
from typing import Callable

client = anthropic.Anthropic()

def make_agent(system_prompt: str, tools: list) -> Callable:
    """Tạo một agent với persona và tool set riêng."""
    def agent(task: str, context: str = "") -> str:
        messages = [{"role": "user", "content": f"{context}\n\nTask: {task}" if context else task}]
        while True:
            resp = client.messages.create(
                model="claude-sonnet-4-6",
                max_tokens=2048,
                system=system_prompt,
                tools=tools,
                messages=messages,
            )
            messages.append({"role": "assistant", "content": resp.content})
            if resp.stop_reason == "end_turn":
                for block in resp.content:
                    if hasattr(block, "text"):
                        return block.text
            if resp.stop_reason == "tool_use":
                tool_results = []
                for block in resp.content:
                    if block.type == "tool_use":
                        result = execute_tool(block.name, block.input)
                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": str(result),
                        })
                messages.append({"role": "user", "content": tool_results})
    return agent

# Định nghĩa sub-agents
researcher = make_agent(
    system_prompt="You are a research specialist. Find and summarize factual information.",
    tools=SEARCH_TOOLS,
)
writer = make_agent(
    system_prompt="You are a technical writer. Transform research notes into clear prose.",
    tools=[],
)
reviewer = make_agent(
    system_prompt="You are a quality reviewer. Check accuracy, clarity, and completeness.",
    tools=[],
)

# Supervisor điều phối
SUPERVISOR_TOOLS = [
    {
        "name": "delegate",
        "description": "Delegate a sub-task to a specialist agent",
        "input_schema": {
            "type": "object",
            "properties": {
                "agent": {"type": "string", "enum": ["researcher", "writer", "reviewer"]},
                "task": {"type": "string"},
                "context": {"type": "string", "description": "Relevant context from previous agents"},
            },
            "required": ["agent", "task"],
        },
    }
]

def run_supervisor(user_request: str) -> str:
    messages = [{"role": "user", "content": user_request}]
    results: dict[str, str] = {}

    while True:
        resp = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=2048,
            system="You are a supervisor. Break tasks down and delegate to specialists. Synthesize results into a final answer.",
            tools=SUPERVISOR_TOOLS,
            messages=messages,
        )
        messages.append({"role": "assistant", "content": resp.content})

        if resp.stop_reason == "end_turn":
            for block in resp.content:
                if hasattr(block, "text"):
                    return block.text

        if resp.stop_reason == "tool_use":
            tool_results = []
            for block in resp.content:
                if block.type == "tool_use" and block.name == "delegate":
                    agent_name = block.input["agent"]
                    sub_task = block.input["task"]
                    context = block.input.get("context", "")
                    agent_fn = {"researcher": researcher, "writer": writer, "reviewer": reviewer}[agent_name]
                    result = agent_fn(sub_task, context)
                    results[agent_name] = result
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result,
                    })
            messages.append({"role": "user", "content": tool_results})

Trade-off: Supervisor phải gọi N sub-agents tuần tự (mặc định). Mỗi sub-agent là một LLM call. Tổng latency = sum of all calls. Nếu sub-agents không depend nhau, chạy parallel (thread pool hoặc asyncio) giảm được latency đáng kể, nhưng supervisor cần xử lý concurrent results phức tạp hơn.

Pattern 2: Handoff

Mô tả: Agents truyền ownership của task theo dạng danh sách. Agent A xử lý, truyền kết quả cho Agent B, B xử lý tiếp, truyền cho C. Không có supervisor. Không ai nhìn toàn cảnh.

Dùng khi: Pipeline có thứ tự cố định, output của bước trước là input của bước sau. Không cần điều phối động.

Cấu trúc:

User
  |
  v
Agent A  -->  Agent B  -->  Agent C  --> Output
(draft)       (refine)       (format)

Code skeleton:

from dataclasses import dataclass
from typing import Optional

@dataclass
class HandoffContext:
    task: str
    history: list[dict]  # accumulated context qua các agent
    current_output: Optional[str] = None

def run_handoff_pipeline(initial_task: str, pipeline: list[dict]) -> str:
    """
    pipeline: list of {"name": str, "system": str, "tools": list}
    """
    ctx = HandoffContext(task=initial_task, history=[])

    for stage in pipeline:
        # Mỗi stage nhận context đầy đủ từ các stage trước
        input_content = initial_task
        if ctx.current_output:
            input_content = (
                f"Original task: {initial_task}\n\n"
                f"Previous stage output:\n{ctx.current_output}\n\n"
                f"Your task: {stage.get('instruction', 'Continue processing')}"
            )

        messages = [{"role": "user", "content": input_content}]

        while True:
            resp = client.messages.create(
                model="claude-sonnet-4-6",
                max_tokens=2048,
                system=stage["system"],
                tools=stage.get("tools", []),
                messages=messages,
            )
            messages.append({"role": "assistant", "content": resp.content})

            if resp.stop_reason == "end_turn":
                for block in resp.content:
                    if hasattr(block, "text"):
                        ctx.current_output = block.text
                break

            if resp.stop_reason == "tool_use":
                tool_results = []
                for block in resp.content:
                    if block.type == "tool_use":
                        result = execute_tool(block.name, block.input)
                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": str(result),
                        })
                messages.append({"role": "user", "content": tool_results})

        ctx.history.append({"stage": stage["name"], "output": ctx.current_output})

    return ctx.current_output

# Ví dụ pipeline
PIPELINE = [
    {
        "name": "drafter",
        "system": "You write first drafts. Focus on coverage, not polish.",
        "instruction": "Write a first draft.",
        "tools": [],
    },
    {
        "name": "editor",
        "system": "You edit text. Improve clarity and remove redundancy. Do not add new information.",
        "instruction": "Edit the draft for clarity.",
        "tools": [],
    },
    {
        "name": "formatter",
        "system": "You format text into clean Markdown with appropriate headers.",
        "instruction": "Format the edited text.",
        "tools": [],
    },
]

result = run_handoff_pipeline("Explain Python's GIL in 300 words", PIPELINE)

Trade-off: Handoff đơn giản nhất để implement. Không có shared state phức tạp, không cần supervisor logic. Nhưng không linh hoạt: nếu bước 3 phát hiện bước 1 sai, không có cơ chế quay lại. Phải thêm “retry stage” vào pipeline nếu cần.

Pattern 3: Debate

Mô tả: Hai hoặc nhiều agent đưa ra góc nhìn/giải pháp trái chiều. Một judge agent (hoặc human) đánh giá và chọn hoặc tổng hợp.

Dùng khi: Task cần đánh giá nhiều chiều, không có đáp án duy nhất, hoặc cần giảm hallucination bằng cách để agents kiểm tra lẫn nhau. Phổ biến trong code review, risk assessment, strategic decisions.

Cấu trúc:

         User
           |
     ------+------
     |           |
     v           v
  Agent A      Agent B
 (proposes)   (critiques)
     |           |
     v           v
     +-----+-----+
           |
           v
       Judge Agent
           |
           v
        Output

Code skeleton:

def run_debate(topic: str, rounds: int = 2) -> str:
    proposer = make_agent(
        system_prompt=(
            "You are a proposer. Given a topic, provide a well-reasoned position or solution. "
            "Be specific and concrete."
        ),
        tools=[],
    )
    critic = make_agent(
        system_prompt=(
            "You are a critic. Given a proposal, identify weaknesses, edge cases, and counterarguments. "
            "Be direct and specific, not just contrarian."
        ),
        tools=[],
    )
    judge = make_agent(
        system_prompt=(
            "You are a judge. Review the debate and produce a final synthesis that incorporates "
            "valid points from both sides. Output a balanced, actionable conclusion."
        ),
        tools=[],
    )

    debate_transcript = []
    current_proposal = proposer(topic)
    debate_transcript.append(f"[Proposer - Round 1]\n{current_proposal}")

    for round_num in range(1, rounds + 1):
        critique = critic(
            f"Critique this proposal:\n\n{current_proposal}",
            context=f"Topic: {topic}"
        )
        debate_transcript.append(f"[Critic - Round {round_num}]\n{critique}")

        if round_num < rounds:
            # Proposer refines based on critique
            current_proposal = proposer(
                f"Refine your proposal based on this critique:\n\n{critique}",
                context=f"Topic: {topic}\n\nOriginal proposal: {current_proposal}"
            )
            debate_transcript.append(f"[Proposer - Round {round_num + 1}]\n{current_proposal}")

    full_transcript = "\n\n".join(debate_transcript)
    verdict = judge(
        f"Judge this debate and provide a final synthesis:\n\n{full_transcript}",
        context=f"Topic: {topic}"
    )

    return verdict

result = run_debate("Should we use async or sync Python for this agent runtime?")

Trade-off: Debate là pattern đắt nhất về token. Hai rounds với proposer + critic + judge = tối thiểu 5 LLM calls, nhiều hơn nếu tools được dùng trong mỗi call. Khi nào đáng chi? Khi cost của quyết định sai lớn hơn cost compute. Code architecture decisions, security assessments, medical triage là những ví dụ hợp lý. Blog post thì không cần.

Compute cost là N times single agent

Đây là điểm quan trọng nhất của bài, cần nói thẳng.

Multi-agent không giảm cost. Nó nhân cost.

Với supervisor pattern có 3 sub-agents:

  • Supervisor call: ~2000 tokens
  • Sub-agent A: ~3000 tokens
  • Sub-agent B: ~3000 tokens
  • Sub-agent C: ~3000 tokens
  • Supervisor final synthesis: ~2000 tokens
  • Tổng: ~13,000 tokens

Một single agent làm cùng task: ~4,000-6,000 tokens.

Supervisor tốn 2-3× token. Đó là chi phí bạn trả cho specialization và structure.

Debate pattern còn tệ hơn: N agents × rounds × judge. Dễ dàng đạt 10× single agent cost.

Rule of thumb: chỉ chấp nhận multi-agent cost khi:

  1. Task thật sự không thể làm tốt với single agent (specialization không giả được)
  2. Parallelism thật giảm latency wall-clock dù token cost cao hơn
  3. Quality improvement đo được, không chỉ cảm giác

Pitfall: shared memory race condition

Đây là bug khó debug nhất trong multi-agent system.

Tình huống: supervisor chạy hai sub-agents song song, cả hai được quyền write vào shared state (ví dụ: một file, một database record, một dict trong memory). Sub-agent A đọc state lúc T=0, Sub-agent B đọc state lúc T=0. A write lúc T=1. B write lúc T=1.5. B’s write ghi đè lên A mà không biết A đã thay đổi gì.

Kết quả: state cuối cùng chỉ phản ánh B, hoàn toàn bỏ qua A.

Tôi gặp điều này khi hai agent cùng append vào một document. Document cuối chỉ có nội dung của agent chạy sau. Agent chạy trước bị xóa hoàn toàn. Mất 40 phút để debug vì không có error, chỉ có missing content.

Fix theo thứ tự khuyến nghị:

  1. Tránh shared write state giữa parallel agents. Mỗi agent write vào namespace riêng của nó. Supervisor thu gom và merge.

  2. Nếu phải share: dùng file locking hoặc queue. Python threading.Lock() cho in-process, filesystem lock (fcntl) cho multi-process, Redis list cho distributed.

  3. Message passing thay vì shared memory. Mỗi agent return result, không mutate global state. Bài 17 về agent communication patterns sẽ đi sâu hơn.

  4. Idempotent writes. Thiết kế write operations để chạy nhiều lần không gây hại. Append với ID unique, upsert thay vì insert.

import threading
from typing import Any

class SafeSharedState:
    def __init__(self):
        self._state: dict[str, Any] = {}
        self._lock = threading.Lock()

    def write(self, agent_id: str, key: str, value: Any) -> None:
        with self._lock:
            # Namespace by agent to avoid collision
            self._state[f"{agent_id}:{key}"] = value

    def read(self, agent_id: str, key: str) -> Any:
        with self._lock:
            return self._state.get(f"{agent_id}:{key}")

    def read_all(self) -> dict[str, Any]:
        with self._lock:
            return dict(self._state)

shared_state = SafeSharedState()

Pattern đơn giản nhưng hiệu quả: mỗi agent write vào namespace của mình, supervisor đọc all và merge. Không có ghi đè, không có race.

Cheatsheet: 3 patterns

PatternTopologyKhi nào dùngCost vs singlePitfall chính
Supervisor1 boss, N workersTask cần multiple expertise, dynamic routing2-4×Supervisor bottleneck, sequential by default
HandoffPipeline tuyến tínhStages cố định, output A là input BN× (N = số stages)Không có cơ chế quay lại khi stage sau phát hiện lỗi
DebateN peers + judgeQuyết định quan trọng, cần multi-perspective5-10×Rất đắt, judge có thể bị biased bởi position được trình bày sau
Câu hỏiCâu trả lời
Single agent có làm được không?Thử trước. Multi-agent là last resort, không phải default
Có parallel opportunity thật không?Chỉ add parallel khi sub-tasks thực sự độc lập
Shared state cần không?Nếu có thể tránh, tránh. Nếu phải có, dùng lock
Cost có justify không?Đo quality gain thật sự, đừng đoán

Khi nào thật sự cần multi-agent

Sau tất cả những cảnh báo trên, đây là những use case tôi thấy multi-agent thật sự đáng:

Code review pipeline. Một agent viết code, một agent review security, một agent review performance, một agent check style. Ba reviewer chạy parallel, supervisor tổng hợp. Parallelism thật, specialization thật.

Research synthesis. Nhiều agent tìm kiếm các nguồn khác nhau song song. Supervisor merge và dedup. Nếu sources độc lập nhau, đây là use case rõ ràng nhất cho parallelism.

Adversarial testing. Một agent viết prompt, một agent cố bypass safety của prompt đó (red-team). Pattern debate kiểu này thật sự phát hiện được lỗ hổng mà single agent thường bỏ qua.

Long-context decomposition. Document quá lớn cho context window. Chia thành chunks, mỗi chunk một agent xử lý song song, supervisor tổng hợp. Đây là trường hợp multi-agent không phải vì specialization mà vì technical limitation.

Tất cả các trường hợp trên có một điểm chung: có lý do kỹ thuật cụ thể (parallelism, context size, genuine specialization), không phải “thêm agent cho nó thông minh hơn”.

Lời kết

Multi-agent pattern là một tool trong toolbox, không phải kiến trúc mặc định. Single agent với prompt tốt giải được nhiều vấn đề mà người ta tưởng cần multi-agent.

Khi dùng multi-agent: chọn pattern phù hợp với topology thật của task (supervisor khi cần điều phối động, handoff khi pipeline cố định, debate khi cần multi-perspective), tính cost trước khi implement, và thiết kế để tránh shared mutable state ngay từ đầu.

Bài tiếp theo, Communication: shared state so với message passing, đi sâu vào cách các agents truyền thông tin cho nhau một cách an toàn, bao gồm so sánh shared memory, message queue, và event-driven patterns. Đó là phần infrastructure mà mọi multi-agent system cần quyết định trước khi viết dòng code đầu tiên.

Khi bạn đã sẵn sàng nhìn các framework làm điều này thế nào, bài 19 so sánh LangGraph, CrewAI, AutoGen: mỗi framework chọn pattern nào làm default, và khi nào sự lựa chọn đó trở thành constraint.