在上一篇文章中,我们用纯 Python 实现了 ReAct 模式,它像一位即兴演员,边走边看,灵活应对。但面对长链路、高复杂度的任务时,ReAct 容易迷失在局部细节中,甚至忘记最初的目标。今天,我们基于 Plan-and-Execute 架构,手搓一个更可靠的 Planner Agent。依然是:0 框架,全原生,纯逻辑

“如果说 ReAct 是带指南针的探险家,那么 Planner 就是带施工图纸的工程师。”

️ 架构哲学:谋定而后动

Plan-and-Execute 模式的核心是将思考过程拆解为两个独立阶段:规划(Planning)执行(Executing)。规划阶段由一个“大脑”负责,它不执行任何操作,只负责将用户问题拆解为清晰的步骤清单。执行阶段则由一个“工人”负责,它不需要理解全局目标,只需逐条执行步骤,并将结果反馈。

这种分离带来了巨大的确定性——每一步都是可预测的,而非黑盒式的随机推理。与 ReAct 的“边想边做”不同,Plan-and-Execute 更接近人类解决复杂问题的流程:先画蓝图,再动手施工。

最佳实践:在 Python、TypeScript 或 Go 项目中,这种架构特别适合需要多步数据处理的场景,比如自动化报告生成、多 API 编排等。如果你曾用 JavaScript 写过复杂的异步链式调用,你一定明白“计划赶不上变化”的痛苦——Plan-and-Execute 正是为此而生。

