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。