Có một buổi demo mà tôi nhớ mãi. Agent tôi đang build có nhiệm vụ tóm tắt ticket Jira và đề xuất fix. Tôi chạy thử với một ticket khá mơ hồ: “Button không hoạt động trên mobile”. Agent trả về một đoạn phân tích dài, tự tin, rất… sai. Nó đề xuất sửa CSS transition trong khi vấn đề thật là onClick không fire trên iOS Safari vì thiếu cursor: pointer.

Output trông “professional” nên khó phát hiện ngay. LLM không biết nó sai. Không có cơ chế nào hỏi lại “output này có hợp lý không trước khi trả về?”.

Self-reflection là pattern thêm bước đó vào.

Phần 1: Intuition từ Reflexion paper

Paper “Reflexion: Language Agents with Verbal Reinforcement Learning” (Shinn et al., 2023) đặt câu hỏi đơn giản: thay vì fine-tune model sau khi nó sai, thì để nó tự đánh giá output, tự viết phản hồi bằng ngôn ngữ tự nhiên, rồi thử lại với phản hồi đó làm context.

Kết quả trên HumanEval (code generation): một model tầm trung đi từ khoảng 65% lên 88% chỉ với vài vòng self-reflection, không cần fine-tune.

Cơ chế hoạt động là gì? Không phải magic. LLM khi ở vai trò “actor” (người làm việc) thường commit vào một hướng sớm. Khi nó được yêu cầu đóng vai “critic” (người đánh giá output của chính mình), nó tiếp cận vấn đề với góc nhìn khác, ít bị anchoring hơn, và thường phát hiện được lỗi mà actor bỏ qua.

Đây không phải insight hoàn toàn mới. Trong software engineering, bạn thường tự review code của mình sau khi để qua đêm. Reviewer tốt nhất vẫn là người viết code, nhưng ở một thời điểm khác, với frame khác. Self-reflection làm điều tương tự, chỉ là nén vào vài giây thay vì một đêm.

Phần 2: Hai pattern khác nhau, dùng đúng lúc

Trước khi code, cần phân biệt hai khái niệm hay bị dùng lẫn.

Verifier

Verifier kiểm tra output theo một spec có thể định nghĩa rõ ràng. Đúng hay sai có câu trả lời rõ.

Ví dụ:

  • Output có phải valid JSON không?
  • Code có chạy được không (thực thi thử)?
  • URL có trả về 200 không?
  • Kết quả SQL query có trả về ít nhất một row không?

Verifier thường không cần LLM. Một đoạn Python đơn giản là đủ. Nếu fail, retry với thông báo lỗi cụ thể từ runtime.

Critic

Critic đánh giá output theo tiêu chí mở và chủ quan hơn. Không có đúng/sai tuyệt đối.

Ví dụ:

  • Giải thích này có đủ rõ cho một developer junior không?
  • Đề xuất refactor này có đúng với codebase context không?
  • Tóm tắt này có bỏ sót thông tin quan trọng không?

Critic cần LLM (hoặc một model khác) vì đây là đánh giá ngữ nghĩa, không phải kiểm tra syntax.

Khi nào dùng cái nào?

Dùng verifier trước bao giờ hết. Nếu có spec rõ ràng, không cần tốn token cho critic. Verifier rẻ, nhanh, và deterministic.

Dùng critic khi verifier không đủ, khi chất lượng output phụ thuộc vào ngữ nghĩa và context, hoặc khi cost của output sai cao (gửi email nhầm, đề xuất sai cho khách hàng).

Phần 3: Implementation

Actor + Verifier

Đây là pattern đơn giản nhất:

import anthropic
import json

client = anthropic.Anthropic()

