PER:计划 + 执行 + 显式重规划
旅游助手做了一份计划:上午西湖、下午茶博物馆、晚上河坊街。然后天气变了,或者茶博物馆临时闭馆。Plan & Solve 写完计划就不管了——它会继续执行一份已经过时的计划。自由 Agent Loop 可以调整,但容易跑偏、没有计划约束。
PER 坐在两者之间:计划、执行一步、然后决定——继续、修改计划、还是直接结束。
一句话说清楚
PER 把"计划一次执行到底"改成"计划 → 执行一步 → 检查/重规划",让长任务可以中途修正方向。
解决什么问题
| 问题 | 表面看起来 | 实际风险 |
|---|---|---|
| 一份计划执行到底 | 有条理 | 过时计划继续跑 |
| 每一步都自由选择 | 灵活 | 目标漂移、成本膨胀 |
| 没有重规划节点 | 简单 | 失败会一路传播 |
引入什么复杂度
- 三个模型角色(Planner、Executor、Replanner),调用次数更多。
- Replanner 需要明确的判断标准——"什么算过时"不好定义。
- 频繁重规划可能导致每步都重写,反而不如 Plan & Solve。
和其他模式的关系
| 模式 | 谁决定下一步 | 什么时候用 |
|---|---|---|
| Plan & Solve | 按计划顺序执行 | 计划不太可能失效 |
| PER | Replanner 每步检查 | 执行可能让计划失效 |
| ReAct | 模型每步选动作 | 不需要显式长计划 |
| LLM Compiler | 计划变成依赖图 | 任务可以并行化 |
PER = Plan & Solve + 重规划能力。比 Plan & Solve 重(多了 replanner),比 ReAct 有结构(始终有计划在手)。
这个模式改变了什么
| 谁 | 负责什么 |
|---|---|
| Planner(模型) | 生成初始计划 |
| Executor(模型) | 执行当前步骤 |
| Replanner(模型) | 看执行结果,决定 continue / replan / final |
| Python | 存计划、限步数、调度三个角色、记 trace |
- 谁决定下一步:Replanner。它返回
continue(继续下一步)、replan(修改剩余计划)或final(直接给最终答案)。 - 谁拥有状态:Python 持有当前计划和所有历史执行结果。
- 什么时候停止:Replanner 返回
final,或达到max_steps。
走一遍真实轨迹
用户请求:"杭州三天两夜,预算 3000,想去茶博物馆。"
| 轮次 | 当前步骤 | 执行结果 | Replanner 决策 | 为什么 |
|---|---|---|---|---|
| 1 | 查天气 | Day2 下午暴雨 | continue |
天气信息不改计划结构 |
| 2 | 安排 Day1 路线 | 西湖 + 灵隐寺 | continue |
正常 |
| 3 | 安排 Day2 路线 | 茶博物馆 + 户外徒步 | replan |
下午暴雨,户外不行 |
| 3' | 安排 Day2 路线(重规划后) | 茶博物馆 + 宋城室内 | continue |
换成室内活动 |
| 4 | 安排 Day3 + 综合 | 良渚博物院 + 返程 | final |
任务完成 |
关键:轮次 3 执行完发现计划里有户外徒步和暴雨冲突,Replanner 把剩余计划改成室内活动。Plan & Solve 不会做这件事。
流程图
flowchart TD
T["用户任务"] --> P["Planner 生成计划"]
P --> E["Executor 执行当前步骤"]
E --> R["Replanner 检查结果"]
R -->|continue| E
R -->|replan| P2["更新剩余计划"]
P2 --> E
R -->|final| O["最终答案"]
完整实现
from __future__ import annotations
from pathlib import Path
from agent_patterns_lab.patterns.planner_executor_replanner import planner_executor_replanner
from agent_patterns_lab.runtime import MockLLM, RunLimits, Tracer
def main() -> None:
tracer = Tracer()
planner = MockLLM(
[
'{"plan":["查杭州天气","安排 Day1 路线","安排 Day2 路线","安排 Day3 路线并综合"]}',
]
)
executor = MockLLM(
[
"杭州:Day1 晴,Day2 下午暴雨,Day3 多云。",
"Day1:上午西湖游船,下午灵隐寺,晚上河坊街。",
"Day2:上午茶博物馆,下午宋城室内演出(原计划户外,因暴雨调整)。",
"Day3:上午良渚博物院,下午返程。预算约 2700 元。",
]
)
replanner = MockLLM(
[
# 查完天气 -> 继续
'{"action":"continue","plan":[],"answer":""}',
# Day1 -> 继续
'{"action":"continue","plan":[],"answer":""}',
# Day2 发现暴雨冲突 -> 重规划
'{"action":"replan","plan":["安排 Day2(改室内)","安排 Day3 并综合"],"answer":""}',
# Day2 改完 -> 继续
'{"action":"continue","plan":[],"answer":""}',
# Day3 -> 结束
'{"action":"final","plan":[],"answer":"杭州三天行程:Day1 西湖+灵隐寺+河坊街;Day2 茶博物馆+宋城(避雨);Day3 良渚+返程。预算 2700 元。"}',
]
)
result = planner_executor_replanner(
planner,
executor,
replanner,
task="杭州三天两夜,预算 3000,想去茶博物馆。",
limits=RunLimits(max_steps=8),
tracer=tracer,
)
print(result.answer)
trace_path = tracer.export_jsonl(Path(".traces") / "51_per_zh.jsonl")
print(f"[trace] {trace_path}")
if __name__ == "__main__":
main()
运行:
PYTHONPATH=src python examples/51_planner_executor_replanner.py
完整运行记录
杭州三天行程:Day1 西湖+灵隐寺+河坊街;Day2 茶博物馆+宋城(避雨);Day3 良渚+返程。预算 2700 元。
[trace] .traces/51_per_zh.jsonl
trace 中的事件序列:
planner_executor_replanner.plan steps=4
planner_executor_replanner.execute step="查杭州天气"
planner_executor_replanner.replan action=continue
planner_executor_replanner.execute step="安排 Day1 路线"
planner_executor_replanner.replan action=continue
planner_executor_replanner.execute step="安排 Day2 路线"
planner_executor_replanner.replan action=replan new_steps=2
planner_executor_replanner.execute step="安排 Day2(改室内)"
planner_executor_replanner.replan action=continue
planner_executor_replanner.execute step="安排 Day3 并综合"
planner_executor_replanner.replan action=final
注意 replan 事件——这是 Plan & Solve trace 里不会出现的。
踩坑与诊断
| 坑 | 现象 | trace 信号 | 修法 |
|---|---|---|---|
| 每步都重规划 | Replanner 每轮返回 replan |
replan 事件占比 > 50% |
给 Replanner 更明确的重规划条件;限制最大重规划次数 |
| 目标漂移 | 最终答案和原始请求不匹配 | final 里的关键词和用户任务关键词交集小 |
Replanner prompt 始终带原始任务和约束 |
| Replanner 不给理由 | 看 trace 不知道为什么重规划 | replan 事件无额外字段 |
要求 Replanner 返回 {"action":"replan","reason":"...","plan":[...]} |
| 无限循环 | 步数耗尽才停 | max_steps 被触发 |
设合理的 max_steps(通常 2x 初始计划步数) |
关键诊断技巧:如果 replan 频率超过 50%,先检查是不是初始计划太粗——更好的初始计划会减少重规划需求。
工程备忘
成本公式:
总调用 = 1(初始计划) + N(执行) + N(重规划判断) + R(额外重规划执行)
= 1 + 2N + R
4 步计划、1 次重规划 ≈ 10 次模型调用。比 Plan & Solve(6 次)贵约 60%。
生产注意事项:
- Replanner 的
continue/replan/final是结构化输出,走校验和重试。 max_steps建议设为初始计划步数的 2 倍。- 重规划次数也要限制(比如最多 3 次),否则 Replanner 会变成一个无限循环的 Agent Loop。
- 如果你的任务基本不需要重规划(<10% 的情况触发 replan),直接用 Plan & Solve 更划算。
读完以后
PER 适合"计划有用但可能过时"的任务。
- 如果计划基本不会变,退回 Plan & Solve。
- 如果任务可以变成依赖图,读 LLM Compiler。
- 如果需要在多个计划之间搜索,读 LATS。