跳转至

Maker-Checker:先写后查

旅游助手能写出一份行程了,但"读起来像那么回事"不等于"行程是好的"。它可能漏掉"轻松步行"的要求,把三个景点排在雨天下午的户外,或者给出模糊的"建议带伞"而没说具体原因。

最小的修法不是上多 Agent,而是把一个任务拆成两个角色:一个写,一个查。

一句话

Maker-Checker 把一次性生成变成"草稿 → 检查 → 修订",让质量标准变成可执行的反馈循环。

它修什么失败

问题 表面看起来 实际风险
直接出最终答案 模糊、遗漏、不可控
同一个 prompt 自我检查 简单 模型倾向于自我辩护
没有通过/不通过标准 听着像审过了 你判断不了它到底过没过

它引入什么复杂度

负责什么
Maker(模型) 写草稿
Checker(模型) 按 rubric 返回 pass/fail + 反馈
Python 控制轮次、把反馈送回 Maker、到上限停止

Checker 的输出必须是结构化的:

{"passed": false, "feedback": "太笼统。加上具体景点名和时间。"}

和其他模式的关系

flowchart LR
  MC["Maker-Checker"] -.->|"事实断言需要逐条验"| COVE["CoVe"]
  MC -.->|"短答案方差大"| VOT["Voting"]
  MC -.->|"失败经验要跨任务"| REF["Reflexion"]
  • CoVe:Maker-Checker 检查整体质量;CoVe 逐条验证事实。如果 Checker 发现"事实不对"但不知道哪条错,就需要 CoVe。
  • Voting:如果答案短且可标准化,多采样投票比来回修订更便宜。
  • Reflexion:Maker-Checker 只管当前这一轮;Reflexion 把失败教训存下来,下次同类任务不用从零开始。

完整实现:旅游助手行程审查

下面是一个完整的旅游场景。Maker 写杭州一日游行程,Checker 按 rubric 检查。

"""maker_checker_travel.py — 旅游行程的 Maker-Checker"""
from __future__ import annotations

import json
from dataclasses import dataclass
from typing import Any


# ── 极简运行时(和项目 runtime 同构,但自包含可直接跑) ──────────

@dataclass(frozen=True)
class Message:
    role: str
    content: str

class MockLLM:
    """按顺序返回预设回复的假模型。"""
    def __init__(self, responses: list[str]):
        self._responses = list(responses)
        self._idx = 0
    def complete(self, messages: list[Message]) -> str:
        if self._idx >= len(self._responses):
            raise RuntimeError("MockLLM 回复用完了")
        out = self._responses[self._idx]
        self._idx += 1
        return out

@dataclass(frozen=True)
class CheckResult:
    passed: bool
    feedback: str


# ── Maker-Checker 核心逻辑 ──────────────────────────────

def maker_checker(
    maker: MockLLM,
    checker: MockLLM,
    *,
    task: str,
    max_rounds: int = 3,
) -> str:
    """
    1) Maker 写草稿
    2) Checker 返回 {passed, feedback}
    3) 没通过 → Maker 带着反馈修订
    4) 到 max_rounds 还没通过,返回最后一版
    """
    draft: str | None = None
    for round_idx in range(max_rounds):
        # ── Maker 阶段 ──
        if draft is None:
            maker_prompt = task
        else:
            maker_prompt = (
                f"任务:\n{task}\n\n"
                f"请根据反馈修订下面的草稿:\n{draft}"
            )
        draft = maker.complete([
            Message(role="system", content="你是行程规划师。写出最佳草稿。"),
            Message(role="user", content=maker_prompt),
        ])
        print(f"[轮次 {round_idx + 1}] Maker 草稿:{draft[:80]}...")

        # ── Checker 阶段 ──
        check_prompt = (
            f"任务:\n{task}\n\n草稿:\n{draft}\n\n"
            "按 rubric 检查,返回 JSON:{{\"passed\": true/false, \"feedback\": \"...\"}}"
        )
        raw = checker.complete([
            Message(role="system", content="你是行程审查员。严格按标准评估。只返回 JSON。"),
            Message(role="user", content=check_prompt),
        ])
        result = _parse_check(raw)
        print(f"[轮次 {round_idx + 1}] Checker:passed={result.passed}, feedback={result.feedback}")

        if result.passed:
            return draft

    print(f"[警告] {max_rounds} 轮后仍未通过,返回最后草稿")
    return draft or ""