def generate_json_output(prompt: str, max_retries: int = 3) -> dict:
    messages = [{"role": "user", "content": prompt}]
    last_error = None

    for attempt in range(max_retries):
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            messages=messages,
        )
        raw = response.content[0].text

        # Verifier: kiểm tra valid JSON
        try:
            result = json.loads(raw)
            return result
        except json.JSONDecodeError as e:
            last_error = str(e)
            # Thêm feedback vào context để retry
            messages.append({"role": "assistant", "content": raw})
            messages.append({
                "role": "user",
                "content": (
                    f"Output trên không phải valid JSON. Lỗi: {last_error}. "
                    "Vui lòng trả về lại, chỉ JSON, không có text nào khác ngoài JSON."
                )
            })

    raise ValueError(f"Không thể lấy valid JSON sau {max_retries} lần. Lỗi cuối: {last_error}")

Pattern này đơn giản nhưng hiệu quả cao. Thay vì chỉ retry từ đầu (mất context), bạn giữ nguyên conversation và thêm thông báo lỗi vào. LLM thường sửa đúng ngay lần thứ hai.

Actor + Critic (cùng model)

Khi tiêu chí đánh giá phức tạp hơn, bạn cần LLM đóng vai critic:

def actor_with_critic(task: str, max_retries: int = 3) -> str:
    actor_prompt = f"""Bạn là một senior developer. Nhiệm vụ: {task}

Trả lời ngắn gọn, cụ thể. Chỉ đề xuất những gì bạn tự tin là đúng."""

    critic_system = """Bạn là một code reviewer kinh nghiệm. Nhiệm vụ của bạn là đánh giá
một đề xuất kỹ thuật. Chỉ tìm lỗi thật sự, không bịa thêm vấn đề không tồn tại.

Trả về JSON với format:
{
  "passed": true/false,
  "issues": ["issue 1", "issue 2"],
  "feedback": "hướng dẫn cụ thể để cải thiện nếu passed=false"
}"""

    draft = None
    feedback = None

    for attempt in range(max_retries):
        # Actor tạo output
        actor_messages = [{"role": "user", "content": actor_prompt}]
        if feedback:
            actor_messages[0]["content"] = (
                f"{actor_prompt}\n\nLần trước bạn đề xuất:\n{draft}\n\n"
                f"Reviewer có nhận xét: {feedback}\n\nHãy sửa lại."
            )

        actor_response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=1024,
            messages=actor_messages,
        )
        draft = actor_response.content[0].text

        # Critic đánh giá
        critic_response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=512,
            system=critic_system,
            messages=[{
                "role": "user",
                "content": f"Task: {task}\n\nĐề xuất cần đánh giá:\n{draft}"
            }],
        )

        try:
            evaluation = json.loads(critic_response.content[0].text)
        except json.JSONDecodeError:
            # Critic trả về text thay vì JSON, coi như passed
            return draft

        if evaluation.get("passed"):
            return draft

        feedback = evaluation.get("feedback", "Cần cải thiện chất lượng.")
        issues = evaluation.get("issues", [])
        if issues:
            feedback = f"{feedback}. Vấn đề cụ thể: {', '.join(issues)}"

    # Hết lần retry, trả về draft cuối cùng kèm cảnh báo
    return draft

Tách actor và critic thành hai model khác nhau

Có trường hợp bạn muốn dùng model nhanh/rẻ làm actor, và model mạnh hơn làm critic. Ví dụ: Haiku tạo draft, Sonnet đánh giá.

def actor_critic_two_models(task: str) -> str:
    # Actor: model rẻ, nhanh
    actor_response = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": task}],
    )
    draft = actor_response.content[0].text

    # Critic: model mạnh hơn, đánh giá kỹ
    critic_response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=512,
        messages=[{
            "role": "user",
            "content": (
                f"Đánh giá output sau cho task: {task}\n\n"
                f"Output: {draft}\n\n"
                "Trả về JSON: {\"passed\": bool, \"reason\": \"...\", \"improved_output\": \"...\"}"
            )
        }],
    )

    try:
        evaluation = json.loads(critic_response.content[0].text)
        if evaluation.get("passed"):
            return draft
        return evaluation.get("improved_output", draft)
    except json.JSONDecodeError:
        return draft

