ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从能聊到能干:Agent任务规划与Planner-Executor闭环

从能聊到能干:Agent任务规划与Planner-Executor闭环

做Agent 30课程开发时,我最怕的就是“第7课魔咒”——前六课大家练得兴致勃勃,到了第七课,学生的Agent开始真正面对“多步骤任务”时,突然变得又蠢又乱:要么在一个子任务里反复打转,要么把整段上下文塞进一次请求,要么生成了一堆看起来很努力但根本不可执行的步骤。第7课恰好是整个30讲系列里“从能聊到能干”的分水岭。这篇文章想跟你聊聊:我把这节课做成了一堂可落地的任务规划实操课,从原理拆解、Planner-Executor最小闭环代码,到课堂案例和评分标准,全程都是自己备课时反复验证过的方案。

如果你是正在设计Agent课程的人,可以直接把这篇文章当成备课参考;如果你是自学Agent开发的爱好者,也能沿着这条路径走一遍,知道当Agent面对复杂目标时,究竟该往哪个方向使劲。

1. 这门30讲课程走到第7课,正好是“从能聊到能干”的分水岭

1.1 前六课:让Agent先会对话、会记忆、会调用工具

Agent 30课程并不是从第1课就直接让学员写Agent。我把前六课设计成一组能力铺垫:第一课讲Agent的总体认知,第二课搭开发环境并跑通最基本的“提问-回答”循环,第三课重点练提示词设计,尤其是角色设定和输出格式控制,第四课讲上下文窗口和对话记忆的取舍,第五课进入工具调用,让Agent能查询数据库、调计算器、搜文件,第六课接入一个简单的知识库,让Agent能基于外部资料回答定向问题。

走到第六课结束时,学生的Agent已经拥有三个核心零件:能理解指令的大脑、能保存状态的工作台、能操作外部世界的几把工具。但真正的缺口也很明显:面对“帮我写一份季度复盘报告”这种需要五六个动作串联的目标,大多数学生的Agent会一股脑把所有资料塞进提示词,或者一只工具反复乱调。缺的不是零件,是“先想清楚分几步做”的规划层。于是第7课就出现了。

1.2 第7课的教学目标:一份可量化的任务规划能力

第7课我给这节课起了个内部代号叫“从指令到路线”,核心目标只有一个:让学生掌握任务分解的基本模式,并能在自己写的Agent里加入“规划-执行-反思”的最小闭环。

为了不让学生觉得这里又是一堆抽象概念,我把教学目标压缩成三条可验收的标准:

  • 能把一个模糊的、多步骤的自然语言目标,拆成3到6个边界清楚的子任务;
  • 子任务之间有正确的依赖顺序,而不是想到哪写到哪;
  • 当某个子任务执行失败时,Agent能基于失败信息调整计划,而不是原地崩溃。

设计课程时我特别有意避开“规划能力很强”这种不可评价的目标。课程验收时的作业里,学生必须提交运行日志,日志里能看到完整的任务列表、每步执行结果和失败后的再规划记录。没有日志,一律视为无效作业。这门课教的不是纸面上的方法论,而是能被日志证明的行为模式。

2. 为什么任务规划是Agent从“玩具”走向“工具”的关键动作

2.1 不做规划的Agent为什么会崩:漫游式执行的三个副作用

很多学生在前六课觉得Agent已经能用,是因为他们的测试任务都只是单步任务:查一个数据、回答一个问题、写一段文案。一旦任务变成“多步、多工具、有中间产物”的复合目标,不做规划的Agent立刻暴露问题。

第一个副作用是上下文失控。Agent如果不预先规划,会习惯性地把所有可用资料、全部对话历史一起塞进模型,很快就撞上上下文窗口的限制。就像一个人进超市,不写购物清单,只能把所有货架都逛一遍,最后购物车塞到爆。

第二个副作用是工具调用频率异常。没有规划的Agent会反复调用同一个工具来试探结果,因为它不知道哪一步才是终点。我让学生做过实验:让一个未加规划层的Agent整理一份用户反馈表格,结果它在同一个查询接口上连续调用了13次,每次只是换了个措辞问同样的问题。

第三个副作用是结果不可复现。不规划的执行过程充满了随机性,同样一个目标,跑三次可能得到三条完全不同的路径。这对真实业务是无法接受的。规划层本质上是把执行路径“显式化”,每一步做什么、输出什么、下一步依赖什么,都写成结构化数据,这样即使某一步出了问题,也能定位到具体环节。

