Lần đầu tôi đọc code của một đồng nghiệp dùng multi-agent, tôi thấy họ có ba agent: một cái gọi là “Planner”, một cái “Executor”, một cái “Reviewer”. Tôi hỏi: “Ba cái này chạy model khác nhau à?” Họ trả lời: “Không, cùng Sonnet 4.6 hết. Chỉ khác system prompt.”
Tôi ngạc nhiên. Cùng model, cùng weights, khác gì nhau?
Câu trả lời, sau vài tháng tôi tự làm, là: khác rất nhiều. Cùng một câu hỏi nhưng system prompt thay đổi cách model frame vấn đề, cách nó cân nhắc tradeoff, cách nó sắp xếp output. Chuyên môn hóa bằng prompt engineering, không phải fine-tuning.
Bài này đi vào pattern role-based: tại sao chia role, prompt template từng role, và tradeoff khi quyết định có nên chia hay không.
Trước khi chia role: bài 7 và bài 9
Hai bài trước đã đặt nền cho pattern này.
Bài 7 Plan-and-Execute tách planning ra khỏi execution: thay vì để agent tự suy nghĩ từng bước trong vòng lặp, bạn có một phase lập kế hoạch trước, sau đó execute theo kế hoạch. Bài 18 mở rộng: thay vì một agent đơn đảm nhận cả planning lẫn execution, bạn có hai agent chuyên biệt.
Bài 9 Self-reflection giới thiệu critic pattern: một agent nhìn lại output của chính nó và phê bình. Reviewer role trong bài này là biến thể của critic, nhưng tách ra thành agent độc lập thay vì reflection trong cùng một agent.
Kết hợp hai pattern đó ra: planner lập kế hoạch, executor thực hiện, reviewer kiểm tra. Ba agent, cùng model, mỗi agent chỉ làm một việc.
Tại sao cùng model lại cần role khác nhau
Một LLM như Sonnet 4.6 không có “tính cách” cố định. Mỗi lần call, nó đọc system prompt và hiệu chỉnh hành vi theo đó. System prompt là instruction, là persona, là constraint.
Ví dụ đơn giản: cùng một câu hỏi “Viết code xử lý file CSV”, model sẽ phản ứng khác nhau tùy prompt:
- Không có system prompt: sẽ viết code, giải thích, có thể hỏi lại requirements
- System prompt “You are a meticulous planner. Never write code. Output only a numbered plan with clear dependencies”: sẽ ra danh sách bước, không ra code
- System prompt “You are a code executor. Receive a plan step. Output only working code, no explanation”: sẽ ra code thẳng, không giải thích
- System prompt “You are a skeptical reviewer. Find bugs, gaps, and edge cases. Be harsh”: sẽ tìm lỗi, không bao giờ ca ngợi
Cùng model, hành vi khác hẳn.
Khi bạn có một agent đơn làm tất cả, LLM phải cân bằng nhiều mục tiêu cùng lúc: vừa lên kế hoạch, vừa code, vừa tự phê bình. Kết quả thường là nó làm trung bình cả ba, không xuất sắc cái nào. Chia role buộc mỗi lần call tập trung vào một việc.
Planner role
Planner nhận task, output ra plan. Không code, không thực hiện, không verify. Chỉ lên kế hoạch.
Mục tiêu của system prompt planner: buộc model suy nghĩ về dependencies, prerequisites, edge cases, và order. Ngăn nó nhảy thẳng vào execution.
PLANNER_SYSTEM_PROMPT = """You are a meticulous planning agent.
Your job: receive a task, produce a structured plan. Nothing else.
Rules:
- Output ONLY a JSON plan, no prose explanation
- Each step has: id, description, depends_on (list of step ids), expected_output
- Identify what can run in parallel vs what must be sequential
- Flag any ambiguity as a clarification_needed item, do not assume
- Never write code, never perform actions, never guess at implementation details
Output format:
{
"clarifications_needed": [...],
"steps": [
{
"id": "step_1",
"description": "...",
"depends_on": [],
"expected_output": "..."
}
]
}
"""
Lưu ý clarifications_needed: planner không được assume khi thiếu thông tin. Đây là điểm thường bị bỏ qua. Một planner tốt biết mình không biết gì và hỏi trước khi lập kế hoạch sai.
Executor role
Executor nhận một step từ plan, thực hiện và trả về kết quả. Không bàn về kế hoạch, không nhìn các step khác, không tự ý thêm scope.
Mục tiêu của system prompt executor: precision. Làm đúng những gì được yêu cầu, không hơn. Đặc biệt quan trọng khi executor có tools.
EXECUTOR_SYSTEM_PROMPT = """You are a precise execution agent.
Your job: receive a single plan step, execute it completely. Nothing else.
Rules:
- Focus ONLY on the given step. Do not anticipate or perform other steps.
- If the step requires a tool call, make exactly the needed calls. No extras.
- If you encounter an error, report it as-is. Do not try to fix adjacent issues.
- Output must match the expected_output format specified in the step.
- Never improvise scope expansion. If the step says "create file X", create only file X.
Input format:
{
"step": { "id": "...", "description": "...", "expected_output": "..." },
"context": { ... } # outputs from previous steps
}
Output format:
{
"step_id": "...",
"status": "success" | "error",
"result": ...,
"error": null | "error message"
}
"""
Phần context là quan trọng: executor cần biết output của các step trước (dependencies). Nhưng nó không được thay đổi scope dựa trên context đó. Context chỉ để tham chiếu, không phải để mở rộng task.
Reviewer role
Reviewer nhận kết quả của executor (hoặc toàn bộ task sau khi execute xong), kiểm tra và trả về verdict.
Mục tiêu của system prompt reviewer: skepticism. Không phải tìm cái hay. Tìm cái sai, cái thiếu, cái nguy hiểm. Một reviewer tốt luôn bắt đầu từ giả định “kết quả này có vấn đề, tôi cần chứng minh ngược lại.”
REVIEWER_SYSTEM_PROMPT = """You are a critical reviewer agent.
Your job: examine results and find problems. Be skeptical by default.
Rules:
- Assume the result has issues until proven otherwise.
- Check for: correctness, completeness, edge cases, security issues, performance red flags.
- If reviewing code: look for bugs, missing error handling, resource leaks, injection risks.
- If reviewing a plan: look for missing steps, wrong dependencies, unhandled failures.
- Do NOT praise. Do NOT validate unless you have checked every claim.
- Your approval carries weight: only approve when you have actively searched for problems and found none.
Output format:
{
"verdict": "approved" | "needs_revision" | "blocked",
"verdict_reason": "...",
"issues": [
{
"severity": "critical" | "major" | "minor",
"location": "...",
"description": "...",
"suggestion": "..."
}
]
}
"""
verdict: "blocked" là cho trường hợp kết quả sai nghiêm trọng đến mức không thể fix bằng revision, phải làm lại từ đầu. Phân biệt blocked và needs_revision quan trọng để orchestrator quyết định retry strategy.
Ghép ba role vào orchestration
Ba role chỉ có ý nghĩa khi có orchestrator điều phối. Orchestrator không cần LLM, có thể là Python thuần.
import anthropic
import json
client = anthropic.Anthropic()
def call_agent(system_prompt: str, user_message: str) -> str:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
system=system_prompt,
messages=[{"role": "user", "content": user_message}],
)
return response.content[0].text
def parse_json_response(raw: str) -> dict:
# Extract JSON from possible markdown code fences
raw = raw.strip()
if raw.startswith("```"):
lines = raw.split("\n")
raw = "\n".join(lines[1:-1])
return json.loads(raw)
def run_role_based_pipeline(task: str, context: dict = None) -> dict:
context = context or {}
# Phase 1: Plan
print("[Planner] Generating plan...")
plan_raw = call_agent(
PLANNER_SYSTEM_PROMPT,
f"Task: {task}\nContext: {json.dumps(context)}"
)
plan = parse_json_response(plan_raw)
if plan.get("clarifications_needed"):
return {
"status": "needs_clarification",
"clarifications": plan["clarifications_needed"]
}
# Phase 2: Execute each step in dependency order
results = {}
for step in plan["steps"]:
# Gather outputs from dependencies
step_context = {dep: results[dep] for dep in step["depends_on"] if dep in results}
print(f"[Executor] Running step: {step['id']} - {step['description']}")
exec_raw = call_agent(
EXECUTOR_SYSTEM_PROMPT,
json.dumps({"step": step, "context": step_context})
)
exec_result = parse_json_response(exec_raw)
results[step["id"]] = exec_result
if exec_result["status"] == "error":
print(f"[Executor] Step {step['id']} failed: {exec_result['error']}")
# Continue to reviewer anyway so it can flag the failure
# Phase 3: Review
print("[Reviewer] Reviewing results...")
review_raw = call_agent(
REVIEWER_SYSTEM_PROMPT,
json.dumps({"task": task, "plan": plan, "results": results})
)
review = parse_json_response(review_raw)
return {
"status": review["verdict"],
"plan": plan,
"results": results,
"review": review,
}
# Ví dụ sử dụng
if __name__ == "__main__":
result = run_role_based_pipeline(
task="Create a Python function that reads a CSV file, validates each row has non-empty name and valid email, and returns a list of valid records.",
context={"language": "Python 3", "csv_format": "name,email,age"}
)
print(f"\nVerdict: {result['status']}")
if result["review"]["issues"]:
for issue in result["review"]["issues"]:
print(f" [{issue['severity'].upper()}] {issue['location']}: {issue['description']}")
Lưu ý: orchestrator hoàn toàn là Python, không có LLM nào ở đây. Dependency resolution, retry logic, state management đều do code thuần xử lý. LLM chỉ được call ở ba điểm cụ thể: plan, execute, review.
Pitfall: roles overlap dẫn đến confusion
Đây là lỗi tôi mắc phải tháng đầu dùng pattern này.
Tôi để system prompt executor có đoạn: “If you notice the plan has a missing step, add it.” Ý định là tốt, executor có thể linh hoạt. Kết quả: executor bắt đầu tự ý thêm scope, phá vỡ dependency tracking, reviewer nhận kết quả không khớp với plan ban đầu và không biết so sánh theo tiêu chí nào.
Lỗi sâu hơn là khi roles chồng lấp, trách nhiệm mờ đi. Executor vừa execute vừa plan lại nghĩa là nếu kết quả sai, bạn không biết lỗi ở phase planning hay execution. Debug không có điểm tựa.
Rule of thumb: mỗi role chỉ được làm một loại quyết định. Planner quyết định “làm gì và theo thứ tự nào”. Executor quyết định “làm cụ thể như thế nào”. Reviewer quyết định “kết quả có đúng không”. Không được mix.
Dấu hiệu role đang bị overlap:
- Executor prompt có từ “if you think”, “feel free to”, “use your judgment about scope”
- Planner prompt có từ “implement”, “write code”, “perform”
- Reviewer prompt có từ “fix”, “rewrite”, “improve” (reviewer chỉ được identify, không được fix)
Khi thấy các dấu hiệu này, cắt bỏ. Roles cần ranh giới cứng.
Tradeoff: 3x cost vs quality gains
Rõ ràng nhất về chi phí: nếu một single agent call tốn X token, ba-role pipeline tốn ít nhất 3X. Trong thực tế thường nhiều hơn vì planner output cần được serialize và gửi vào executor prompt, executor output gửi vào reviewer prompt. Context accumulation làm mỗi call sau nặng hơn.
Vậy khi nào worth it?
Worth dùng role-based khi:
- Task phức tạp, nhiều bước phụ thuộc lẫn nhau, sai một bước có thể lan xuống các bước sau
- Output cần độ tin cậy cao: code production, plan có hệ quả thật (triển khai, giao tiếp khách hàng)
- Bạn cần auditability: ai lên kế hoạch gì, ai execute step nào, reviewer nói gì, mỗi phase có trace riêng
- Task lặp lại trong production, quality gain một lần là gain mãi mãi
Không worth dùng khi:
- Task đơn giản, một bước, kết quả dễ verify bằng code
- Prototype hoặc experiment, sai thì thử lại không hại gì
- Cost là constraint cứng và single agent đủ tốt
- Latency là constraint: ba LLM call nối tiếp nhau rõ ràng chậm hơn một call
Một heuristic tôi dùng: nếu bạn sẽ review kết quả thủ công trước khi dùng, hãy để reviewer agent làm việc đó. Bạn review thủ công không scale, reviewer agent scale. Nhưng nếu kết quả từ single agent đủ tốt để dùng trực tiếp không cần review, thêm reviewer chỉ tốn tiền.
Cheatsheet
| Role | Input | Output | System prompt focus | Pitfall |
|---|---|---|---|---|
| Planner | Task + context | Structured plan với dependencies | Rationality, completeness, ambiguity detection | Planner assume thay vì hỏi |
| Executor | One plan step + step context | Step result (success/error) | Precision, scope discipline | Executor tự expand scope |
| Reviewer | Full results + plan | Verdict + issues list | Skepticism, thoroughness | Reviewer approve quá dễ (“looks good”) |
| Orchestrator | Task | Final verdict | N/A (code thuần) | Không handle blocked verdict, loop vô hạn |
| Câu hỏi | Trả lời |
|---|---|
| Ba role có cần ba model khác nhau không? | Không. Cùng Sonnet 4.6 nhưng system prompt khác đã đủ |
| Orchestrator có phải LLM không? | Không nhất thiết. Python code thuần xử lý được |
| Reviewer có được sửa kết quả không? | Không. Chỉ identify. Orchestrator quyết định có retry không |
| Nên dùng khi nào? | Task phức tạp, nhiều bước, cần auditability |
Lời kết
Role-based pattern là một trong những ví dụ rõ nhất về “prompt engineering thay thế fine-tuning”. Bạn không cần ba model, không cần train gì thêm. System prompt khác nhau tạo ra hành vi khác nhau từ cùng một model.
Điều quan trọng nhất từ bài này: ranh giới role phải cứng. Một khi roles chồng lấp, bạn mất khả năng debug, mất auditability, và thường mất cả quality gain mà bạn chia role để đạt được.
Bài tiếp theo, LangGraph, CrewAI, AutoGen: so sánh framework, sẽ xem các framework phổ biến hiện tại implement pattern này như thế nào, và liệu abstraction của họ có giúp hay cản trở khi bạn đã hiểu control loop bên dưới.