前言
计划和执行架构过将推理任务分为两个核心阶段(规划、执行)来增强推理任务。在处理需要为长期目标进行显式规划的复杂多步骤推理任务时特别有用。
由于多数模型也经常难以进行明确的长期规划,计划和执行可以通过下面方式来处理复杂问题
- 将复杂的任务分解为更小的、可管理的步骤。
- 根据中间结果动态调整。
- 必要时重新审视计划以调整或纠正步骤。
LangGraph 实现计划和执行
一个简单的计划和执行的示例
import operator
import os
import random
import subprocess
import sys
from typing import Annotated, List, Tuple, Union
from dotenv import load_dotenv
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables.graph import MermaidDrawMethod
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import create_react_agent
from pydantic import BaseModel, Field
from typing_extensions import TypedDict
def display_graph(graph, output_folder="output", ):
# Code to visualise the graph, we will use this in all lessons
mermaid_png = graph.get_graph(xray=1).draw_mermaid_png(
draw_method=MermaidDrawMethod.API
)
# Create output folder if it doesn't exist
output_folder = "."
os.makedirs(output_folder, exist_ok=True)
filename = os.path.join(output_folder, f"graph_{random.randint(1, 100000)}.png")
with open(filename, 'wb') as f:
f.write(mermaid_png)
if sys.platform.startswith('darwin'):
subprocess.call(('open', filename))
elif sys.platform.startswith('linux'):
subprocess.call(('xdg-open', filename))
elif sys.platform.startswith('win'):
os.startfile(filename)
#
load_dotenv()
llm = ChatOpenAI(model="qwen-plus",
base_url=os.getenv("BASE_URL"),
api_key=os.getenv("OPENAI_API_KEY"),
temperature=0,
streaming=True)
# Define diagnostic and action tools
# tools = [TavilySearchResults(max_results=3)] # Search tool used for subtask execution
tools = []
# Set up the model and agent executor
prompt = ChatPromptTemplate.from_messages(
[
("system", """You are a helpful assistant."""),
("placeholder", "{messages}")
]
)
# prompt.pretty_print()
agent_executor = create_react_agent(llm, tools, state_modifier=prompt, debug=False)
# Define the Plan and Execution structure
class PlanExecute(TypedDict):
input: str
plan: List[str]
past_steps: Annotated[List[Tuple], operator.add]
response: str
class Plan(BaseModel):
steps: List[str] = Field(description="Numbered unique steps to follow, in order")
class Response(BaseModel):
response: str = Field(description="Response to user.")
class Act(BaseModel):
action: Union[Response, Plan] = Field(
description="Action to perform. If you want to respond to user, use Response. "
"If you need to further use tools to get the answer, use Plan.")
# Planning step
# 对于给定的目标,制定一个简单的分步计划。
# 该计划应包括单独编号的任务,如果执行正确,将得到正确的答案。不要添加任何多余的步骤。
# 最后一步的结果应该是最终答案。确保每个步骤都有所需的所有信息,不要跳过步骤。
planner_prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"""For the given objective, come up with a simple step-by-step plan. \
This plan should involve individual numbered tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \
The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.
You must output in the following JSON format:
{{
"steps": [
"Step 1 description",
"Step 2 description",
"Step 3 description"
]
}}
Make sure to include only the JSON object with the steps array, nothing else.
""",
),
("placeholder", "{messages}"),
]
)
planner = planner_prompt | llm.with_structured_output(Plan)
# Re-planning step
# 对于给定的目标,制定一个简单的分步计划。
# 该计划应包括单独编号的任务,如果执行正确,将得到正确的答案。不要添加任何多余的步骤。
# 最后一步的结果应该是最终答案。确保每个步骤都有所需的所有信息,不要跳过步骤。
# 你的目标是 {}
# 你最初的计划是这样的:{}
# 您当前已完成以下步骤:{}
# 相应地更新你的计划。如果不需要更多步骤,并且您可以返回给用户,那么请以该步骤进行响应。否则,填写计划。只在计划中添加仍然需要完成的步骤。不要将之前完成的步骤作为计划的一部分。
replanner_prompt = ChatPromptTemplate.from_template(
"""For the given objective, come up with a simple step by step numbered plan. \
This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps. \
The result of the final step should be the final answer. Make sure that each step has all the information needed - do not skip steps.
Your objective was this:
{input}
Your original plan was this:
{plan}
You have currently done the follow steps:
{past_steps}
Update your plan accordingly. If no more steps are needed and you can return to the user, then respond with that. Otherwise, fill out the plan. Only add steps to the plan that still NEED to be done. Do not return previously done steps as part of the plan.
You must respond in one of the following JSON formats:
Option 1 - If providing final response:
{{
"action": {{
"response": "Your final response to the user"
}}
}}
Option 2 - If providing new steps:
{{
"action": {{
"steps": [
"Step 1 description",
"Step 2 description"
]
}}
}}
"""
)
replanner = replanner_prompt | llm.with_structured_output(Act)
# Execution step function
def execute_step(state: PlanExecute):
print("execute_step")
plan = state["plan"]
plan_str = "\n".join(f"{i + 1}. {step}" for i, step in enumerate(plan))
task = plan[0]
task_formatted = f"For the following plan:\n{plan_str}\n\nYou are tasked with executing step 1, {task}."
agent_response = agent_executor.invoke({"messages": [("user", task_formatted)]})
return {
"past_steps": [(task, agent_response["messages"][-1].content)],
}
# Planning step function
def plan_step(state: PlanExecute):
print("plan_step")
plan = planner.invoke({"messages": [("user", state["input"])]})
# print(f"plan_step {plan}")
return {"plan": plan.steps}
# Re-planning step function
def replan_step(state: PlanExecute):
print("replan_step")
output = replanner.invoke(state)
# If the re-planner decides to return a response, we use it as the final answer
if isinstance(output.action, Response): # Final response provided
return {"response": output.action.response} # Return the response to the user
else:
# Otherwise, we continue with the new plan (if re-planning suggests more steps)
return {"plan": output.action.steps}
# Conditional check for ending
def should_end(state: PlanExecute):
print("should_end")
if "response" in state and state["response"]:
return END
else:
return "agent"
# Build the workflow
workflow = StateGraph(PlanExecute)
# Add nodes to the workflow
workflow.add_node("planner", plan_step)
workflow.add_node("agent", execute_step)
workflow.add_node("replan", replan_step)
# Add edges to transition between nodes
workflow.add_edge(START, "planner")
workflow.add_edge("planner", "agent")
workflow.add_edge("agent", "replan")
workflow.add_conditional_edges("replan", should_end, ["agent", END])
# Compile the workflow into an executable application
app = workflow.compile()
# display_graph(app)
# Function to run the Plan-and-Execute agent
def run_plan_and_execute():
# Input from the user 格雷斯重125磅。亚历克斯的体重比格蕾丝轻2磅,还不到4倍。他们的总重量是多少磅?
inputs = {
"input": "Grace weighs 125 pounds. Alex weighs 2 pounds less than 4 times what Grace weighs. What are their combined weights in pounds?"
}
# Configuration for recursion limit
config = {"recursion_limit": 10}
# Run the Plan-and-Execute agent asynchronously
for event in app.stream(inputs, config=config):
for k, v in event.items():
if k != "__end__":
print(v)
# Run the async function
if __name__ == "__main__":
run_plan_and_execute()
运行结果
plan_step
{'plan': ["Step 1: Determine Alex's weight by multiplying Grace's weight by 4 and subtracting 2. Alex's weight = (4 * 125) - 2 = 500 - 2 = 498 pounds.", "Step 2: Add Grace's weight and Alex's weight to find their combined weight. Combined weight = 125 + 498 = 623 pounds.", 'Step 3: The result of Step 2, 623 pounds, is their combined weight.']}
execute_step
{'past_steps': [("Step 1: Determine Alex's weight by multiplying Grace's weight by 4 and subtracting 2. Alex's weight = (4 * 125) - 2 = 500 - 2 = 498 pounds.", "To execute **Step 1**, we determine Alex's weight using the formula provided:\n\n**Alex's weight = (4 × Grace's weight) - 2**\n\nGiven that **Grace's weight = 125 pounds**, we substitute into the formula:\n\n$$\n\text{Alex's weight} = (4 \times 125) - 2\n$$\n\n$$\n\text{Alex's weight} = 500 - 2\n$$\n\n$$\n\text{Alex's weight} = 498 \text{ pounds}\n$$\n\nSo, **Alex weighs 498 pounds**.")]}
replan_step
should_end
{'plan': ["Step 2: Add Grace's weight and Alex's weight to find their combined weight. Combined weight = 125 + 498 = 623 pounds.", 'Step 3: The result of Step 2, 623 pounds, is their combined weight.']}
execute_step
{'past_steps': [("Step 2: Add Grace's weight and Alex's weight to find their combined weight. Combined weight = 125 + 498 = 623 pounds.", "It looks like **Step 1** is missing from the plan. Based on the context, **Step 1** should involve identifying or obtaining the individual weights of Grace and Alex before adding them together in Step 2.\n\nHere's how **Step 1** should read:\n\n**Step 1: Identify Grace's weight and Alex's weight.** \nGrace weighs 125 pounds, and Alex weighs 498 pounds.\n\nThen you can proceed with Step 2 as given:\n\n**Step 2: Add Grace's weight and Alex's weight to find their combined weight.** \nCombined weight = 125 + 498 = 623 pounds.\n\nSo, the completed plan is:\n\n1. **Step 1:** Identify Grace's weight and Alex's weight. Grace weighs 125 pounds, and Alex weighs 498 pounds. \n2. **Step 2:** Add Grace's weight and Alex's weight to find their combined weight. Combined weight = 125 + 498 = 623 pounds.")]}
replan_step
should_end
{'response': "Grace weighs 125 pounds. Alex weighs 2 pounds less than 4 times Grace's weight, which is (4 × 125) - 2 = 498 pounds. Their combined weight is 125 + 498 = 623 pounds."}
问题换成中文的的运行下
inputs = {
"input": "格蕾丝体重 125 磅。亚历克斯的体重比格蕾丝体重的 4 倍少 2 磅。他们两人的体重总和是多少磅?"
}
输出结果为
plan_step
{'plan': ['Step 1: 格蕾丝的体重是 125 磅。', 'Step 2: 计算亚历克斯的体重,公式为:亚历克斯的体重 = (格蕾丝的体重 × 4) - 2,即 (125 × 4) - 2 = 498 磅。', 'Step 3: 计算两人体重总和,公式为:总和 = 格蕾丝的体重 + 亚历克斯的体重,即 125 + 498 = 623 磅。']}
execute_step
{'past_steps': [('Step 1: 格蕾丝的体重是 125 磅。', 'Step 1 已执行:\n\n**格蕾丝的体重是 125 磅。**')]}
replan_step
should_end
{'plan': ['Step 2: 计算亚历克斯的体重,公式为:亚历克斯的体重 = (格蕾丝的体重 × 4) - 2,即 (125 × 4) - 2 = 498 磅。', 'Step 3: 计算两人体重总和,公式为:总和 = 格蕾丝的体重 + 亚历克斯的体重,即 125 + 498 = 623 磅。']}
execute_step
{'past_steps': [('Step 2: 计算亚历克斯的体重,公式为:亚历克斯的体重 = (格蕾丝的体重 × 4) - 2,即 (125 × 4) - 2 = 498 磅。', '执行 Step 2 的计算如下:\n\n**亚历克斯的体重 = (格蕾丝的体重 × 4) - 2**\n\n已知格蕾丝的体重是 125 磅:\n\n$$\n亚历克斯的体重 = (125 × 4) - 2 = 500 - 2 = 498 \\text{ 磅}\n$$\n\n✅ **计算结果:亚历克斯的体重是 498 磅。**')]}
replan_step
should_end
{'plan': ['Step 3: 计算两人体重总和,公式为:总和 = 格蕾丝的体重 + 亚历克斯的体重,即 125 + 498 = 623 磅。']}
execute_step
{'past_steps': [('Step 3: 计算两人体重总和,公式为:总和 = 格蕾丝的体重 + 亚历克斯的体重,即 125 + 498 = 623 磅。', '好的,我们来执行 **Step 3:计算两人体重总和**。\n\n---\n\n### 📌 已知信息:\n- 格蕾丝的体重 = 125 磅 \n- 亚历克斯的体重 = 498 磅\n\n---\n\n### ✅ 计算公式:\n$$\n总和 = 格蕾丝的体重 + 亚历克斯的体重\n$$\n\n$$\n总和 = 125 + 498\n$$\n\n---\n\n### 🧮 计算过程:\n$$\n125 + 498 = 623\n$$\n\n---\n\n### ✅ 最终结果:\n两人的体重总和是 **623 磅**。\n\n---\n\n如果你还有其他步骤需要执行,欢迎继续提供任务! 😊')]}
replan_step
should_end
{'response': '两人的体重总和是 **623 磅**。'}