1. 从“直接干”到“先想后做”:为什么复杂任务需要Plan-and-Execute Agent?
如果你用过LangChain的Agent,或者尝试过直接让大模型(LLM)去完成一个多步骤的任务,大概率会遇到这种情况:你给了一个看似明确的指令,比如“帮我分析一下这个季度的销售数据,找出问题并生成一份报告”,结果模型要么卡在某个步骤,要么输出的内容七零八落,完全不成体系。更常见的是,它可能直接开始“写报告”,却忘了先去“获取数据”和“分析数据”这两个前置步骤。这就是典型的“一步到位”思维在复杂任务面前的局限性。
传统的Agent,比如LangChain里经典的ZeroShotAgent,其工作模式可以概括为“思考一步,执行一步”。它拿到任务后,会利用LLM的推理能力,结合可用的工具(Tools),决定下一步该做什么,然后执行,再根据结果思考下一步。这种模式对于简单、线性的任务很有效,比如“查一下北京的天气”或者“计算一下15的平方根”。但当任务变得复杂、步骤之间存在依赖关系、或者需要宏观规划时,这种“走一步看一步”的方式就容易陷入局部最优,甚至迷失方向。
Plan-and-Execute Agent的核心思想,就是把“规划”和“执行”这两个认知阶段分离开来。它引入了一个“规划者”(Planner)角色,专门负责“想”。这个规划者会通盘考虑整个任务,将其分解成一个有序的、逻辑清晰的步骤列表,也就是一个“计划”。然后,另一个“执行者”(Executor)角色,负责严格地、按部就班地“干”,它只关心如何完成当前步骤,并把结果传递给下一步。这种架构,非常像我们人类处理复杂项目时的做法:先开个会定个方案(Plan),再分头去执行(Execute)。
那么,这种模式具体解决了哪些痛点呢?
第一,提升任务完成的可靠性和一致性。一个深思熟虑的计划可以避免执行过程中的逻辑混乱和步骤遗漏。比如,在数据处理的场景中,计划会明确“先清洗数据,再计算指标,最后可视化”,确保流程正确。
第二,更好地处理长上下文和复杂依赖。规划者可以一次性看到所有步骤及其关系,从而做出更优的排序。例如,它知道“安装依赖包”必须在“运行脚本”之前。
第三,便于调试和优化。因为计划和执行是分离的,当任务失败时,你可以清晰地看到是计划本身不合理(比如步骤顺序错了),还是某个具体步骤的执行出了问题(比如工具调用失败),这大大降低了排查成本。
第四,为更高级的协作模式铺平道路。理论上,规划者和执行者可以由不同能力的模型担任。比如,用一个擅长战略规划但速度慢的模型(如GPT-4)做规划者,再用一个速度快、成本低的模型(如Claude Haiku)做执行者,实现成本与效果的平衡。
接下来,我们就深入LangChain的内部,看看这个“先写计划再干活”的智能体究竟是如何构建和运作的。
2. Plan-and-Execute Agent的架构拆解:Planner与Executor如何协同工作?
要理解Plan-and-Execute Agent,我们必须先抛开对单个“智能体”的模糊印象,把它看作一个由两个核心组件构成的微型系统。这个系统的设计哲学是“各司其职,专业分工”。
2.1 核心组件一:规划者(Planner)
规划者的唯一职责,就是将用户输入的、通常比较模糊的自然语言指令,转化成一个具体的、可操作的任务计划。这个计划不是一个简单的待办清单,而是一个结构化的指令序列。在LangChain的实现中,规划者本身通常也是一个LLMChain,它接收用户输入和可用的工具描述,然后输出一个计划。
这个计划的具体格式,在LangChain的早期版本中可能是一个用特定分隔符(如\n)分隔的字符串列表。但在更成熟或定制的实现里,它很可能是一个结构化的对象,比如一个包含多个步骤的列表,每个步骤可能有id、action、input等字段。规划者输出的关键质量在于:
- 完整性:是否覆盖了完成任务所必需的所有步骤。
- 顺序性:步骤之间的依赖关系是否被正确识别和排序。
- 可执行性:每个步骤是否都能被后续的执行者利用现有工具来落实。
规划者模型的提示词(Prompt)设计至关重要。它需要清晰地被告知:“你是一个规划者,你有这些工具(Tool A, Tool B, Tool C),请为这个任务制定一个分步计划。” 提示词中通常会包含一些示例(Few-shot),教模型如何产出格式正确的计划。
2.2 核心组件二:执行者(Executor)
执行者是计划的忠实履行者。它从规划者那里拿到计划,然后从头开始,一步一步地执行。对于计划中的每一个步骤,执行者需要做的是:
- 理解当前步骤:解析步骤描述,明确这一步要做什么。
- 选择并调用工具:根据步骤要求,从可用的工具集中选出最合适的那个,并生成正确的调用参数。
- 执行并观察结果:运行工具,获取执行结果(可能是成功的数据,也可能是错误信息)。
- 传递上下文:将当前步骤的结果作为上下文,传递给下一个步骤。有些复杂的计划,后续步骤可能需要前面步骤的输出作为输入。
执行者通常也是一个Agent,比如一个ZeroShotAgent。但它和传统Agent有一个关键区别:它的目标非常聚焦,不是思考整个任务,而是思考“如何完成当前这一步”。这降低了它的认知负荷,也使得它的行为更加稳定和可预测。
2.3 协同工作流与状态管理
这两个组件是如何串联起来的呢?一个典型的工作流如下:
- 初始化:用户提交任务,系统初始化规划者和执行者,并加载所有可用工具。
- 规划阶段:将用户任务和工具描述传递给规划者。规划者LLM运行,生成一个分步计划(Plan)。
- 执行循环: a. 执行者读取计划中的第一个(或下一个)未完成步骤。 b. 执行者将当前步骤描述、之前步骤的结果(如果有)作为上下文,决定调用哪个工具及参数。 c. 系统调用工具,获得结果。 d. 执行者将结果记录到该步骤的状态中(如标记为完成,并存储输出)。 e. 检查计划是否全部完成。如果未完成,回到步骤a,处理下一个步骤;如果完成,进入下一步。
- 汇总与返回:所有步骤执行完毕后,系统将所有步骤的结果进行整理,有时可能还需要一个“总结者”模型来生成最终的用户友好答复,然后返回给用户。
在这个过程中,计划的状态管理是一个容易被忽略但极其重要的细节。我们需要一个数据结构来跟踪:计划总共有多少步?当前执行到哪一步?每一步的输入、输出、执行状态(成功/失败)是什么?在LangChain的生态中,PlanAndExecute执行器通常会与BaseChatMessageHistory等组件结合,来维护这个执行上下文。对于更复杂、可能涉及循环或条件分支的计划,则需要引入像LangGraph这样的框架来管理状态图。
理解了架构,我们就可以动手搭建一个属于自己的Plan-and-Execute Agent了。下面,我将用一个贴近实际开发的例子,带你走完全程。
3. 实战构建:手把手实现一个数据分析报告生成Agent
我们假设一个场景:你是一家电商公司的数据分析师,经常需要重复性地完成“获取、清洗、分析、报告”的数据工作流。今天,我们构建一个Agent,让它根据你的指令,自动完成“获取最近7天的订单数据,计算每日销售额和订单量,找出销售额最高和最低的那天,并用一段话总结发现”。
为了完成这个任务,我们需要为Agent配备几个工具(Tools):
get_recent_orders(days: int): 模拟从数据库获取最近N天的订单数据,返回一个包含order_id,date,amount的列表。clean_data(order_data: list): 模拟数据清洗,处理缺失值、异常值等,返回清洗后的数据。calculate_daily_metrics(cleaned_data: list): 计算每日的销售额总和和订单量,返回一个按日期汇总的字典。find_extreme_days(metrics: dict): 从每日指标中找出销售额最高和最低的日期。generate_summary(metrics: dict, extremes: tuple): 根据指标和极值日,生成一段文字总结。
3.1 第一步:环境准备与工具定义
首先,确保你的环境已安装LangChain和OpenAI(或其他你选择的LLM提供商)的包。
pip install langchain langchain-openai然后,我们定义上述工具。在真实场景中,这些工具的后端可能是数据库查询、Pandas操作或API调用。这里我们用函数模拟。
from langchain.tools import tool from typing import List, Dict, Any import random from datetime import datetime, timedelta # 工具1:获取近期订单(模拟) @tool def get_recent_orders(days: int) -> List[Dict]: """Fetches order data for the recent specified number of days.""" # 模拟生成数据 data = [] base_date = datetime.now() for i in range(days): current_date = base_date - timedelta(days=i) date_str = current_date.strftime("%Y-%m-%d") # 模拟每天1-5个订单 for j in range(random.randint(1, 5)): order_id = f"ORD-{date_str}-{j:03d}" # 模拟订单金额在50-500之间 amount = round(random.uniform(50.0, 500.0), 2) data.append({"order_id": order_id, "date": date_str, "amount": amount}) return data # 工具2:清洗数据(模拟) @tool def clean_data(order_data: List[Dict]) -> List[Dict]: """Cleans the order data, e.g., handling missing values or outliers.""" # 这里简单模拟,移除金额为0或负数的异常订单(实际中可能更复杂) cleaned = [order for order in order_data if order['amount'] > 0] # 模拟处理缺失日期,这里假设数据源可靠,跳过 print(f"Data cleaning: Removed {len(order_data) - len(cleaned)} invalid orders.") return cleaned # 工具3:计算每日指标 @tool def calculate_daily_metrics(cleaned_data: List[Dict]) -> Dict[str, Dict]: """Calculates total sales amount and order count per day.""" metrics = {} for order in cleaned_data: date = order['date'] if date not in metrics: metrics[date] = {'total_sales': 0.0, 'order_count': 0} metrics[date]['total_sales'] += order['amount'] metrics[date]['order_count'] += 1 return metrics # 工具4:找出极值日 @tool def find_extreme_days(metrics: Dict[str, Dict]) -> Dict[str, Any]: """Finds the day with the highest and lowest sales.""" if not metrics: return {"highest": None, "lowest": None} sorted_days = sorted(metrics.items(), key=lambda x: x[1]['total_sales']) lowest_day, lowest_data = sorted_days[0] highest_day, highest_data = sorted_days[-1] return { "highest": {"date": highest_day, "sales": highest_data['total_sales']}, "lowest": {"date": lowest_day, "sales": lowest_data['total_sales']} } # 工具5:生成总结 @tool def generate_summary(metrics: Dict[str, Dict], extremes: Dict[str, Any]) -> str: """Generates a textual summary based on metrics and extreme days.""" total_days = len(metrics) total_orders = sum(day['order_count'] for day in metrics.values()) total_sales = sum(day['total_sales'] for day in metrics.values()) avg_sales = total_sales / total_days if total_days > 0 else 0 summary = f"在过去{total_days}天中,共有{total_orders}笔订单,总销售额为${total_sales:.2f},日均销售额为${avg_sales:.2f}。" if extremes['highest']: summary += f" 销售额最高的一天是{extremes['highest']['date']},达到${extremes['highest']['sales']:.2f}。" if extremes['lowest']: summary += f" 销售额最低的一天是{extremes['lowest']['date']},为${extremes['lowest']['sales']:.2f}。" return summary # 将所有工具放入列表 tools = [get_recent_orders, clean_data, calculate_daily_metrics, find_extreme_days, generate_summary]3.2 第二步:构建规划者(Planner)
规划者需要一个大语言模型(LLM)和一个精心设计的提示词。我们将使用ChatOpenAI和LLMChain。
from langchain_openai import ChatOpenAI from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain.schema import SystemMessage, HumanMessage, AIMessage # 初始化LLM,这里使用gpt-3.5-turbo,成本较低且足够完成规划任务 planner_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) # 构建规划者提示词 planner_prompt = ChatPromptTemplate.from_messages([ SystemMessage(content="""你是一个任务规划专家。你的目标是将用户的复杂请求分解成一个清晰的、线性的、可执行的分步计划。 每个步骤都应该对应一个可用的工具。请只使用以下工具: {tools} 请严格按照以下格式输出计划: 1. 第一步:使用[工具名称]做[简短描述],输入参数是:[参数说明] 2. 第二步:使用[工具名称]做[简短描述],输入参数是:[参数说明] ... 例如: 用户:帮我分析上周的销售数据。 计划: 1. 第一步:使用get_recent_orders获取最近7天的订单数据,输入参数是:days=7 2. 第二步:使用clean_data清洗第一步获取的订单数据,输入参数是:order_data=第一步的输出 3. 第三步:使用calculate_daily_metrics计算清洗后数据的每日指标,输入参数是:cleaned_data=第二步的输出 4. 第四步:使用find_extreme_days从每日指标中找出销售额最高和最低的日期,输入参数是:metrics=第三步的输出 5. 第五步:使用generate_summary生成分析报告,输入参数是:metrics=第三步的输出, extremes=第四步的输出 现在,请为下面的用户请求制定计划。"""), MessagesPlaceholder(variable_name="chat_history"), HumanMessage(content="{input}") ]) # 创建规划链 planner_chain = planner_prompt | planner_llm3.3 第三步:构建执行者(Executor)
执行者我们使用LangChain标准的create_react_agent来构建,它基于ReAct范式,适合逐步执行计划。
from langchain.agents import create_react_agent, AgentExecutor from langchain.memory import ConversationBufferMemory # 为执行者准备一个独立的LLM,可以用同一个,也可以区分。 executor_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) # 执行者需要记忆来存储中间结果,我们使用一个简单的内存 memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True) # 创建执行者Agent executor_agent = create_react_agent(executor_llm, tools, verbose=True) # 创建Agent执行器 agent_executor = AgentExecutor.from_agent_and_tools( agent=executor_agent, tools=tools, memory=memory, verbose=True, handle_parsing_errors=True # 处理解析错误 )3.4 第四步:组装Plan-and-Execute工作流
现在,我们需要编写一个协调器,将规划者和执行者串联起来。这个协调器负责调用规划者生成计划,然后解析计划,一步步驱动执行者去完成。
import re class PlanAndExecuteAgent: def __init__(self, planner_chain, agent_executor): self.planner = planner_chain self.executor = agent_executor def run(self, user_input: str): print("=== 规划阶段 ===") # 1. 生成计划 plan_response = self.planner.invoke({ "input": user_input, "tools": "\n".join([f"- {tool.name}: {tool.description}" for tool in tools]) }) plan_text = plan_response.content print(f"生成的计划:\n{plan_text}") # 2. 解析计划步骤 steps = self._parse_plan(plan_text) if not steps: return "无法解析出有效的计划步骤。" print(f"\n=== 执行阶段 ===") # 3. 按顺序执行计划 intermediate_results = {} # 用于存储每一步的输出 for i, step in enumerate(steps, 1): print(f"\n--- 执行步骤 {i}: {step['description']} ---") # 构建给执行者的指令。这里的关键是:告诉执行者当前要做什么,并把之前步骤的结果作为上下文。 # 我们简单地将步骤描述和之前的结果格式化后作为输入。 context = self._build_context(intermediate_results, step) executor_input = f"当前任务:{step['description']}。请使用工具完成。上下文信息:{context}" try: result = self.executor.invoke({"input": executor_input}) output = result['output'] print(f"步骤 {i} 结果:{output}") # 存储结果,键名可以是步骤序号或工具名 intermediate_results[f"step_{i}"] = output # 特别地,如果这一步调用了工具,我们也可以按工具输出存储,方便后续步骤引用 # 这里简化处理,只按步骤存储 except Exception as e: print(f"步骤 {i} 执行出错:{e}") intermediate_results[f"step_{i}"] = f"Error: {e}" # 在实际应用中,可能需要更复杂的错误处理逻辑,比如重试或修改计划 # 4. 返回最终结果(通常是最后一步的输出) final_output = intermediate_results.get(f"step_{len(steps)}", "执行未完成") print(f"\n=== 任务完成 ===") return final_output def _parse_plan(self, plan_text: str) -> List[Dict]: """解析规划者输出的文本计划,提取步骤列表。""" steps = [] # 简单的正则匹配,匹配 "1. 第一步:使用[工具]做[事],输入是:xxx" 这种格式 pattern = r'\d+\.\s*[^:]+:使用(\w+)\s*[^,]+,输入参数是:([^\n]+)' matches = re.findall(pattern, plan_text) for match in matches: tool_name, input_desc = match # 根据工具名找到对应的工具对象,获取其描述用于构建步骤指令 tool_obj = next((t for t in tools if t.name == tool_name), None) if tool_obj: steps.append({ 'tool': tool_name, 'input_description': input_desc.strip(), 'description': f"使用工具 {tool_name},{tool_obj.description}" }) return steps def _build_context(self, results: Dict, current_step: Dict) -> str: """构建当前步骤的上下文字符串。""" context_parts = [] for step_key, result in results.items(): # 避免上下文过长,可以截断 context_parts.append(f"{step_key}的结果是:{result[:200]}...") return "; ".join(context_parts) if context_parts else "暂无之前的执行结果。" # 实例化我们的Plan-and-Execute Agent plan_execute_agent = PlanAndExecuteAgent(planner_chain, agent_executor)3.5 第五步:运行与测试
现在,让我们用一开始设定的任务来测试这个Agent。
user_query = "获取最近7天的订单数据,计算每日销售额和订单量,找出销售额最高和最低的那天,并用一段话总结发现。" final_result = plan_execute_agent.run(user_query) print(f"\n最终报告:\n{final_result}")当你运行这段代码时,会在控制台看到清晰的“规划阶段”和“执行阶段”日志。规划者会输出一个类似示例的五步计划。然后执行者会一步步调用工具:获取数据、清洗、计算指标、找极值日、生成总结。最终,你将得到一段包含关键数据的文本报告。
这个例子虽然简化,但完整展示了Plan-and-Execute Agent从设计到实现的核心流程。在实际项目中,你可能需要更健壮的计划解析器、更完善的错误处理、以及更强大的状态管理(例如使用LangGraph)。但万变不离其宗,其“先规划,后执行”的思想内核是一致的。
4. 深入原理:Plan-and-Execute与ReAct、LangGraph的对比与选型
在LangChain的Agent生态里,除了Plan-and-Execute,你肯定还听说过ReAct和LangGraph。它们之间是什么关系?又该如何选择呢?理解这一点,能帮助你在实际项目中做出更合适的技术决策。
4.1 ReAct:思考与行动的即时循环
ReAct(Reasoning + Acting)是驱动大多数基础Agent(如ZeroShotAgent)的核心范式。它的工作流在一个循环内完成:
- 思考(Think):LLM根据当前任务描述、可用工具和上一步的观察结果,思考下一步应该做什么。
- 行动(Act):LLM决定调用哪个工具,并生成调用参数。
- 观察(Observe):执行工具,获得结果(或错误)。
- 将观察结果作为新的上下文,回到步骤1,直到LLM认为任务完成(输出
Final Answer)。
优点:灵活、通用,对于动态变化的环境或需要即时调整策略的任务表现良好。缺点:对于需要长远规划的多步骤任务,容易“短视”,可能做出局部最优但全局低效的决策。同时,所有“思考”都发生在同一个LLM调用中,上下文可能变得冗长混乱。
Plan-and-Execute可以看作是对ReAct范式的一种结构化增强。它将“宏观规划”(Planning)这个高层次的“思考”剥离出来,放在最前面一次性完成。剩下的“执行”阶段,虽然内部可能还是一个ReAct循环(用于完成每个子步骤),但每个子步骤的目标非常明确且有限,从而降低了复杂度。
4.2 LangGraph:用图来定义复杂的工作流
LangGraph是LangChain中用于构建有状态、多参与者的工作流(或称“图”)的库。在LangGraph中,你可以定义多个节点(Node,可以是LLM、工具、函数等)和边(Edge,决定流程走向的条件),从而构建出带循环、分支、并行等复杂逻辑的智能体系统。
Plan-and-Execute Agent非常适合用LangGraph来实现。你可以将“规划者”和“执行者”定义为两个节点。工作流可以是:
- 开始->规划节点(生成计划)->执行节点(按计划执行步骤,本身可能是一个子图或循环)->结束。
使用LangGraph的好处是:
- 显式状态管理:整个工作流的状态(包括当前计划、已完成的步骤、中间结果)被封装在一个状态对象中,在各个节点间传递,非常清晰。
- 支持复杂逻辑:如果某个步骤执行失败,你可以很容易地通过条件边(Conditional Edge)跳转到错误处理节点或重试逻辑。
- 可视化与可调试性:LangGraph的图结构可以可视化,便于理解和调试复杂的工作流。
4.3 如何选择?一个简单的决策框架
面对一个任务,你该用基础的ReAct Agent,Plan-and-Execute,还是上LangGraph?可以参考以下思路:
- 任务简单且步骤少(<3步):直接使用ReAct Agent(如
create_react_agent)。它简单快捷,足够应付大多数一次性问答和简单操作。 - 任务步骤清晰、顺序固定、但较多(>=3步):使用Plan-and-Execute模式。它能显著提升任务完成的可靠性和可解释性。你可以用我们上面演示的方式自己组装,也可以寻找社区中更成熟的实现。
- 任务流程复杂,包含条件判断、循环或并行:使用LangGraph。例如,“监控系统日志,如果出现错误A则执行预案B,如果出现错误C则执行预案D,并循环监控”。LangGraph是描述这种复杂、有状态流程的终极工具。
注意:这三种模式不是互斥的,而是可以组合的。例如,在LangGraph构建的Plan-and-Execute工作流中,“执行者”节点本身可能就是一个ReAct Agent。选择的核心在于用合适的工具匹配任务的复杂度,避免“杀鸡用牛刀”或“小马拉大车”。
5. 避坑指南与性能优化:让Plan-and-Execute Agent真正可靠
构建一个能跑通的Demo是一回事,让它在生产环境中稳定、高效地运行是另一回事。在实际使用Plan-and-Execute Agent时,你会遇到一些典型的“坑”。下面是我从实践中总结出的关键问题和优化建议。
5.1 规划阶段的常见问题与对策
问题1:规划者生成不可执行的计划。
- 表现:计划中的步骤调用了不存在的工具,或者输入参数描述模糊,导致执行者无法理解。
- 根因:规划者LLM对可用工具的理解不足,或者提示词(Prompt)不够精确。
- 解决方案:
- 优化工具描述:确保每个
@tool装饰器下的函数文档字符串(docstring)清晰、准确,包含参数类型和示例。LLM严重依赖这些描述。 - 提供高质量示例(Few-shot):在规划者的提示词中,提供2-3个针对你常用任务类型的、完美的计划示例。这是引导LLM输出正确格式的最有效方法。
- 输出格式约束:要求规划者以严格的格式(如JSON、YAML或带编号的列表)输出。这可以通过在Prompt中指定,或者使用LangChain的
OutputParser(如PydanticOutputParser)来实现,强制结构化输出。
- 优化工具描述:确保每个
问题2:计划步骤顺序不合理或遗漏关键步骤。
- 表现:计划让执行者“计算指标”在“获取数据”之前,或者忘了“保存结果”这一步。
- 根因:任务复杂度超出规划者模型的上下文理解能力,或者领域知识不足。
- 解决方案:
- 分而治之:对于极其复杂的任务,可以考虑两级规划。第一级规划者进行粗粒度分解(如“阶段一:数据准备,阶段二:模型训练,阶段三:结果评估”),第二级规划者再对每个阶段进行细粒度规划。
- 使用更强的规划模型:如果任务至关重要,可以考虑使用能力更强的模型(如GPT-4)作为规划者。虽然成本高,但规划质量直接影响整个任务的成败,这笔投资往往是值得的。
- 人工审核或修正计划:在关键业务流中,可以引入“人工在环”(Human-in-the-loop)机制,让规划者生成的计划先经过人工确认或微调后再执行。
5.2 执行阶段的常见问题与对策
问题3:执行者无法正确解析上一步的输出作为当前步骤的输入。
- 表现:计划中写“输入参数是:第一步的输出”,但执行者LLM在调用工具时,传递的是一个无意义的字符串,而不是第一步的实际结果对象。
- 根因:我们自制的协调器在
_build_context函数中,只是简单地将之前步骤的输出结果以文本形式拼接。如果输出是复杂对象(如字典、列表),直接转换成字符串可能丢失结构信息,导致后续LLM无法正确提取所需字段。 - 解决方案:
- 结构化中间状态:不要只存储文本结果。为每个步骤定义一个明确的输出模式(Schema)。例如,
get_recent_orders步骤的输出可以定义为List[Order]类型。协调器维护一个结构化的状态字典。 - 智能参数绑定:在执行每个步骤时,协调器应能根据步骤描述(如
input_desc)和当前结构化状态,自动将正确的值绑定到工具参数上。这可能需要一个简单的模板引擎或参数解析器。例如,解析到input_desc为metrics=第三步的输出,就去状态字典里找到step_3的结果,并将其作为metrics参数的值。 - 使用LangChain Expression Language (LCEL)或LangGraph:这些高级框架内置了更完善的状态管理和数据流机制,能更好地处理步骤间的数据传递。
- 结构化中间状态:不要只存储文本结果。为每个步骤定义一个明确的输出模式(Schema)。例如,
问题4:某个步骤执行失败,导致整个任务中断。
- 表现:工具调用超时、返回错误、或LLM解析工具输出失败。
- 根因:网络问题、工具异常、或LLM的“幻觉”导致生成了不合法的工具调用。
- 解决方案:
- 重试机制:为工具调用和LLM调用增加指数退避的重试逻辑。许多HTTP客户端和LLM SDK都支持重试。
- 超时设置:为每个步骤设置合理的超时时间,防止因某个工具卡死而阻塞整个流程。
- 备选计划(Plan B):在规划时,可以让规划者思考“如果某工具失败,备用方案是什么”。或者在执行时,由协调器捕获异常,并尝试使用备用工具或跳过该步骤(如果允许)。
- 检查点(Checkpoint):定期保存执行状态。这样即使整个进程崩溃,重启后可以从最近一个成功的步骤继续,而不是从头开始。
5.3 性能与成本优化
- 策略1:模型分工,降低成本。正如之前提到的,让GPT-4做规划者(调用一次),让GPT-3.5-Turbo做执行者(可能调用多次)。规划需要深谋远虑,值得用更好的模型;执行是机械化的子任务,用性价比高的模型即可。
- 策略2:缓存规划结果。对于高频、重复的任务(如“每日销售报告”),其计划几乎是固定的。可以将规划结果缓存起来(例如,以任务指令的哈希值为Key),下次直接使用,省去一次LLM调用。
- 策略3:异步执行。如果计划中的某些步骤之间没有依赖关系,可以考虑让它们并行执行。这需要更高级的工作流引擎(如LangGraph)来支持。例如,“获取用户信息”和“获取产品列表”这两个步骤如果可以同时进行,就能缩短总执行时间。
- 策略4:精简上下文。在执行阶段,传递给执行者LLM的上下文应只包含当前步骤必需的信息,避免将整个计划历史都塞进去,导致令牌数激增、成本上升且可能影响模型性能。
构建一个健壮的Plan-and-Execute Agent是一个迭代过程。从最简单的原型开始,逐步增加错误处理、状态管理、性能优化等特性。理解其核心思想——分离关注点——能帮助你在面对各种复杂需求时,设计出清晰、可维护的智能体系统。