2.2 任务分解的三种基础模式:顺序、并行、条件分叉

第7课里我把任务分解的模式总结成三种,配着案例讲,学生理解起来很快。

顺序模式是最常见的。任务B必须用到任务A的结果,所以步骤天然排成先后顺序。比如写季度复盘报告,先得拉数据,再分析数据,最后写结论。这种模式的关键是“依赖关系优先,不是习惯顺序优先”。

并行模式适合那些互相不依赖的子任务。比如写一份产品调研报告,可以同时查竞品信息、收集用户评价、整理内部数据。但在具体实现时我会提醒学生:不要真的同时开好几个并发请求,先学会把并行步骤“平铺”成列表,后续再考虑并发优化。课程阶段追求的是逻辑清晰,不是性能极致。

条件分叉模式下,后续步骤取决于前面某步的输出。负责人工审核的Agent收到一份表单,如果表单里有高风险字段就走人工复核分支,否则走自动通过分支。这种模式在手写规则的时代叫分支处理,在Agent里变成规划层根据当前状态动态生成“下一步候选集合”。

我用一个表格帮学生快速对齐三种模式:

模式适用场景必经条件典型反例
顺序模式后一步依赖前一步的输出依赖关系明确先写结论再拉数据
并行模式子任务之间无依赖结果可合并硬把相互纠缠的任务拆成并行
条件分叉下一步取决于当前结果有可检测的分支状态把分叉当顺序执行导致反复走错

2.3 规划粒度:过粗则失控,过细则低效

讲任务分解时,学生最常见的困惑是“一个子任务应该拆多细”。我给了一条经验性原则:**一个子任务应该是一个人能独立完成、且完成后有明确产出物的动作单位。**拆到太细,比如“先移动鼠标,再打开文件”,会让规划器陷入执行细节,步骤列表动辄十几行,反而拖垮效率;拆到太粗,比如“写报告”作为一个步骤,执行时模型根本不知道从哪入手,等同于没规划。

实操中我用的检验方法叫“三分钟法则”:如果Executior执行这个子任务时,预计在三次工具调用以内能完成并产生中间结果,这个粒度就是合理的。超过三次调用,说明这个步骤还有继续拆分的空间。这个法则听起来不够精确,但它特别好用,尤其给基础薄弱的学生当判断标准时,比任何泛泛而谈都有指导意义。课时里我还会拿几个不同的任务当场拆一遍,让学生自己感受粗与细的边界。

3. 最小可用规划闭环:我用二三百行Python带学生搭出Planner-Executor

3.1 数据结构设计:从哪里开始记录规划状态

这一节是第7课的动手核心。我要求学生不依赖任何现成的Agent编排框架,直接用Python标准库写一个最小的规划闭环。原因很简单:一旦用了重框架,很多关键机制就被封装在黑盒里,学生根本看不出规划状态的流转过程。自己写二三百行代码,才能撞见真实的问题。

我先带学生定义三个核心数据结构。第一个叫Step,代表一个子任务:

from dataclasses import dataclass, field from typing import List, Optional @dataclass class Step: id: str description: str status: str = "pending" # pending / running / done / failed result: Optional[str] = None retries: int = 0

第二个是Plan,它就是整个任务的“路线图”:

@dataclass class Plan: goal: str steps: List[Step] = field(default_factory=list) current_index: int = 0 def next_pending_step(self) -> Optional[Step]: for step in self.steps: if step.status == "pending": step.status = "running" return step return None def is_finished(self) -> bool: return all(s.status == "done" for s in self.steps)

不要小看这个极度简化的结构,它解决了一个关键问题:Agent终于有了“当前做到哪一步”的显式记忆。过去学生的Agent一进入多轮执行,就像没有书签的浏览器,刷新一下又回到起点。有了Plan对象,每一步的进度、结果、状态都落在确定的数据结构里。

3.2 规划、执行、反思的三个循环函数

有了数据结构,下一步是规划器。我的方案很直接:先让LLM根据目标生成步骤列表。课程里我不纠结于把规划做得又花哨又复杂,而是聚焦在有步骤列表之后,随后要如何执行、如何反馈。

