Agent的规划能力:分解与执行复杂任务及构建会思考的Agent
上周调试一个智能家居控制Agent,遇到一个典型问题:用户说“把客厅温度调到24度然后打开扫地机器人”,结果Agent直接调用“调温+清扫”的复合接口,忽略了扫地机器人需要先检查地面是否有障碍物的前置条件。这种场景让我重新思考:Agent面对复杂指令时,到底该直接执行,还是先拆解再规划?
规划不是简单拆句子
很多初级实现会把“先A后B”的指令切成两个独立任务,但真实场景的依赖关系要复杂得多。比如“下载最新日志并分析错误趋势”:
- 表面看是两个动作
- 实际隐含依赖:下载完成才能分析
- 还可能隐藏条件:需要确保磁盘空间>1GB才下载
早期版本我写过这样的代码:
def handle_command(command):
tasks = command.split('然后') # 字符串切分太脆弱了
for task in tasks:
execute(task) # 没有依赖检查
结果经常因为任务顺序错乱导致执行失败。这种字符串切割的方式对自然语言的理解太肤浅了。
规划的本质是状态空间搜索
后来在嵌入式调度系统中找到了灵感:复杂任务规划其实是搜索从初始状态到目标状态的动作序列。以“准备会议”为例:
目标状态:会议室已预订、投影已连接、材料已打印
初始状态:当前时间9:00,会议室占用到10:00
动作集:[预订会议室,连接投影,打印材料...]
约束:打印需要15分钟,投影连接需会议室空闲
这时候规划器需要:
- 识别打印必须在会议开始前完成
- 发现10:00前会议室不可用
- 生成序列:[10:00预订会议室,9:45开始打印,10:05连接投影]
实现时我用状态机来建模:
class TaskPlanner:
def plan(self, goal_state, current_state):
# 这里踩过坑:一定要先检查可达性
if not self.is_reachable(goal_state):
return self.suggest_alternative(goal_state) # 别直接抛异常
# 反向链式规划:从目标倒推
path = []
state = goal_state
while state != current_state:
action = self.find_action_leading_to(state) # 找能到达当前状态的动作
if not action:
break # 规划失败
path.insert(0, action) # 逆序插入
state = self.apply_action_reverse(state, action) # 反向应用动作
return path
这个反向规划的方法在资源有限的嵌入式系统里特别有用,因为可以从目标约束出发,避免探索无效分支。
执行时的动态调整
规划好的任务在执行时经常遇到意外。上周的物流Agent就遇到这种情况:
规划路径:A->B->C
执行时发现:B点临时关闭
这时候需要重新规划,但不能完全推倒重来。我的做法是局部重规划:
def execute_with_monitoring(task_sequence):
context = {}
for i, task in enumerate(task_sequence):
try:
result = execute(task, context)
context.update(result)
except ConditionChanged as e: # 环境变化
# 只重规划剩余部分
remaining = task_sequence[i:]
new_plan = replan(remaining, context, e.new_condition)
task_sequence = task_sequence[:i] + new_plan
# 注意这里要回退一步重新执行当前任务
continue
这里有个细节:重规划时要保留已执行任务产生的上下文(比如已经获取到的数据),否则会出现信息断层。
多层分解策略
复杂任务需要分层处理。我设计过的一个芯片测试Agent采用三级分解:
用户指令:“全面测试通信模块”
L1分解:射频测试 + 协议栈测试 + 压力测试
L2分解:协议栈测试 = 连接测试 + 数据传输测试 + 异常恢复测试
L3分解:连接测试 = 发起连接 + 验证握手 + 确认参数
每层使用不同的规划策略:
- L1用依赖图分析(哪些测试可以并行)
- L2用时间预估(避免超时)
- L3用实时资源分配(GPIO、DMA通道等)
代码结构大致这样:
class HierarchicalPlanner:
def decompose(self, task, level):
if level == "high":
return self.split_by_domain(task) # 按功能域切分
elif level == "middle":
return self.split_by_workflow(task) # 按工作流切分
else:
return self.split_to_atomic(task) # 拆到原子操作
def plan(self, task):
# 自顶向下分解
high_level_tasks = self.decompose(task, "high")
full_plan = []
for hl_task in high_level_tasks:
mid_level = self.decompose(hl_task, "middle")
for ml_task in mid_level:
atomic_tasks = self.decompose(ml_task, "atomic")
full_plan.extend(self.sequence_atomic_tasks(atomic_tasks))
return self.optimize_parallelism(full_plan) # 最后做并行优化
注意原子任务的识别边界:一个操作如果可能失败且需要独立回滚,就应该作为原子任务。
实现ReAct模式:一步步构建会思考的Agent
ReAct到底在解决什么?
你肯定遇到过这种场景:让Agent查“北京明天适合穿什么衣服”,它可能直接调天气API返回一串数据给你,让你自己判断。这显然不够智能。ReAct的核心思想是让Agent学会“边想边做”——先推理当前该做什么,再执行动作,观察结果,继续推理下一步。
拆开看就是Reasoning和Acting的交替循环。这个模式特别适合需要多步骤决策的场景,比如故障诊断、复杂查询、动态规划类任务。下面我们动手搞一个。
搭建基础思考循环
先定义Agent的思考状态机。别一上来就搞复杂的状态模式,咱们从最简单的字典开始:
class ReActAgent:
def __init__(self, tools):
self.tools = tools # 可调用的工具集
self.memory = [] # 历史记录
self.max_steps = 10 # 防死循环
def run(self, query):
# 初始提示词,这个模板我调了三天才稳定
prompt = f"""
目标:{query}
你可以使用以下工具:
{self._format_tools()}
历史记录:
{self._format_memory()}
请按以下格式回应:
思考:<你的推理>
动作:<工具名>|<输入参数>
或
最终答案:<结论>
"""
for step in range(self.max_steps):
# 调用LLM生成响应
response = self._call_llm(prompt)
if "最终答案:" in response:
return self._extract_answer(response)
# 解析动作并执行
tool_name, input_arg = self._parse_action(response)
observation = self._use_tool(tool_name, input_arg)
# 关键步骤:把本轮思考和观察记下来
self.memory.append({
"step": step,
"thought": self._extract_thought(response),
"action": (tool_name, input_arg),
"observation": observation
})
# 更新提示词,加入新的历史
prompt = self._update_prompt(prompt, observation)
return "超过最大步数,任务失败" # 这里踩过坑,必须设上限
注意那个max_steps,没它的话Agent可能陷入“思考-查询-再思考-再查询”的无限循环。特别是当工具返回的结果不明确时,LLM容易钻牛角尖。
工具调用那点坑
工具层看起来简单,但细节能坑死人。先看一个反面教材:
# 别这样写!
def call_tool(tool_name, arg):
if tool_name == "search_web":
return google_search(arg) # 可能超时、可能被封
elif tool_name == "query_db":
return db.execute(arg) # SQL注入警告!
正确的写法要包含异常处理、超时控制、结果清洗:
def safe_tool_call(tool_name, arg):
# 工具映射表,避免if-else链
tool_map = {
"search": self._safe_search,
"calculate": self._calculator,
"get_time": lambda x: datetime.now().isoformat()
}
if tool_name not in tool_map:
return f"未知工具:{tool_name}"
try:
# 关键:限制执行时间
with timeout(seconds=5):
result = tool_map[tool_name](arg)
# 结果不能太长,否则会撑爆上下文
return str(result)[:500]
except Exception as e:
# 返回错误信息供Agent推理
return f"工具执行失败:{type(e).__name__}"
这里有个经验:工具返回的观察结果要简洁但信息完整。曾经因为返回了整页HTML,Agent居然开始解析HTML标签,完全跑偏了。
提示词工程实战
ReAct的提示词需要精心设计。分享一个我目前在用的模板:
REACT_TEMPLATE = """
你是一个善于分步解决问题的助手。请交替进行思考和行动。
当前任务:{task}
可用工具:
{tool_descs}
格式要求:
1. 先写“思考:<推理过程>”
2. 再写“动作:<工具名>|<输入>”
3. 看到工具返回后,继续思考下一步
4. 当足够确信时,输出“最终答案:<答案>”
历史记录:
{history}
现在开始:
"""
注意几个要点:
- 工具描述要写清楚输入输出格式,比如“search(query: str) -> str”
- 历史记录要倒序显示最近几条,避免上下文过长
- 在最后加“现在开始:”能显著提高格式遵循率
我试过让GPT-4自己生成思考过程,但效果不稳定。后来改成要求“用中文简短思考”,反而更符合预期。
调试技巧:给Agent装个仪表盘
当Agent行为异常时,别光看日志。我写了个简单的监控装饰器:
def debug_agent(func):
def wrapper(*args, **kwargs):
print(f"[Agent调用] {func.__name__}")
print(f"输入:{kwargs.get('query', '无')}")
result = func(*args, **kwargs)
print(f"步数:{len(result['steps'])}")
for step in result['steps'][-3:]: # 只看最后三步
print(f" 思考:{step['thought'][:50]}...")
print(f" 动作:{step['action']}")
return result
return wrapper
通过观察最后几步的思考-动作对,能快速发现Agent是在原地打转还是在推进问题。常见的问题模式:
- 思考越来越长但动作不变 -> 陷入过度推理
- 动作在不同工具间跳变 -> 目标不明确
- 观察结果被忽略 -> 历史记录有问题
性能优化实战
当步数增多时,上下文长度会爆炸。我的解决方案是选择性记忆:
def compress_memory(self, memory_list):
"""只保留关键历史"""
compressed = []
for m in memory_list:
# 跳过纯查询无结果的步骤
if "未找到" in m["observation"] and "搜索" in m["action"]:
continue
# 保留有决策价值的步骤
if "确定" in m["thought"] or "选择" in m["thought"]:
compressed.append(m)
return compressed[-6:] # 最多保留6步
另一个技巧是让Agent自己总结进度。在每5步后插入一个总结步骤:
if step % 5 == 0:
summary_prompt = f"请用一句话总结当前进展:{history[-5:]}"
summary = self._call_llm(summary_prompt)
self.memory.append({"summary": summary})
这样即使处理长任务,上下文也不会无限增长。
个人经验建议
-
从简单工具开始:先实现搜索、计算、时间查询这三个基础工具,验证思考循环能跑通,再加复杂工具。
-
设置明确的停止条件:除了最大步数,还可以让Agent在连续两次动作相同时自动停止,防止死循环。
-
给工具分类:我把工具分为“信息获取类”和“动作执行类”,在提示词中明确告知Agent:前者可以多次调用,后者确认无误后再执行。
-
接受不完美:早期版本能达到70%的任务完成率就可以继续往下做,过度优化单点不如完善整体架构。
-
人机协作设计:在关键决策点让Agent给出选项,由用户选择。完全自主的Agent在复杂场景下反而不可靠。
最后说句实话:ReAct不是银弹。对于简单查询,它比直接调用工具慢;但对于需要多源信息整合、动态决策的场景,这种“慢思考”恰恰是价值所在。就像老工程师解决问题时,总会先停下来想清楚再动手——我们现在只是把这种能力赋予了机器。
更多推荐
所有评论(0)