Planner–Executor 与 Reviewer 模式
正在读取学习记录…
Planner–Executor是把计划与执行分开的协作方式。Planner是计划者,说明要查哪些资料、按什么顺序处理、交付什么。Executor是执行者,读取资料并生成候选结果。Reviewer是审查者,用明确条件检查候选结果,给出通过或具体问题。
拆开这三件事,有助于看见错误发生在哪个环节。计划漏查订单,和已经查到订单却把退款金额写错,修正方式不同。审查者若只说“我觉得不错”,队伍虽然多了一名成员,检查并没有多出一条。
先看一个能够修正的错误
订单O-17已付款120元,还未发货。规则允许在未发货时退已付金额。候选草稿却写了150元,超出了实际付款。
计划先读取订单,再读取退款规则,最后写草稿。执行者把读取的事实与依据编号一起交出。审查者检查150是否超过可信订单的120,返回amount_exceeds_paid,意思是候选金额超过已付款。下一轮把金额修正为120,再审查。
程序固定了两轮上限。只给一轮时,会保留需要人工处理的状态,不能把“我已经写完了”当成“审查通过了”。这里的approved表示候选材料通过这组检查;没有调用退款支付接口。
保存完整程序并运行
在自己的学习目录打开PowerShell,创建一个新的子目录:
mkdir l6-reviewer
cd l6-reviewer把下面完整代码保存为 reviewer_demo.py。固定数据和运行入口都已包含,文件名保留 .py 后缀。
"""用固定订单数据执行计划,再按业务条件审查草稿。"""
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,再运行:
ls
python -X utf8 reviewer_demo.py程序输出:
计划: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,表示在给定轮数内没有通过。程序没有取最后一份草稿强行交付。轮数限制和通过条件共同控制修正回路:前者限制消耗,后者决定结果能否离开回路。
三个角色之间需要怎样的契约
计划应能映射到执行器。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
"""审查条件和修正次数都由程序验证。"""
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()python -X utf8 -m unittest -v test_reviewer_demo.py测试名称后应显示 ok,末尾显示 Ran 8 tests 和 OK。若找不到模块,先用 dir 确认两个文件保存在同一目录。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 订单实际金额100元,初稿说可退1000元。检查者最有用的反馈是什么
订单实际金额100元,初稿说可退1000元。检查者最有用的反馈是什么?
对应知识点:L6-reviewer
简答题 · 解释机制
计划者、执行者和检查者各交付什么?
对应知识点:L6-reviewer
代码题 · 核对给定数据
写一条断言检查答复中的退款金额与订单金额相等。
对应知识点:L6-reviewer
场景题 · 分析具体变化
两轮检查后仍有同一金额错误,怎样结束当前循环?
对应知识点:L6-reviewer
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。