def plan_goal(goal: str, llm_func) -> List[dict]: prompt = ( "请把以下目标拆解为3到6个可执行的子任务。\n" f"目标:{goal}\n" "输出JSON数组,每个元素包含description字段即可,不要输出任何多余内容。" ) raw = llm_func(prompt) # 这里要写一个容错解析函数:把LLM输出的JSON解析成列表 steps_data = parse_llm_json_array(raw) return [ {"id": f"step_{i}", "description": item["description"]} for i, item in enumerate(steps_data) ]

这段代码里注释提到的parse_llm_json_array是我专门设计的一个容错函数。因为LLM==不总是乖乖输出纯JSON,它会多写一段“以下是结果:”。这个函数要能剥离多余文字、提取JSON块,甚至修复少一个括号的常见错误。我特意把这个函数当做一个小的课堂挑战,让学生自己先写,再对照我的实现。这个环节非常考验细节,经常有学生漏掉“从JSON块开始截取”的逻辑,结果解析一直报错。

执行循环的第2版,是这一课里最重要的部分:

def run_pipeline(goal: str, llm_func, tools, max_retries=2) -> Plan: step_dicts = plan_goal(goal, llm_func) plan = Plan(goal=goal, steps=[Step(**s) for s in step_dicts]) while not plan.is_finished(): step = plan.next_pending_step() if step is None: break print(f">> 执行 {step.id}: {step.description}") prompt = ( "你是执行器。请完成这个子任务:\n" f"{step.description}\n" "你可以调用以下工具并输出工具名称和参数。如果任务已完成,请输出【完成】。" ) response = llm_func(prompt) result = execute_tool_response(response, tools) if result.get("success"): step.status = "done" step.result = result.get("output") else: step.retries += 1 if step.retries > max_retries: step.status = "failed" # 触发反思重规划 new_steps = replan(goal, plan, llm_func) if new_steps: plan.steps = new_steps else: # 允许换个措辞重试一次 retry_prompt = f"刚才的执行结果是:{result}\n请换一种方法重新调用工具。" retry_response = llm_func(retry_prompt) retry_result = execute_tool_response(retry_response, tools) # ... 同样的流程 return plan

这版代码刻意写得“朴素”,它没有令人炫目的技巧,但我发现正是这种朴素让学生的思路特别清楚:一个循环,四个状态,失败就重试,重试超过阈值就重新规划。课堂里80%的学生动手后能自己复现出类似的逻辑,这说明它的设计是合理的。

3.3 让Agent在失败后自主修正的“回退缓冲”

接下来是“反思机制”。这一个机制,是第7课远远超出其余内容的地方——Agent的规划层级能不能真正解决失败问题,靠的往往是这里的设计。

初始方案里,一整个失败就触发“全盘重新规划”,这看起来合理,但在实操中灾难性。因为重新规划往往会推翻前面已完成的子任务,导致同样的步骤反复重做。真正的解法是做局部回退:把失败点之前三步都标记为“需复核”,让Agent只针对这段区域重试或重规划。

我把它称作“回退缓冲”,这个概念来自版本控制里的回滚思想。失败不意味着全盘皆输,只意味着某个局部假设不成立。回退缓冲的实现可以简单到给Plan增加一个checkpoint字段,不断记录已成功步骤的摘要。当第N步失败时,把current_index调整到max(0, N-3),并清空这三个Step的结果,再让Planner基于这些Step的失败原因重新生成更精细的替换步骤。

这个机制我单独用一个课时后补段来讲,因为学生会发现Agent的成功率明显提升。原来一次全盘重规划能救回30%的失败任务,回退缓冲能把救回率拉到75%以上。学生在日志里看到同一段失败任务被局部修复后继续执行完整流程,通常才会真正理解“规划的粒度”是什么意思。

3.4 一次完整运行示例和中断条件

为了让学生对“什么算跑通”有清晰感知,我准备了一个迷你演示。目标设定为“统计目录下所有日志文件中的错误行数”,并限制工具只有两个:list_files和count_lines。规划器生成步骤:

  1. 列出目录下所有.log文件;
  2. 读取每个文件的错误关键字行数;
  3. 汇总得到总数。

执行器按步执行,日志会输出每一步的进度。如果第二步写错工具参数,会触发重试,两次后走回退缓冲,重新生成针对该文件的拆分步骤。课堂演示时,我会故意让一个工具降速到5秒响应,让学生看到“等待”和“卡死”之间的区别,并解释为什么要给工具调用设置超时上限,以及如何用超时作为失败条件。