Pattern này giảm latency và cost cho actor (vì Haiku rẻ hơn nhiều), nhưng vẫn có Sonnet kiểm soát chất lượng cuối. Hữu ích khi agent cần tạo nhiều draft song song.

Phần 4: So sánh với plan-execute và replan

Bài 7 về Plan-and-Execute đã đề cập hai hướng khi một step thất bại: replan (làm lại plan từ đầu) và reflect (tự đánh giá và retry step hiện tại).

Sự khác biệt ở scope:

  • Reflect/retry trong self-reflection: đánh giá một output cụ thể, retry đúng output đó. Không thay đổi plan tổng thể.
  • Replan: output một step sai đến mức plan cũ không còn phù hợp. Phải bắt đầu lại từ planning.

Nên dùng reflect khi: output sai nhưng hướng đúng, cần polish. Nên replan khi: phát hiện assumption sai từ đầu, hoặc context thay đổi hoàn toàn.

Trong code, heuristic đơn giản: nếu critic nói “cần sửa nhỏ”, retry. Nếu critic nói “sai hướng hoàn toàn”, escalate lên replan.

Phần 5: Pitfall hay gặp nhất

Đây là câu chuyện tôi muốn kể chi tiết hơn.

Tôi build một agent tóm tắt pull request. Critic được prompt như sau: “Đánh giá tóm tắt này xem có đầy đủ, rõ ràng, và đúng kỹ thuật không.” Nghe hợp lý.

Lần chạy thử đầu tiên: actor tạo tóm tắt, critic trả về passed: false, feedback “cần thêm context về breaking changes”. Actor sửa lại. Critic lần 2: passed: false, feedback “cần giải thích rõ hơn tác động đến performance”. Actor sửa lại. Critic lần 3: passed: false, feedback “nên đề cập đến test coverage”…

Vòng lặp chạy đến lần thứ 10 mới dừng (do max_retries). Mỗi iteration actor phải đọc PR diff, tổng cộng khoảng 40.000 token chỉ cho một PR tóm tắt. Chi phí cho một task đáng lẽ tốn 2000 token.

Vấn đề: critic bị prompt quá mở. “Đầy đủ, rõ ràng, và đúng kỹ thuật” không có giới hạn, critic có thể tìm thêm điểm cải thiện mãi mãi. LLM không tự biết “đủ tốt rồi, dừng lại”.

Cách sửa:

critic_system = """Bạn là reviewer đánh giá một tóm tắt pull request.
Chỉ fail nếu output vi phạm ít nhất một trong các điều kiện sau:
1. Thông tin kỹ thuật sai so với diff được cung cấp.
2. Thiếu mô tả về breaking change (nếu có breaking change trong diff).
3. Dài hơn 200 words.

Nếu không vi phạm điều nào, bắt buộc trả về passed: true.
Không được fail vì "có thể cải thiện thêm" hay "nên thêm X".
Tiêu chuẩn là đủ tốt, không phải hoàn hảo."""

Ba thay đổi quan trọng:

  1. Tiêu chí cụ thể và có thể kiểm tra.
  2. Điều kiện fail rõ ràng (binary: vi phạm hay không).
  3. Explicit instruction “không fail vì có thể cải thiện thêm” để chống over-criticism.

Sau khi sửa, agent pass sau 1-2 lần và dừng.

Bài học: Critic cần được constrain không kém gì actor. Prompt critic tốt cần chỉ ra rõ: fail condition là gì, stop condition là gì, và phân biệt “lỗi thật” với “có thể tốt hơn”.

Phần 6: LLM-as-judge bias

Khi dùng LLM làm critic, cần nhớ một bias đã được nghiên cứu: LLM có xu hướng đánh giá cao output dài hơn và verbose hơn, ngay cả khi output ngắn hơn đúng hơn.