️ 核心实现:Planner Agent(planner_agent.py

这是完整的 Planner Agent 实现。请注意,我们通过两个不同的 Prompt 分别控制“规划”和“执行”阶段。不需要复杂的 Prompt Engineering,只需要强制模型输出一个 JSON 列表。这里没有魔法,我们不需要 OutputParser 类,只需要 json.loads()。如果模型输出格式不对,那是 Prompt 写得不够强硬,或者 Temperature 设置得太高(建议 0.1)。

在执行阶段,我们将 context 字典直接注入到 System Prompt 中,实现显式的状态管理。这种方式比用全局变量或数据库更轻量,也更容易调试。

import json
from typing import List, Dict, Any
from openai import OpenAI
from tools import ToolRegistry
class PlanAndExecuteAgent:
"""
Plan-and-Execute Agent (Native Implementation)
Philosophy:
Instead of "thinking on the fly" (ReAct), this agent:
1. PLANS: Breaks the complex task into a sequence of simple steps first.
2. EXECUTES: Executes each step sequentially.
This is better for complex tasks where maintaining long-term context in a single loop is difficult.
"""
def __init__(self, model: str = "gpt-4o", tools_registry: ToolRegistry = None, api_key: str = None, base_url: str = None):
self.client = OpenAI(api_key=api_key, base_url=base_url)
self.model = model
self.registry = tools_registry
self.tool_descriptions = self._build_tool_descriptions()
self.tool_names = ", ".join([s["name"] for s in self.registry.get_tools_schema()])
def _build_tool_descriptions(self) -> str:
schemas = self.registry.get_tools_schema()
lines = []
for s in schemas:
lines.append(f"{s['name']}: {s['description']}")
lines.append(f"    Args: {json.dumps(s['parameters'])}")
return "\n".join(lines)
def plan(self, query: str) -> List[str]:
"""
Phase 1: The Planner
Generates a list of logical steps to solve the problem.
"""
print(f"\n Planning for: {query}")
system_prompt = f"""
You are a global planner.
Your goal is to break down a complex user question into a sequence of simple, logical steps.
You have access to the following tools (but do not use them yet, just plan for them):
{self.tool_descriptions}
Output Format:
You must output a strict JSON list of strings. Each string is a step.
Example:
["Get the weather in Beijing", "Get the weather in New York", "Compare the temperatures"]
Do not output anything else. Just the JSON list.
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": query}
]
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=0.1 # Lower temperature for stable planning
)
content = response.choices[0].message.content.strip()
# Clean up markdown if present
content = content.replace("```json", "").replace("```", "").strip()
try:
steps = json.loads(content)
print(f" Generated Plan: {json.dumps(steps, indent=2, ensure_ascii=False)}")
return steps
except Exception as e:
print(f"❌ Planning Failed: {e}. Output: {content}")
return [query] # Fallback to treating the whole query as one step
def execute_step(self, step: str, context: Dict[str, Any]) -> str:
"""
Phase 2: The Executor (Solver)
Executes a single step, having access to previous context.
This is essentially a mini-ReAct or Function Calling loop, but we'll simplify it to a "One-Shot" tool use for this demo.
"""
print(f"\n Executing Step: {step}")
# Context string
context_str = "\n".join([f"Previous Step: {k} -> Result: {v}" for k, v in context.items()])
system_prompt = f"""
You are a worker agent.
Your task is to execute the current step given the context of previous steps.
You have access to tools:
{self.tool_descriptions}
Context:
{context_str}
Current Step: {step}
Instructions:
1. If you can answer the step using the context, just answer.
2. If you need a tool, output a JSON object: {{"tool": "tool_name", "args": {{...}}}}
3. If you don't need a tool, output a JSON object: {{"answer": "your answer"}}
Output MUST be strict JSON.
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Execute this step: {step}"}
]
response = self.client.chat.completions.create(
model=self.model,
messages=messages
)
content = response.choices[0].message.content.strip()
content = content.replace("```json", "").replace("```", "").strip()
try:
result_json = json.loads(content)
if "tool" in result_json:
tool_name = result_json["tool"]
tool_args = result_json["args"]
print(f"️ Worker invoking: {tool_name} with {tool_args}")
observation = self.registry.execute(tool_name, tool_args)
print(f" Observation: {observation}")
return f"Tool Output: {observation}"
elif "answer" in result_json:
return result_json["answer"]
else:
return str(result_json)
except Exception as e:
return f"Error executing step: {e}. Raw output: {content}"
def run(self, query: str):
# 1. Plan
plan = self.plan(query)
# 2. Execute Loop
context = {}
for step in plan:
result = self.execute_step(step, context)
context[step] = result
print(f"✅ Step Result: {result}")
# 3. Final Synthesis
print("\n Synthesizing Final Answer...")
final_prompt = f"""
Original Question: {query}
Execution History:
{json.dumps(context, indent=2, ensure_ascii=False)}
Please provide the final comprehensive answer based on the execution history.
"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": final_prompt}]
)
final_answer = response.choices[0].message.content
print(f"\n Final Answer:\n{final_answer}")
return final_answer

⚠️ 常见问题:很多开发者在使用 C++ 或 Go 时,习惯用复杂的类继承来管理状态,但在 Python 中,一个简单的字典足以胜任。记住:控制权在开发者手中,我们可以精确控制 Plan 的生成、Context 的传递格式,以及每一步的容错逻辑。

运行实战:Demo 脚本(demo_planner.py

为了运行这个 Agent,我们需要组装环境,加载 API Key,并注册一些测试用的工具。以下脚本展示了如何将 Planner Agent 与简单的工具函数(如计算、搜索等)集成。

import os
import sys
from dotenv import load_dotenv
# Import main to ensure tools (get_weather, calculate) are registered in the registry
# In a real project, tools would be in a separate module like 'my_tools.py'
try:
import main
except ImportError:
# If main.py has issues or specific runtime code, we might define tools here
pass
from tools import registry
from planner_agent import PlanAndExecuteAgent
# If main didn't register tools (e.g. if we refactored), let's ensure we have them
if "get_weather" not in registry._tools:
@registry.register(name="get_weather", description="Get the current weather for a given city.")
def get_weather(city: str):
print(f"[System] Querying weather for {city}...")
mock_data = {
"Beijing": "Sunny, 25°C",
"Shanghai": "Rainy, 22°C",
"New York": "Cloudy, 15°C",
"Tokyo": "Sunny, 20°C"
}
return mock_data.get(city, "Unknown city, assuming Sunny, 20°C")
@registry.register(name="calculate", description="Calculate a mathematical expression.")
def calculate(expression: str):
print(f"[System] Calculating: {expression}")
try:
return eval(expression)
except Exception as e:
return f"Error: {e}"
def run_demo():
load_dotenv(override=True)
api_key = os.getenv("OPENAI_API_KEY")
base_url = os.getenv("BASE_URL", "https://api.moonshot.cn/v1")
model_name = os.getenv("MODEL_NAME", "moonshot-v1-8k")
if not api_key:
print("Error: OPENAI_API_KEY environment variable is not set.")
return
print(f" Initializing Plan-and-Execute Agent with model: {model_name}")
agent = PlanAndExecuteAgent(
model=model_name,
tools_registry=registry,
api_key=api_key,
base_url=base_url
)
# A multi-step query suitable for planning
query = "What is the temperature difference between Beijing and Shanghai? (Get weather for both first)"
print(f"\n User Query: {query}")
agent.run(query)
if __name__ == "__main__":
run_demo()

在实际项目中,你可以将工具函数替换为任何 API 调用、数据库查询或文件操作。比如,用 TypeScript 写一个爬虫工具,或用 JavaScript 调用第三方服务。关键在于,执行器不需要知道全局目标,它只负责“做”和“报告”。

扩充建议:如果你的任务涉及多步骤的数值计算或数据清洗,可以考虑在规划阶段加入分支逻辑(例如条件判断)。Plan-and-Execute 的规划器完全可以输出包含 if-else 结构的步骤列表,只要执行器能正确解析。

[AFFILIATE_SLOT_1]

总结:原生最快,控制最稳

通过 planner_agent.py 的实现,我们再次证明了:Agent 不是黑盒——它只是 While 循环、List 数据结构和 String 拼接。没有层层封装的 overhead,系统响应速度极快,调试极其简单。

与 ReAct 相比,Plan-and-Execute 更适合那些需要高确定性的场景,比如自动化测试、流水线处理、多步骤查询等。如果你想在 Python、TypeScript 或 Go 中实现一个可靠的多步骤 Agent,这个架构是首选。

行动建议:从今天起,尝试用 Plan-and-Execute 重构你的一个 ReAct Agent。你会发现,代码不仅更清晰,而且更容易维护和扩展。

[AFFILIATE_SLOT_2]

最后,记住:框架只是工具,逻辑才是核心。无论你使用 C++、JavaScript 还是 Python,原生最快——没有层层封装的 overhead,系统响应速度极快,调试极其简单。