中断条件是必须讲的:不是所有任务都该无限滚动下去。我规定三个硬性退出条件——所有步骤完成;超过最大重试次数;累计调用模型次数超过50次。第三种退出是为了防止学生在实验时烧掉太多调用预算,也是逼他们写出更高效的规划,而不是让Agent随机漫游到天荒地老。

4. 课堂实操案例:用规划Agent完成一次季度复盘报告

4.1 需求拆解:把一句话目标变成可执行的任务树

第7课的实操案例选得很考究:一个复杂到需要规划、但又贴近办公场景的任务——生成季度项目复盘报告。目标描述故意模糊:“根据团队最近的开发记录,生成一份季度复盘,说明进展、问题和我下一步计划。”

这个案例能炸出所有规划问题,就是因为目标里出现了三个级别的任务:读取记录、分析并决策、写文档顺序。学生刚拿到题时,很多人试图一次提示词解决全部,跑出来就是一篇毫无数据支撑的空话。

我把拆解后的参考版本以任务树的形式贴在课堂文档里,让学生对着改:

任务:生成季度项目复盘报告 ├─ 1. 收集本季度关键记录 │ ├─ 1.1 扫描本地提交记录,统计合并次数 │ └─ 1.2 汇总里程碑完成状态 ├─ 2. 数据分析 │ ├─ 2.1 识别进展亮点(完成率、平均耗时) │ └─ 2.2 识别风险点(延期、未关闭问题) ├─ 3. 生成报告大纲 │ └─ 3.1 确定结构:进展-问题-计划 ├─ 4. 分块撰写正文 │ ├─ 4.1 写进展部分 │ ├─ 4.2 写问题部分 │ └─ 4.3 写下一步计划 └─ 5. 整体校验 ├─ 5.1 核对数据一致性 └─ 5.2 统一语气与格式

这个任务树的妙处在于,每个叶子节点都对应一个或几个工具调用,而且层级分明。学生照着这个结构写Plan代码时,基本不会出现上下文塞爆的问题,因为每一阶段只需要关注局部产出的结果摘要。

4.2 学生作业里反复出现的三个典型问题

批改第7课作业时,我发现学生的规划Agent往往会犯三类典型错误,而且它们几乎每次都出现。

第一类是“把工具当秘书”。学生给规划器设计了太多无意义的工具调用,比如把“分析数据”也定义成一个工具,但工具本身只是把参数直接传给LLM,并没有真正执行任何数据分析动作。这种设计让步骤列表看起来丰富,执行质量却极差。我的建议是:工具必须有真实的外部动作,否则就不叫工具,叫花架子。

第二类是“中间产物不落地”。很多学生的步骤执行完,只是把返回的结果打印出来,并不记录到Step.result里,导致下一步执行器面对一片空白。用一个内存变量缓存中间结果,是提高多步Agent稳定性的重要习惯。

第三类是“失败后只会换提示词重试,不会换个解”。如果工具返回“未找到文件”,重试时系统提示词再怎么说“认真找”,也改变不了文件不存在的现实。真正该做的是回到规划层,把“查找文件”改成“查找同名文件或询问用户”。我在代码演示里特意放了一个这种失败日志,让学生看到重试和重新规划的分界线在哪里。

4.3 为什么这个案例适合作为第7课的课后作业

季度复盘案例能火过整个课,有一个原因:它的产出物是文本报告,学生可以直观对比“有规划”和“没规划”的质量差距。没有规划层的Agent跑出来的报告,结构松散,数据错位,前后观点矛盾;有规划层的Agent即便代码写得一般,报告至少是从分块产物拼接而来的,逻辑清晰得多。

我还给这个案例设计了一个可验证的断言:报告里出现的每个数字,都必须能在中间步骤结果里找到来源。学生写完作业,自己先用代码脚本检查一遍,再提交。这个约束保证了规划闭环真正起了作用——事实来源可追溯,而这一点正是Agent从“会聊天”变成“能交差”的重要标记。

5. 评估一道规划题做得好不好:四个维度与课程评分标准

5.1 覆盖度、顺序合理性、资源约束、风险兜底

给学生的作业打分,我不用“看上去不错”这种主观标准,而是固定看四个维度。每个维度又都有明确的不合格表现与合格表现。

