AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

Planner–Executor 与 Reviewer 模式

我的学习 →

正在读取学习记录…

Planner–Executor是把计划与执行分开的协作方式。Planner是计划者,说明要查哪些资料、按什么顺序处理、交付什么。Executor是执行者,读取资料并生成候选结果。Reviewer是审查者,用明确条件检查候选结果,给出通过或具体问题。

拆开这三件事,有助于看见错误发生在哪个环节。计划漏查订单,和已经查到订单却把退款金额写错,修正方式不同。审查者若只说“我觉得不错”,队伍虽然多了一名成员,检查并没有多出一条。

先看一个能够修正的错误

订单O-17已付款120元,还未发货。规则允许在未发货时退已付金额。候选草稿却写了150元,超出了实际付款。

计划先读取订单,再读取退款规则,最后写草稿。执行者把读取的事实与依据编号一起交出。审查者检查150是否超过可信订单的120,返回amount_exceeds_paid,意思是候选金额超过已付款。下一轮把金额修正为120,再审查。

程序固定了两轮上限。只给一轮时,会保留需要人工处理的状态,不能把“我已经写完了”当成“审查通过了”。这里的approved表示候选材料通过这组检查;没有调用退款支付接口。

保存完整程序并运行

在自己的学习目录打开PowerShell,创建一个新的子目录:

Bash
mkdir l6-reviewer
cd l6-reviewer

把下面完整代码保存为 reviewer_demo.py。固定数据和运行入口都已包含,文件名保留 .py 后缀。

Python
"""用固定订单数据执行计划,再按业务条件审查草稿。"""
ORDER = {"id": "O-17", "paid": 120, "shipped": False}

def plan():
    # 计划列出能实际执行的资料读取步骤和预期产物。
    return ["read_order", "read_refund_rule", "write_draft"]

def execute(steps, requested=150):
    facts, evidence = {}, []
    for step in steps:
        if step == "read_order":
            facts["paid"] = ORDER["paid"]
            facts["shipped"] = ORDER["shipped"]
            evidence.append("order:O-17")
        elif step == "read_refund_rule":
            facts["rule"] = "未发货可退已付金额"
            evidence.append("policy:R-1")
        elif step == "write_draft":
            if "paid" not in facts or "rule" not in facts:
                raise ValueError("写草稿前缺少订单或规则")
        else:
            raise ValueError("未知计划步骤")
    if not steps or steps[-1] != "write_draft":
        raise ValueError("计划缺少草稿产物")
    return {"amount": requested, "evidence": evidence, "facts": facts}

def review(draft):
    problems = []
    # Reviewer根据数据与规则检查,不能凭角色名称宣告正确。
    if set(draft["evidence"]) != {"order:O-17", "policy:R-1"}:
        problems.append("missing_evidence")
    paid = ORDER["paid"]
    if (draft["facts"].get("paid") != paid
            or draft["facts"].get("shipped") != ORDER["shipped"]):
        problems.append("facts_mismatch")
    if type(draft["amount"]) is not int or paid is None or not 0 < draft["amount"] <= paid:
        problems.append("amount_exceeds_paid")
    if draft["facts"].get("shipped") is not False or ORDER["shipped"] is not False:
        problems.append("eligibility_unknown")
    return problems

def run(max_rounds=2):
    if type(max_rounds) is not int or max_rounds < 1:
        raise ValueError("审查轮数必须是正整数")
    requested, trace = 150, []
    for number in range(1, max_rounds + 1):
        draft = execute(plan(), requested)
        problems = review(draft)
        trace.append(f"第{number}轮:金额={draft['amount']};问题={','.join(problems) or '无'}")
        if not problems:
            return "approved", draft, trace
        # 只根据已经查到的订单事实修正,不增加凭空的退款额度。
        requested = draft["facts"]["paid"]
    return "needs_human", draft, trace

def main():
    status, draft, trace = run()
    print("计划:" + " → ".join(plan()))
    for row in trace:
        print(row)
    print(f"处理结果:{status};候选退款={draft['amount']}元")
    print("依据:" + "、".join(draft["evidence"]))
    print("只给一轮:" + run(1)[0])

if __name__ == "__main__":
    main()

保存后执行 dir,确认能看到 reviewer_demo.py,再运行:

Bash
ls
python -X utf8 reviewer_demo.py

程序输出:

TXT
计划:read_order → read_refund_rule → write_draft
第1轮:金额=150;问题=amount_exceeds_paid
第2轮:金额=120;问题=无
处理结果:approved;候选退款=120元
依据:order:O-17、policy:R-1
只给一轮:needs_human

每一轮具体检查了什么

第一轮的150来自run中requested的初始值。execute并没有把这个值包装成事实:paid仍然来自ORDER,evidence保存订单与规则的编号。review对照可信ORDER检查金额,发现150大于120。

第二轮requested改为已读取的paid,重新执行计划,再审查。通过时返回120元候选金额和两条依据。两轮都能在trace中找到,没有省略一次看不见的“自动修好”。

只给一轮得到needs_human,表示在给定轮数内没有通过。程序没有取最后一份草稿强行交付。轮数限制和通过条件共同控制修正回路:前者限制消耗,后者决定结果能否离开回路。

计划者列步骤,执行者读事实生成150元草稿,审查者依120元事实退回;最多两轮,耗尽则转人工
反馈只有能指向具体事实和字段,才会驱动有效修正;循环还需要明确的次数上限。

三个角色之间需要怎样的契约

计划应能映射到执行器。read_order、read_refund_rule、write_draft都是程序认识的步骤名;未知步骤会失败。write_draft之前缺少事实或规则,也会失败。实际计划常包含子任务编号、输入来源、预期产物和依赖条件,这些字段比“请认真查证”更能指导执行。

