Day 23 / 共 60 天 · 阶段4 Crew 与流程
Planning:让团队"先开策划会,再动手干"
Day 22 里 prepare_kickoff 结尾有一句 if crew.planning: crew._handle_crew_planning()。今天就拆这个 planning=True 功能:正式执行前,框架会专门造一个"规划 agent",让它读遍所有任务、每个 agent 手里有什么工具、有什么知识,然后为每个任务生成一份详细的分步计划,再把计划追加进任务描述。这样干活的 agent 拿到任务时,等于附带了一份"操作手册"。核心在 utilities/planning_handler.py 的 CrewPlanner。
📍 你在 60 天里的位置(阶段4 Crew 与流程 · 共 8 天)
D19 Crew 全字段→
D20 顺序流程→
D21 层级+manager→
D22 kickoff 家族→
D23 规划→
D24 训练+replay→
D25 记忆开关→
D26 事件系统
💡 先用一个类比兜住今天
规划就像施工队开工前先请一位工头写"施工方案":工头(规划 agent)看一眼图纸(任务)、清点工具箱(每个 agent 的 tools)、翻翻资料库(knowledge),然后给每个工序写一张"第一步做什么、用哪个工具、然后做什么"的操作卡。卡片贴到对应任务上,工人(执行 agent)照着做,就不容易乱来、少走弯路。规划本身也是一次 LLM 调用(工头也得动脑),只不过只在开工前跑一次。
L01
痛点:agent 拿到任务容易"没头绪地乱试"
🤔 痛点你给 agent 一个任务"分析这家公司的财报",它手里有 3 个工具(搜索、计算器、读文件)。但 agent 常常一上来就东一榔头西一棒子——先乱搜一通、忘了用计算器、或者跳过关键步骤。因为它拿到的只是"目标",没有"该怎么一步步做"的路线图。能不能在动手前,先让某个"更会统筹"的角色帮它把步骤想清楚?
💡 一句话本质
Planning = kickoff 前,用一个规划 agent 为每个任务生成分步计划,追加进任务描述。它不改变执行流程(还是 Day 20/21 那套),只是在任务的
description 后面拼上一段"建议这么做"的文字。执行 agent 读任务时就顺带读到了计划。整个规划是一次性的(开跑前跑一遍),产出一个结构化的 PlannerTaskPydanticOutput(每个任务一份 plan)。大白话别把规划想得太玄。它做的事朴素到有点"土":造个 agent、让它写份计划、把计划字符串
+= 拼到任务描述后面。就这样。没有复杂的图搜索、没有强化学习。它的价值全在那份计划的内容质量(规划 agent 用了好模型 + 看到了任务和工具的全貌),而不在机制的复杂度。L02
规划在哪被触发
两处:备战末尾触发,crew 方法里承接(crews/utils.py:356 / crew.py:1417):
# crews/utils.py:356 在 prepare_kickoff 最后
if crew.planning:
crew._handle_crew_planning() # 开了 planning 才跑,且在建好 agent 之后
# crew.py:1417 crew 侧的承接方法
def _handle_crew_planning(self) -> None:
self._logger.log("info", "Planning the crew execution")
result = CrewPlanner(
tasks=self.tasks, planning_agent_llm=self.planning_llm # 用 planning_llm(D19)
)._handle_crew_planning() # ★真正干活的在 CrewPlanner(L03-L06)
... # 拿到 result 后回填(L06)
if crew.planning只有 planning=True 才规划。默认关(Day 19 字段)。在 prepare_kickoff 末尾★时机很关键:在 _interpolate_inputs(插值)和 setup_agents(建 agent)之后。因为规划要看插值后的最终任务描述和 agent 的真实工具。planning_agent_llm=self.planning_llm把 Day 19 的 planning_llm 字段传给规划器。你可以专门给规划配个便宜快的模型。CrewPlanner(...)._handle_crew_planning()crew 只是转发,真正的规划逻辑全在 CrewPlanner 类里。注意有两个同名方法
_handle_crew_planning:一个在 Crew(crew.py:1417,负责调用 + 回填),一个在 CrewPlanner(planning_handler.py:57,负责真正生成计划)。别看混了——crew 侧是"下单",CrewPlanner 侧是"做菜"。L03
CrewPlanner 与它的结构化输出模型
先看输出要长什么样(planning_handler.py:15):
# planning_handler.py:15
class PlanPerTask(BaseModel):
"""Represents a plan for a specific task."""
task_number: int = Field(description="The 1-indexed task number this plan corresponds to", ge=1)
task: str = Field(description="The task for which the plan is created")
plan: str = Field(description="The step by step plan on how the agents can execute "
"their tasks using the available tools with mastery")
class PlannerTaskPydanticOutput(BaseModel):
"""Output format for task planning results."""
list_of_plans_per_task: list[PlanPerTask] = Field(...,
description="Step by step plan on how the agents can execute their tasks ...")
# planning_handler.py:37
class CrewPlanner:
def __init__(self, tasks: list[Task], planning_agent_llm: str | BaseLLM | None = None):
self.tasks = tasks
self.planning_agent_llm = planning_agent_llm or "gpt-5.4-mini" # ★兜底默认模型
PlanPerTask★一个任务对应一份计划:task_number(第几个任务,1 起)、task(任务文本)、plan(分步计划文本)。task_number ... ge=1约束 ≥1(1-indexed)。后面回填时靠这个编号对应到哪个任务。PlannerTaskPydanticOutput★整个规划的输出:一个 list_of_plans_per_task 列表,每项是一个 PlanPerTask。这是要求规划 agent 结构化输出的目标类型。planning_agent_llm or "gpt-5.4-mini"★没传 planning_llm 就用兜底模型。规划任务不复杂,默认用个 mini 级模型省钱。💡 为什么规划输出要用 Pydantic 结构化,而不是纯文本?因为规划结果要被程序精确使用——要按
task_number 把每份 plan 对应回具体任务。如果规划 agent 返回一大段自由文本,框架就得再写正则去"猜"哪段计划属于哪个任务,脆弱又易错。用 output_pydantic=PlannerTaskPydanticOutput 强制它输出结构化列表(这是 Day 15 讲的结构化输出机制),框架直接按字段读,稳。L04
_handle_crew_planning:造一个规划 agent 去干
规划的主流程(planning_handler.py:57):
# planning_handler.py:57
def _handle_crew_planning(self) -> PlannerTaskPydanticOutput:
planning_agent = self._create_planning_agent() # ① 造规划 agent
tasks_summary = self._create_tasks_summary() # ② 把所有任务汇成一段摘要(L05)
planner_task = self._create_planner_task( # ③ 造一个"做规划"的任务
planning_agent=planning_agent, tasks_summary=tasks_summary)
result = planner_task.execute_sync() # ④ 同步执行这个规划任务(调 LLM)
if isinstance(result.pydantic, PlannerTaskPydanticOutput):
return result.pydantic # ⑤ 返回结构化计划
raise ValueError("Failed to get the Planning output")
# planning_handler.py:80 规划 agent 的人设
def _create_planning_agent(self) -> Agent:
return Agent(
role="Task Execution Planner",
goal="Your goal is to create an extremely detailed, step-by-step plan based on "
"the tasks and tools available to each agent ...",
backstory="Planner agent for crew planning",
llm=self.planning_agent_llm)
# planning_handler.py:97 规划任务本身
@staticmethod
def _create_planner_task(planning_agent: Agent, tasks_summary: str) -> Task:
return Task(
description=f"Based on these tasks summary: {tasks_summary} \n Create the most "
"descriptive plan based on the tasks descriptions, tools available, "
"and agents' goals ...",
expected_output="Step by step plan on how the agents can execute their tasks ...",
agent=planning_agent,
output_pydantic=PlannerTaskPydanticOutput) # ★强制结构化输出
_create_planning_agent★规划 agent 也是个普通 Agent!role 固定为 "Task Execution Planner",goal 是"生成极其详细的分步计划"。_create_tasks_summary把 crew 的所有任务(描述、期望输出、agent、工具、知识)拼成一大段文本喂给规划器(L05)。_create_planner_task造一个 Task:描述 = "基于这份摘要,做最详细的计划",绑定规划 agent,output_pydantic 指定结构化输出。planner_task.execute_sync()★规划本质就是执行一个普通任务——复用了 Day 13-15 的整套 Task 执行 + 结构化输出机制。规划不是特殊魔法。result.pydantic 类型检查确认拿到的是 PlannerTaskPydanticOutput,否则 raise——规划失败要显式报错,不能拿个空计划继续。💡 设计取舍①:规划为什么复用 Agent+Task,而不写一套独立逻辑?
CrewAI 大可以写个专门的"规划引擎"。但源码选择把规划表达成一个再普通不过的 Agent 执行一个 Task。好处巨大:结构化输出、工具、记忆、事件、错误处理……全部白嫖已有机制,规划代码本身不到 200 行。这是"用自己的抽象解决自己的问题"(dogfooding)——如果连规划都能用 Agent+Task 表达,说明这套抽象足够通用。代价是规划 agent 的行为受限于 Agent 的能力边界,但对"写份计划"这种任务完全够用。
L05
_create_tasks_summary:把全局信息喂给规划器
规划质量取决于喂进去什么(planning_handler.py:137):
# planning_handler.py:137
def _create_tasks_summary(self) -> str:
tasks_summary = []
for idx, task in enumerate(self.tasks):
knowledge_list = self._get_agent_knowledge(task) # 取该任务 agent 的知识源
agent_tools = (
f"[{', '.join(str(tool) for tool in task.agent.tools)}]"
if task.agent and task.agent.tools else '"agent has no tools"',
...)
task_summary = f"""
Task Number {idx + 1} - {task.description}
"task_description": {task.description}
"task_expected_output": {task.expected_output}
"agent": {task.agent.role if task.agent else "None"}
"agent_goal": {task.agent.goal if task.agent else "None"}
"task_tools": {task.tools}
"agent_tools": {"".join(agent_tools)}"""
tasks_summary.append(task_summary)
return " ".join(tasks_summary)
for idx, task in enumerate遍历每个任务,idx+1 就是后面 task_number 用的 1-based 编号。task_description / expected_output任务要干啥、期望产出啥——规划的基本依据。agent + agent_goal★告诉规划器"这活是谁干的、他的目标是什么",好让计划贴合执行者。task_tools + agent_tools★最关键:把可用工具列进去。规划器知道有哪些工具,才能写出"第一步用搜索工具查 X"这种具体到工具的计划。_get_agent_knowledge把 agent 的知识源内容也带上(有的话),让计划能利用已有知识。📝 例子:规划器看到的摘要(简化)
规划器基于此产出
Task Number 1 - 分析财报 / "agent": 分析师 / "agent_goal": 精准分析 / "agent_tools": [ReadFileTool, CalculatorTool, SearchTool]。规划器基于此产出
plan:"1. 用 ReadFileTool 读取财报 PDF;2. 用 CalculatorTool 计算同比增长率;3. 用 SearchTool 查行业均值对比;4. 综合成结论。" 这段 plan 随后被拼到任务描述后(L06)。正因为规划器"看得到工具清单",计划才能点名具体工具。L06
回填:把计划 += 进任务描述
回到 crew 侧,拿到计划后的回填(crew.py:1417):
# crew.py:1417
def _handle_crew_planning(self) -> None:
result = CrewPlanner(tasks=self.tasks, planning_agent_llm=self.planning_llm)._handle_crew_planning()
plan_map: dict[int, str] = {}
for step_plan in result.list_of_plans_per_task:
if step_plan.task_number in plan_map:
self._logger.log("warning",
f"Duplicate plan for Task Number {step_plan.task_number}, using the first plan")
else:
plan_map[step_plan.task_number] = step_plan.plan # 编号 → 计划
for idx, task in enumerate(self.tasks):
task_number = idx + 1
if task_number in plan_map:
task.description += plan_map[task_number] # ★计划拼到任务描述后面
else:
self._logger.log("warning", f"No plan found for Task Number {task_number}")
plan_map: dict[int, str]把规划结果整理成"任务编号 → 计划文本"的查找表,方便回填。if ... in plan_map: 警告★规划器万一给同一个任务出了两份计划(编号重复)→ 告警并只用第一份,不覆盖。防御 LLM 的不确定输出。task.description += plan★整个规划的落点:把计划追加到任务描述末尾。执行 agent 读任务时就一并读到计划了。else: No plan found 警告某个任务规划器漏了 → 告警但不报错:这个任务没计划,照常执行即可(降级容忍)。⚠️ 边界:规划是"就地修改"任务描述,有副作用
task.description += ... 直接改了任务对象的描述。这意味着:如果你用同一个 crew 反复 kickoff 且每次都开 planning,任务描述会被一次次追加计划、越滚越长!不过实践中 kickoff_for_each 用的是 copy() 副本(Day 22),单次 kickoff 也通常是新建 crew,所以问题不大。但你若手动复用 crew 实例,要留意这个就地修改的副作用。这也是"修改传入对象"这类副作用的典型隐患。L07
进阶:执行中"边做边修正计划"的 PlannerObserver
上面的 CrewPlanner 是"开工前一次性规划"。还有个更高级的 PlannerObserver(agents/planner_observer.py:39),做"执行中动态观察修正":
# agents/planner_observer.py:39
class PlannerObserver:
"""Observes step execution results and decides on plan continuation.
When ``observe_steps`` is enabled ..., after EVERY step execution this class:
1. Analyzes what the step accomplished
2. Identifies new information learned
3. Decides if the remaining plan is still valid
4. Suggests lightweight refinements or triggers full replanning
...
LLM resolution (magical fallback):
- If ``agent.planning_config.llm`` is explicitly set → use that
- Otherwise → fall back to ``agent.llm`` (same LLM for everything)"""
def __init__(self, agent, task=None, kickoff_input=""):
self.agent = agent
self.task = task
self.llm = self._resolve_llm()
after EVERY step★和 CrewPlanner 不同:它在每一步执行后都跑,把"刚才这步学到了啥"融进剩余计划。是"观察-修正"循环。runs on every step, including successes注释强调:它不是错误检测器——成功也照跑,目的是"把运行时观察纳入后续计划"。StepRefinement 结构化修正修正是结构化对象,直接从观察结果应用,不需要第二次 LLM 调用——省一次调用。magical fallback LLM 解析规划用哪个 LLM:优先 planning_config.llm,没设就退回 agent 自己的 llm。"零配置也能用"的兜底哲学。💡 两种规划的分工
CrewPlanner(
planning=True)= 静态、开工前一次、给每个任务贴计划,简单便宜。PlannerObserver(observe_steps)= 动态、每步后修正、能应对"计划赶不上变化",但每步多花 LLM 成本。前者是入门就能开的开关,后者是需要"边走边调整"的复杂长任务才值得开。规划的粒度越细,适应性越强,成本也越高。L08
取舍 + 今日小结
图注:planning 只在开工前插入"收集摘要→规划→回填"三步,不改变后续执行流程。
👶 小白:开了 planning 一定更好吗?
👨🏫 老师:不一定。它有成本:多一次 LLM 调用(延迟+钱),而且如果任务本身很简单,塞进去的计划反而是噪声、让任务描述变长、干扰模型。planning 真正发光的场景是:任务复杂、工具多、agent 容易迷路——这时一份好计划能显著提升成功率。简单任务别开,属于过度工程。
🧠 今天你应该能回答
- planning 在什么时机被触发?为什么必须在插值和建 agent 之后?
- 规划的产物是什么?为什么用 Pydantic 结构化而非纯文本?
- 规划 agent 和普通 agent 有区别吗?规划复用了哪些已有机制?
- 任务摘要里为什么一定要带上"可用工具"?
- 计划最终怎么作用到执行?
task.description +=有什么副作用隐患? - CrewPlanner 和 PlannerObserver 分工有何不同?
✋ 10 分钟动手
P=lib/crewai/src/crewai
sed -n '1,120p' $P/utilities/planning_handler.py # CrewPlanner 全貌
sed -n '1417,1444p' $P/crew.py # 回填逻辑
grep -n "if crew.planning" $P/crews/utils.py # 触发点
# 开 planning 跑一次,观察任务描述被追加了计划
python -c "
from crewai import Agent, Task, Crew
a=Agent(role='分析师', goal='精准分析', backstory='资深', verbose=True)
t=Task(description='用一句话分析:Python 为什么流行', expected_output='一句话', agent=a)
c=Crew(agents=[a], tasks=[t], planning=True, verbose=True) # 开规划
c.kickoff()
print('规划后任务描述:', t.description[:200])
"
明日预告 · Day 24:规划是"开工前动脑",明天讲两个"运维级"能力——
train(训练:让 crew 反复跑、收集人工反馈、给 agent 生成改进建议)和 replay(回放:从某个任务重新跑,不用从头来)。会看到 _setup_for_training、CrewTrainingHandler 和 _task_output_handler 存档机制。