维度考察点不合格表现合格表现
覆盖度目标涉及的关键动作是否齐全明显遗漏步骤,比如忘了数据来源核心步骤无遗漏,叶子节点可执行
顺序合理性步骤之间的依赖关系是否可靠先写正文后拉数据,或并行依赖有依赖关系的步骤严格先后执行
资源约束是否考虑上下文与调用成本一次把所有记录塞进单次请求分块处理,只传摘要与结论
风险兜底失败后的处理方案是否完备失败即崩溃,或反复重试同一操作有重试阈值,有局部回退方案

每项20分,总分80分;剩余20分看运行日志质量,即步骤状态的流转有没有完整记录、失败是否可回溯。这个评分标准第5课就发给学生了,所以第7课没有人来问“老师怎样才能拿高分”,因为标准已经很明确。

5.2 一套可复制的课堂验收模板

我把课堂验收做成一个四层追问模板,学生写完作业后要先对照模板自查,再提交。

第一层追问:目标是单一还是复合?如果目标是复合的,有没有形成步骤列表?第二层追问:每个步骤是否有明确的成功标准?比如“拉数据”成功不等于拿到数据,而是拿到且格式正确、字段完整。第三层追问:失败后怎么办?有没有触发重试或回退,还是直接丢弃结果继续往下走?第四层追问:最终结果能否串联到最初的原始目标?中间产物是否能作为证据保留?

这四个问题基本上过滤掉所有“假装做了规划”的代码。我还开玩笑地跟学生说:你要是能把这四个问题答清楚,以后面试讲项目经验都多了一份底气。Agent规划能力没有面试题库,但这套追问模板就是最接近实战的考察清单。

6. 备课中的经验黑白名单:哪些设计有效,哪些老师容易想偏

6.1 有效设计:小步快跑、样例驱动、留半成品

第7课备课时,我第一次试讲就发现,如果只讲原理不给代码,学生听完觉得懂了,一到作业就崩。后来改成“原理一段、代码一段、跑通再走下一段”的小步快跑结构,吸收效果明显好转。每一小节控制在20分钟以内,配套一个十几行的代码片段,节奏不会拖沓。

样例驱动也特别重要。我给每个任务分解模式都配了两个对比样例:一个错误版本,一个正确版本。错误版本的规划日志真实来自某学生作业,我会先把日志投到屏幕上让学生一起“诊断”,看问题出在哪一步。这个过程很费时间,但课堂效果非常好,因为学生会突然发现:原来自己的Agent也有同样问题,不是只有他一个人踩坑。

留半成品也是一招。我不把第7课的关键代码全部给全,而是刻意少给replan和retry那段的细节,让学生自己补齐。半成品式教学能防止学生“抄完即走”,逼着他们理解循环控制流。不过代价是课堂节奏变缓,花在答疑上的时间增加,需要预留一些弹性区间。

6.2 三种容易想偏的认知误区

作为备课复盘,我还想记录下来,第一次把自己也给困住的三个误区。

第一个误区是“把规划当成高级提示词”。刚开始我设计了超级长的规划提示模板,里面塞满角色说明、输出格式、示例。结果学生稍一微调目标,规划质量瞬间崩掉。后来我简化了规划提示,只保留结构约束,效果反而稳定。

第二个误区是“跳过执行层只练规划层”。我一度觉得任务分解本身才是第7课的重点,于是专门准备了一整节纯提示词练习。但作业统计显示,只练规划不改执行,学生的整体系统能力并没有明显提升。后来才想明白:规划的质量只有在执行时才能被真正检验,规划和执行必须作为闭环一起练,分开练都是空中楼阁。

第三个误区是“让Agent一次生成所有内容”。有学生在作业里让规划器负责大段报告的生成,一步就跨过了“分块写入”的边界。这种做法的结果跟没有规划几乎一样,因为单步生成的上下文负担和漫游式执行并无本质差异。在第7课的课堂里,我会多次强调:规划的作用是把大问题切成一串小问题,而不是把小问题强行合成一个大问题。

再回头看,第7课之所以在整个30讲里卡在“能聊到能干”的这个位置,是因为它第一次要求学生的代码在结构上同时承担判断、执行、记忆、纠错四种职责。规划是整个系统的“脊椎”,没有它,工具再多也只是堆零件。而通过一套能跑、可验证、可复盘的最小闭环,每个学生都能在运行日志里亲眼看到自己的Agent从一团乱麻变成逐步收敛的执行序列,这个过程本身就比任何理论讲解都更有说服力。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进