执行结果应携带可检查的数据。草稿中的amount是候选金额;facts是读到的字段;evidence是依据引用。把三个部分分开,才能区分“字段缺了”和“金额算错了”。读取失败时不能把空资料解释成允许退款。

审查条件要紧贴业务。程序要求两条依据齐全,金额是正整数且不超过已付额,发货状态明确为False。为了防止执行者把facts中的paid改成999来绕过检查,审查者还直接对照可信ORDER。复制执行者自己的解释去判断自己,就像让考生先写答案,再自己写标准答案。

这份固定资料表让核对可见。接入真实订单服务后,可信事实应来自受权限约束的读取接口或经过验证的证据对象,而不是另一位Agent一句“我查过了”。

有审查者还会怎样出错

执行者和审查者可能共享同一个错误规则,两者也可能从同一份过期资料得出相同答案。角色分离改善责任可见性,却不能保证信息独立。审查前应确认资料来源、适用条件和版本;确定性的金额、字段与权限检查交给代码,解释性质量再交给人工或模型审查。

退回原因应具体到可修改的位置,例如“缺少发货状态”,而不是“再完善一点”。如果每轮产生同一问题,继续重写通常只会增加费用。可以根据问题类型选择补读资料、修正字段或转人工,并记录已尝试的修正。

修改轮数,看见停止边界

把main中的run()改为run(max_rounds=1),运行python -X utf8 reviewer_demo.py。主结果变为needs_human,金额仍为150,记录只有第一轮。恢复为2后,修正路径重新出现。

若把run里requested的初始值150改为120,再运行同一命令,第一轮便无问题,候选直接通过。此时末行“只给一轮”也变成approved;初始数据变化同时影响两次run调用。

两项对照分别完成后,将main的主调用恢复为run(),将run里的初始requested恢复为150,再运行。应重新看到150退回、120通过两轮,以及末行needs_human,随后再运行配套检查。

若看见缺少计划产物,检查steps最后是否为write_draft;若金额已正确却仍退回,查看问题代码和对应证据,不要增加轮数来掩盖条件缺失。

参考阅读:Building effective agents中的orchestrator-workers与evaluator-optimizer,重点区分任务分配和基于反馈的修正回路。

运行配套检查

把下面测试完整保存为 test_reviewer_demo.py,与主程序放在同一目录。8项检查覆盖计划顺序、超额金额、缺失依据、伪改付款与发货事实、未知发货状态、修正次数和非法上限。

查看测试代码:test_reviewer_demo.py
Python
"""审查条件和修正次数都由程序验证。"""
import copy
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parent))
import reviewer_demo as demo

class ReviewerTests(unittest.TestCase):
    def test_plan_must_read_before_drafting(self):
        with self.assertRaises(ValueError):
            demo.execute(["write_draft"])
        with self.assertRaises(ValueError):
            demo.execute(["read_order"])

    def test_amount_over_paid_is_rejected(self):
        self.assertIn("amount_exceeds_paid", demo.review(demo.execute(demo.plan())))

    def test_missing_evidence_is_rejected(self):
        draft = demo.execute(demo.plan(), 120)
        draft["evidence"].pop()
        self.assertIn("missing_evidence", demo.review(draft))

    def test_executor_cannot_raise_authoritative_paid_amount(self):
        draft = demo.execute(demo.plan(), 999)
        draft["facts"]["paid"] = 999
        self.assertIn("facts_mismatch", demo.review(draft))
        self.assertIn("amount_exceeds_paid", demo.review(draft))

    def test_executor_cannot_hide_shipping_from_authoritative_order(self):
        with patch.object(demo, "ORDER", {"id": "O-17", "paid": 120, "shipped": True}):
            draft = demo.execute(demo.plan(), 120)
            draft["facts"]["shipped"] = False
            self.assertIn("facts_mismatch", demo.review(draft))
            self.assertIn("eligibility_unknown", demo.review(draft))

    def test_unknown_shipping_is_rejected(self):
        draft = copy.deepcopy(demo.execute(demo.plan(), 120))
        draft["facts"]["shipped"] = None
        self.assertIn("eligibility_unknown", demo.review(draft))

    def test_two_rounds_repair_and_one_round_stops(self):
        status, draft, trace = demo.run()
        self.assertEqual((status, draft["amount"], len(trace)), ("approved", 120, 2))
        self.assertEqual(demo.run(1)[0], "needs_human")

    def test_invalid_round_budget(self):
        for value in (0, True, 1.5):
            with self.subTest(value=value), self.assertRaises(ValueError):
                demo.run(value)

if __name__ == "__main__":
    unittest.main()
Bash
python -X utf8 -m unittest -v test_reviewer_demo.py

测试名称后应显示 ok,末尾显示 Ran 8 tests 和 OK。若找不到模块,先用 dir 确认两个文件保存在同一目录。

课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.1.0

选择题 · 订单实际金额100元,初稿说可退1000元。检查者最有用的反馈是什么

订单实际金额100元,初稿说可退1000元。检查者最有用的反馈是什么?

对应知识点:L6-reviewer

选择一个答案
练习 2 / 4 · v1.1.0

简答题 · 解释机制

计划者、执行者和检查者各交付什么?

对应知识点:L6-reviewer

练习 3 / 4 · v1.1.0

代码题 · 核对给定数据

写一条断言检查答复中的退款金额与订单金额相等。

对应知识点:L6-reviewer

练习 4 / 4 · v1.1.0

场景题 · 分析具体变化

两轮检查后仍有同一金额错误,怎样结束当前循环?

对应知识点:L6-reviewer

图片查看器
100%

文章配图 · 按 Esc 关闭