Paper “Large Language Models Are Not Robust Multiple Choice Selectors” và nhiều benchmark khác đều confirm bias này. Critic LLM thường prefer output có cấu trúc rõ ràng (bullet, header), câu dài, nhiều từ kỹ thuật, dù content không tốt hơn.

Cách mitigate:

critic_prompt = """Đánh giá hai output sau cho cùng một task.
QUAN TRỌNG: Không được ưu tiên output dài hơn hay có nhiều bullet hơn.
Đánh giá dựa trên: độ chính xác kỹ thuật và mức độ trả lời đúng câu hỏi.

Task: {task}
Output A: {output_a}
Output B: {output_b}

Trả về: {{"winner": "A" hoặc "B", "reason": "lý do cụ thể dựa trên nội dung, không phải format"}}"""

Ngoài prompt, cũng có thể dùng kỹ thuật blind evaluation: critic không biết output nào là draft lần mấy, tránh recency bias.

Phần 7: Khi nào nên thêm critic

Self-reflection không free. Mỗi vòng actor+critic tốn ít nhất gấp đôi token so với actor một mình. Latency tăng. Đôi khi critic sai.

Nên thêm critic khi:

Tình huốngNên critic
Output đi vào production trực tiếp (email, báo cáo)
Output dùng để làm input cho step tiếp theo quan trọng
Task phức tạp, nhiều cách làm
Output có thể verifier bằng codeKhông (dùng verifier)
Prototype nhanh, cost là ưu tiênKhông
Agent chatbot đơn giản, user tự đánh giáKhông

Cũng nên nhớ: critic không thay thế được external evaluation. Bài 21 về eval cho agent với trace và replay sẽ đi sâu vào cách build golden set và regression test cho agent. Critic runtime là tự đánh giá của model trong một session cụ thể; eval trace/replay là đánh giá hệ thống, so sánh với ground truth, phát hiện regression qua nhiều deployment.

Cheatsheet

PatternKhi nào dùngCostDeterministic?
VerifierCó spec rõ (JSON, HTTP status, syntax)Thấp
Actor + Verifier retryFormat output cần đúngThấpTrung bình
Actor + Critic (cùng model)Chất lượng ngữ nghĩa quan trọngCaoKhông
Actor + Critic (hai model)Tối ưu cost và qualityTrung bìnhKhông
PitfallTriệu chứngCách phòng
Critic over-criticizeLoop 5-10 lần không dừngCritic prompt có stop condition rõ
LLM verbose biasCritic luôn prefer output dàiExplicit “không ưu tiên length”
Critic saiOutput tệ hơn sau retryCap max_retries ở mức thấp (2-3)
Cost vượt kiểm soátToken tăng đột biếnTrack token mỗi vòng, có hard cap
# Skeleton tối giản cho actor+critic pattern
def actor_critic(task: str, max_retries: int = 3) -> str:
    draft = actor_generate(task)
    for _ in range(max_retries):
        result = critic_evaluate(task, draft)
        if result["passed"]:
            return draft
        draft = actor_generate(task, feedback=result["feedback"])
    return draft  # trả về best effort sau max_retries

Lời kết

Self-reflection là pattern thêm một “lần nhìn lại” trước khi agent trả về output. Không phải bài thuốc vạn năng, không phải pattern nên mặc định thêm vào mọi agent. Nhưng khi output cần chất lượng cao và bạn không thể kiểm tra bằng code, nó là công cụ đáng giá.

Điểm mấu chốt: critic cần được constrain cũng chặt như actor. Một critic prompt quá mở sẽ over-criticize và khiến agent loop mãi.

Bài tiếp theo, Chain-of-Thought so với structured reasoning, sẽ nhìn vào câu hỏi khác: trước khi actor tạo output, nó “nghĩ” như thế nào? Chain-of-Thought scratchpad khác gì structured output schema, và khi nào cần cái nào.