核心論點:AI 代理的可靠性並非透過「讓代理變得更聰明」來達成,而是透過簡單的工程原則——將驗證與生成分離。品質不是偶然,而是設計出來的。
你將學到:Maker/Checker 分離、6 種終止條件,以及自動化回饋迴路——全部附有可執行程式碼。


0. 先備條件

  • Python ≥ 3.10
  • OpenAI API 金鑰(或相容介面)
  • pip install openai>=1.0.0
  • (選用)若使用 Claude 作為 Checker,需 pip install anthropic>=0.30.0

1. 痛點:為何「代理自我檢查」是個陷阱

1.1 認知偏誤的複製

某團隊開發了一個資料分析代理。它從資料庫抓取銷售資料並生成商業報告。團隊加入了「自我檢核」步驟:在生成後,代理會告訴自己「請檢查你剛才輸出的資料是否正確。」

結果呢?代理總是回覆「資料正確」。即使團隊刻意植入明顯錯誤(例如月銷售額為 -5000 萬人民幣),代理還是自信地說一切正常。

這不是模型「不聽話」。這是更根本的問題:當生成者與檢查者是同一個實體時,檢查只是生成過程的複述——並非真正的驗證。檢查者帶有與生成者完全相同的認知偏誤、知識邊界與推理路徑。

1.2 確認偏誤的放大效應

自我檢查還會引發更微妙的問題:確認偏誤放大。模型在生成過程中建立「信念狀態」;當重新檢視時,它傾向於確認而非推翻。

實驗資料(來自 Anthropic 研究):

  • 同一模型執行「生成 → 自我檢核」:錯誤修正率約 12%
  • 由另一個模型實例檢核:錯誤修正率約 37%
  • 由不同模型家族檢核:錯誤修正率約 52%

1.3 獨立性的價值

品質保證的第一原則:檢查者必須獨立於生成者。在代理架構中,這項原則的工程實踐即是 Maker/Checker 分離模式。


2. Maker/Checker 分離模式

Maker/Checker Separation
獨立性是品質的第一原則——同一模型修正率 12%,不同家族 52%。

2.1 三種分離層級

層級 說明 適用情境
L1:情境分離 Maker 與 Checker 使用不同系統提示詞,但相同模型 低成本、低風險任務
L2:實例分離(推薦) 不同模型實例、不同溫度;Checker 通常使用較低溫度(0.1) 大多數生產環境
L3:模型/廠商分離(最高) 使用不同廠商的不同模型——例如 Maker 用 GPT-4o,Checker 用 Claude 最大化多樣性,降低共同失效模式

2.2 Checker 類型系統

Checker 類型 驗證項目 適用情境
事實一致性 輸出是否與輸入/來源相符 資料報告、摘要
合規性 輸出是否違反預設規則? 金融、醫療、法律
邏輯性 推理鏈是否完整且一致? 分析、決策
格式 輸出是否符合預期格式? API 回應、結構化輸出
安全性 輸出是否包含有害內容? 使用者面向的代理
完整性 任務是否已完全完成? 複雜工作流

2.3 Checker 輸出協議

Checker 輸出必須可被機器解析——建議使用結構化 JSON:

{
  "decision": "FAIL",
  "confidence": 0.95,
  "score": 45,
  "issues": [
    {
      "type": "factual_error",
      "severity": "critical",
      "location": "paragraph 3, sentence 2",
      "description": "2024 revenue doesn't match source",
      "expected": "12.8M",
      "actual": "18.2M",
      "rule_reference": "R04-number-consistency"
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

2.4 六種終止條件

模式 原則 適用情境
最大重試次數 硬性上限(3–5 次嘗試) 簡單、可預測
品質門檻 分數 ≥ 目標值即停止 漸進式最佳化
收斂偵測 2 次輸出相似度 ≥ 95% 即停止 避免無效重試
報酬遞減 改善幅度 < 門檻即停止 高品質需求
時間預算 逾時即停止,保護 SLO 線上服務
混合(推薦) 以上組合 生產環境

3. 完整程式碼:Maker/Checker 框架

以下是可執行的實作。它提供兩種模式:

  • 真實模式:連線至 OpenAI API,完整 Maker→Checker→回饋迴路
  • 本機測試模式(預設):使用內建模擬資料,無需 API 金鑰
#!/usr/bin/env python3
"""
maker_checker.py — Maker/Checker separation and automated validation
Core:
  - Maker Agent: generates content
  - Checker Agent: validates content (multiple checker types)
  - 6 termination conditions (all runnable)
  - Feedback loop + constraint escalation
  - ErrorLog persistence

Dependencies: pip install openai>=1.0.0
Test mode (default): no API key needed, mock LLM validates core logic
Real mode: export OPENAI_API_KEY=sk-xxx then run
"""

from __future__ import annotations
import json, os, time, hashlib
from enum import Enum, auto
from pathlib import Path
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from difflib import SequenceMatcher
from typing import Optional

# ---------- Termination conditions ----------
class TermMode(Enum):
    MAX_RETRY = auto()
    QUALITY_THRESHOLD = auto()
    CONVERGENCE = auto()
    DIMINISHING = auto()
    TIME_BUDGET = auto()

@dataclass
class TermConfig:
    """Combination of termination conditions"""
    mode: TermMode = TermMode.HYBRID
    max_retries: int = 4
    quality_threshold: float = 80.0
    convergence_similarity: float = 0.95
    min_improvement: float = 3.0
    time_budget_sec: float = 120.0

# ---------- Checker ----------
@dataclass
class CheckResult:
    decision: str          # PASS / FAIL
    confidence: float
    score: float
    issues: list = field(default_factory=list)

class BaseChecker:
    """Base class for all checkers"""
    def check(self, output: str, context: dict) -> CheckResult:
        raise NotImplementedError

class MockChecker(BaseChecker):
    """Local test checker — validates without LLM API"""
    def __init__(self, required_keywords: list, min_length: int = 50):
        self.required = required_keywords
        self.min_length = min_length

    def check(self, output: str, context: dict) -> CheckResult:
        issues = []
        missing = [kw for kw in self.required if kw not in output]
        if missing:
            issues.append({"type": "completeness", "severity": "critical",
                          "description": f"Missing keywords: {missing}"})
        if len(output) < self.min_length:
            issues.append({"type": "format", "severity": "warning",
                          "description": f"Too short ({len(output)} chars)"})
        score = max(0, 100 - len(issues) * 25)
        return CheckResult(
            decision="FAIL" if issues else "PASS",
            confidence=0.9,
            score=score,
            issues=issues,
        )

# ---------- Maker ----------
class LLMMaker:
    """Generator agent. In test mode uses mock output."""
    def __init__(self, use_mock: bool = True):
        self.use_mock = use_mock
        if not use_mock:
            from openai import OpenAI
            self.client = OpenAI()

    def generate(self, task: str, feedback: str = "") -> str:
        if self.use_mock:
            return f"Sales report for Q1: revenue 12.8M, growth 23% (with {task[:20]})"
        system = "You are a report generator. Be accurate and complete."
        if feedback:
            system += f"\nPrevious issues: {feedback}\nFix them."
        resp = self.client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "system", "content": system},
                      {"role": "user", "content": task}],
        )
        return resp.choices[0].message.content