def _parse_check(raw: str) -> CheckResult:
    obj = json.loads(raw)
    return CheckResult(
        passed=bool(obj["passed"]),
        feedback=str(obj.get("feedback", "")),
    )


# ── 运行 ────────────────────────────────────────────────

def main() -> None:
    task = (
        "用户要求:杭州一日游,偏好轻松步行,下午可能下雨。\n"
        "Rubric:\n"
        "1. 至少 3 个景点,含室内备选\n"
        "2. 时间安排具体到小时\n"
        "3. 提到下雨应对\n"
        "4. 提到交通方式"
    )

    maker = MockLLM([
        # 第 1 轮草稿:太笼统
        "上午去西湖,下午去灵隐寺,晚上河坊街。建议带伞。",
        # 第 2 轮草稿:修订后更好
        (
            "09:00 西湖断桥(步行 40 分钟)→ "
            "10:30 中国茶叶博物馆(室内,步行 30 分钟)→ "
            "12:00 龙井村午餐 → "
            "14:00 如遇下雨转中国丝绸博物馆(室内)/ 晴天继续太子湾公园 → "
            "17:00 河坊街小吃。全程公交+步行,备折叠伞。"
        ),
    ])

    checker = MockLLM([
        # 第 1 轮检查:不通过
        json.dumps({
            "passed": False,
            "feedback": "缺少具体时间安排;未提供室内备选;交通方式不明确。"
        }, ensure_ascii=False),
        # 第 2 轮检查:通过
        json.dumps({
            "passed": True,
            "feedback": ""
        }),
    ])

    result = maker_checker(maker, checker, task=task, max_rounds=3)
    print(f"\n最终行程:\n{result}")


if __name__ == "__main__":
    main()

运行日志

[轮次 1] Maker 草稿:上午去西湖,下午去灵隐寺,晚上河坊街。建议带伞。...
[轮次 1] Checker:passed=False, feedback=缺少具体时间安排;未提供室内备选;交通方式不明确。
[轮次 2] Maker 草稿:09:00 西湖断桥(步行 40 分钟)→ 10:30 中国茶叶博物馆(室内,步行 30 分钟)→ 12:00 龙...
[轮次 2] Checker:passed=True, feedback=

最终行程:
09:00 西湖断桥(步行 40 分钟)→ 10:30 中国茶叶博物馆(室内,步行 30 分钟)→ 12:00 龙井村午餐 → 14:00 如遇下雨转中国丝绸博物馆(室内)/ 晴天继续太子湾公园 → 17:00 河坊街小吃。全程公交+步行,备折叠伞。

调试:trace 里看什么

要看的 怎么看 说明什么
round_index trace 里每轮的编号 太多轮说明 rubric 太严或 Maker 能力不足
passed Checker 输出的布尔值 如果一直 false,检查 feedback 是否具体
feedback 内容 Checker 返回的文字 "挺好的"这种空话意味着 Checker prompt 需要改
Maker 的修订幅度 对比两轮草稿 如果修订后几乎没变,说明 feedback 没被利用

工程笔记

成本公式

总调用数 = 轮次数 × 2(一次 Maker + 一次 Checker)
最好情况 = 2 次调用(第 1 轮就通过)
最坏情况 = max_rounds × 2

每多一轮,token 成本大约翻倍(因为 Maker 要看到反馈和历史草稿)。

常见坑

现象 修法
Checker 太客气 永远 "Looks good" 用 rubric 清单 + 结构化 JSON
无限修订 永远不通过 max_rounds
反馈不可执行 "写好点" 要求 Checker 列出具体缺失项
共享幻觉 Maker 和 Checker 都相信同一个假事实 加工具验证或 CoVe
Checker 和 Maker 用同一个模型 自我辩护 用不同 temperature 或不同模型

什么时候用

  • 你能写出 rubric。
  • 草稿质量不稳定但反馈能改善它。
  • 风险中等,值得多一轮模型调用。

什么时候别用

  • 没有可执行的质量标准。
  • Maker 和 Checker 有相同的盲区。
  • 一次回答就够了。
  • 需要事实验证,那是 CoVe 或检索的活。

读完以后

Maker-Checker 让不稳定的质量变得可见、可修复。

如果问题是事实断言需要逐条验证,读 CoVe。 如果问题是短答案随机方差大,读 Voting。 如果失败经验需要跨任务保留,读 Reflexion