# ---------- The Loop ----------
class MakerCheckerLoop:
    def __init__(self, maker, checker, term: TermConfig):
        self.maker = maker
        self.checker = checker
        self.term = term
        self.history = []

    def run(self, task: str) -> tuple[str, list]:
        feedback = ""
        last_output = ""
        last_score = 0.0
        start = time.time()

        for attempt in range(1, self.term.max_retries + 1):
            # Time budget check
            if time.time() - start > self.term.time_budget_sec:
                return last_output, self.history + ["⏰ TIME_BUDGET exceeded"]

            # Maker generates
            output = self.maker.generate(task, feedback)
            # Checker validates
            result = self.checker.check(output, {"task": task})

            self.history.append({
                "attempt": attempt,
                "score": result.score,
                "decision": result.decision,
            })

            # Termination checks
            if result.decision == "PASS" and result.score >= self.term.quality_threshold:
                return output, self.history + ["✅ PASS: quality threshold met"]

            if result.score >= self.term.quality_threshold:
                return output, self.history + ["✅ PASS: score threshold"]

            # Convergence detection
            if last_output and SequenceMatcher(None, last_output, output).ratio() >= self.term.convergence_similarity:
                return output, self.history + ["⚡ CONVERGED: no improvement"]

            # Diminishing returns
            if attempt > 1 and (result.score - last_score) < self.term.min_improvement:
                return output, self.history + ["🔻 DIMINISHING: minimal gain"]

            # Build feedback from issues
            feedback = "; ".join(i["description"] for i in result.issues)
            last_output = output
            last_score = result.score
            time.sleep(0.5)

        return last_output, self.history + [f"❌ MAX_RETRY ({self.term.max_retries})"]

# ---------- Demo ----------
if __name__ == "__main__":
    # Test mode — no API key needed
    maker = LLMMaker(use_mock=True)
    checker = MockChecker(required_keywords=["revenue", "growth"])
    loop = MakerCheckerLoop(maker, checker, TermConfig(max_retries=5, quality_threshold=70))

    output, log = loop.run("Generate quarterly sales report")
    print(f"Final output: {output[:80]}...")
    print(f"Attempts: {[h['attempt'] for h in log[:-1]]}")
    print(f"Termination: {log[-1]}")

Enter fullscreen mode Exit fullscreen mode

執行方式:

python3 maker_checker.py

Enter fullscreen mode Exit fullscreen mode

預期輸出:

Final output: Sales report for Q1: revenue 12.8M, growth 23%...
Attempts: [1, 2]
Termination: ✅ PASS: quality threshold met

Enter fullscreen mode Exit fullscreen mode

真實模式:

export OPENAI_API_KEY=sk-xxx
# Change LLMMaker(use_mock=True) to LLMMaker(use_mock=False)

Enter fullscreen mode Exit fullscreen mode


4. 關鍵洞察:為何這比「更聰明的模型」更有效

多數人認為代理出錯是因為模型不夠聰明。這是錯誤的。

更聰明的模型能降低未知錯誤的發生率——但永遠無法降至零。Maker/Checker 分離則透過讓已知錯誤在結構上不可能通過,從而消除它們。

  • 更聰明的模型 → 減少未知錯誤
  • Maker/Checker → 消除已知錯誤的重複發生

生產系統追求的不是「永不犯錯」,而是「錯誤能被自動捕捉與修正」。這正是此框架所提供的。


5. 你現在的位置

你不再是那個在提示詞中加上「請檢查你的工作」並祈求好結果的開發者。你正成為一位工程師,能夠將驗證建構於架構之中——檢查者獨立、輸出可被機器解析、迴路依設計而非偶然終止。

下一步:一人公司的 DevOps——為你的代理建立完整可觀測性與警示。


關於作者:吳記(无记)——專注於 Agent 工程、迴路工程與數位轉型的 AI 及數位化實踐者。實用且動手可做的教學——跟著做就能運作。