条件任务:让某个任务"看情况再决定跑不跑"
阶段3 最后一站。真实流程里常有"看情况"的分支:分数低于 60 才触发补救任务、搜到结果才做深挖、上一步失败才走兜底。ConditionalTask 就是"带一个开关的任务"——执行前先看上一步产出,条件满足才跑、不满足就跳过(但留个空占位,保持链条完整)。今天读透它的实现,源码在 tasks/conditional_task.py、crews/utils.py、crew.py,收尾时回顾整个阶段3。
痛点:不是每个任务都该无脑执行
condition 函数。执行到它时,Crew 先把上一个任务的产出喂给这个函数,返回 True 才真跑,False 就跳过——但跳过不是"消失",而是留一个空产出占位,让后面的任务链不断裂。继承 Task,加一个 condition 字段
ConditionalTask 直接继承 Task,只多一个 condition(tasks/conditional_task.py:14、tasks/conditional_task.py:28):
# tasks/conditional_task.py:14
class ConditionalTask(Task):
"""A task that can be conditionally executed based on the output of another task.
Notes:
- Cannot be the only task in your crew
- Cannot be the first task since it needs context from the previous task
"""
condition: SerializableCallable | None = Field(
default=None,
description="Function that determines whether the task should be executed "
"based on previous task output.",
)
def __init__(self, condition=None, **kwargs) -> None:
super().__init__(**kwargs) # 先按普通 Task 造好(继承全部字段/校验)
self.condition = condition # 再挂上开关函数
condition=None,为 None 就无脑跑。但 CrewAI 选择独立子类,好处有二:① Crew 能用 isinstance(task, ConditionalTask) 一眼区分它需要特殊处理(L05);② 能对它施加专属约束(不能当第一个、不能异步,L06)而不污染普通 Task。代价是多一个类。"用类型表达一类特殊行为,让特殊逻辑有明确的挂载点"——这比在一个大类里堆 if 更清晰。should_execute:把开关拨一下
判断跑不跑的方法是 should_execute(tasks/conditional_task.py:41)——它把上一步产出喂给你的 condition 函数:
# tasks/conditional_task.py:41
def should_execute(self, context: TaskOutput) -> bool:
"""Determines whether the conditional task should be executed
based on the provided context."""
if self.condition is None:
raise ValueError("No condition function set for conditional task")
return bool(self.condition(context)) # ★把上一步产出丢进 condition,取布尔结果
context: TaskOutput注意参数就是上一个任务的 TaskOutput(D14 那个三副面孔的对象),不是拼好的字符串。你的 condition 可以读 context.raw / context.pydantic 等做判断。if self.condition is None: raise边界:条件任务却没给 condition 函数——没法判断,直接报错。bool(self.condition(context))调用你的开关函数,结果强转布尔。True = 跑,False = 跳过。def has_results(out: TaskOutput) -> bool: return len(out.raw) > 50——上一步产出超过 50 字才认为"搜到了东西"。ConditionalTask(description="深度分析搜索结果", condition=has_results, agent=analyst)。上一步只回了"未找到相关信息"(12 字)→
should_execute 返回 False → 分析任务跳过。跳过时:留一个空 TaskOutput 占位
关键设计:跳过不是让任务凭空消失,而是产出一个"空壳" TaskOutput(tasks/conditional_task.py:57):
# tasks/conditional_task.py:57
def get_skipped_task_output(self) -> TaskOutput:
"""Generate a TaskOutput for when the conditional task is skipped."""
return TaskOutput(
description=self.description,
raw="", # ★空文本:表示"没产出"
agent=self.agent.role if self.agent else "",
output_format=OutputFormat.RAW,
)
task_outputs 这个列表串起来的:后面任务的上下文(D16 _get_context)、最终产出的挑选(_create_crew_output)都按这个列表来。如果跳过的任务什么都不留,列表就会"缺一格",下游按索引/位置的逻辑可能错乱。留一个 raw="" 的空壳,等于说"这一格我来过、但没干活"——链条完整,语义清晰。D16 讲过 aggregate_raw_outputs 拼接时空 raw 拼进去也无害(就是段空串)。Crew 里的跳过决策
Crew 顺序循环里,碰到 ConditionalTask 会先走 _handle_conditional_task(crew.py:1590),它调 check_conditional_skip(crews/utils.py:186):
# crew.py:1590
def _handle_conditional_task(self, task, task_outputs, futures, task_index, was_replayed):
if futures: # ★先把在跑的异步任务收割掉(D17)
task_outputs.extend(self._process_async_tasks(futures, was_replayed))
futures.clear()
return check_conditional_skip(self, task, task_outputs, task_index, was_replayed)
# crews/utils.py:186
def check_conditional_skip(crew, task, task_outputs, task_index, was_replayed):
previous_output = task_outputs[-1] if task_outputs else None # ★只看"上一个"产出
if previous_output is not None and not task.should_execute(previous_output):
crew._logger.log("debug", f"Skipping conditional task: {task.description}", ...)
skipped_task_output = task.get_skipped_task_output() # 造空占位
if not was_replayed:
crew._store_execution_log(task, skipped_task_output, task_index)
return skipped_task_output # 返回空占位 = "跳过了"
return None # 返回 None = "该执行,照常跑"
先收割 futures★呼应 D17:判断条件要看"上一个产出",若前面有异步任务还没收割,就拿不到真正的上一步结果。所以先把异步全 result() 回来。task_outputs[-1]条件只依据紧邻的上一个产出判断(不是全部历史)。返回空占位 vs 返回 None巧妙的信号:返回空占位对象=已决定跳过(主循环把它加进列表、continue);返回 None=不跳过(主循环照常 execute_sync)。用"返回值是不是 None"传递决策。三条硬约束
Crew 对 ConditionalTask 有三条 after 校验(构造 crew 时就检查):
# crew.py:790 —— ① 不能当第一个任务
@model_validator(mode="after")
def validate_first_task(self) -> Crew:
if self.tasks and isinstance(self.tasks[0], ConditionalTask):
raise PydanticCustomError("invalid_first_task",
"The first task cannot be a ConditionalTask.", {})
return self
# crew.py:774 —— ② 不能是唯一/全部(至少要有一个非条件任务)
@model_validator(mode="after")
def validate_must_have_non_conditional_task(self) -> Crew:
if not self.tasks: return self
non_conditional_count = sum(
1 for task in self.tasks if not isinstance(task, ConditionalTask))
if non_conditional_count == 0:
raise PydanticCustomError("only_conditional_tasks",
"Crew must include at least one non-conditional task", {})
return self
# crew.py:800 —— ③ 不能是异步
@model_validator(mode="after")
def validate_async_tasks_not_async(self) -> Crew:
for task in self.tasks:
if task.async_execution and isinstance(task, ConditionalTask):
raise PydanticCustomError("invalid_async_conditional_task",
"Conditional Task: {description}, cannot be executed asynchronously.",
{"description": task.description})
return self
task_outputs 为空)。② 不能是唯一/全部条件任务:若全是条件任务,可能全被跳过,crew 最后一个产出都没有——_create_crew_output 会因"没有有效产出"报错。所以至少留一个"铁定会跑"的普通任务兜底。③ 不能异步:D17 说过异步任务的收割由后面同步任务触发、时机微妙;而条件任务的"跳过决策"必须在确定的时刻同步做出(要先拿到上一步结果)。异步 + 条件跳过混在一起,收割/判断顺序会纠缠不清。三条约束都是为了保证"条件判断时,它依赖的上一步产出一定已经确定"。和 Flow 的 router 有什么不同
👶 小白:这个"看条件决定跑不跑",和别的框架的路由/分支有啥区别?
👨🏫 老师:ConditionalTask 是 Crew(顺序流程)里的轻量分支——它只能表达"这一步跑 or 跳过",不能改变整体走向(不能"跳到第 5 个任务"或"回到第 2 个")。它本质是线性链条上的一个可选节点。如果你要真正的路由/循环/多路分叉(根据结果去 A 或 B、绕回去重来),那是 CrewAI Flow 的 @router 领域(S7 阶段,D44 会讲)。一句话:ConditionalTask 管"跳过某一步",Flow router 管"改变整个流向"。简单分支用前者,复杂编排用后者。
阶段3 收官 + 动手
🎓 阶段3(D13-18)知识地图:你已经把 Task 拆透了
- D13 模型:Task 是声明式配方卡,字段分 5 组,id(身份)/key(内容指纹),NOT_SPECIFIED 三态埋点。
- D14 输出与护栏:TaskOutput 三副面孔(raw/pydantic/json_dict);护栏是质检员,(bool,Any) 契约,带反馈重试。
- D15 结构化输出:output_pydantic/json/response_model 三种;convert_to_model 的降级链;解析失败优雅降级。
- D16 context 依赖:NOT_SPECIFIED 三态语义;_get_context 分流;raw 用分隔线拼接;不许依赖未来任务。
- D17 异步任务:execute_async 开线程 + copy_context;Future 装结果/异常;攒批-同步屏障-收割;最多一个异步收尾。
- D18 条件任务:继承 Task 加 condition;跳过留空占位保持链条;三条约束都为"条件判断时上一步已确定"。
贯穿阶段3 的两条主线:① fail-fast vs 优雅降级——配置矛盾(互斥、未来依赖、异步收尾)在构造时就报错;LLM 不配合(解析失败、护栏不过)则重试/降级。② 声明式——你只声明"要什么、依赖谁、跑不跑、并不并行",搬运/调度/解析全由框架接管。
🧠 今天你应该能回答
- ConditionalTask 比普通 Task 多了什么?为什么用子类而非加字段?
should_execute的参数是什么?拿它做什么判断?- 跳过时为什么要造一个空 TaskOutput,而不是什么都不留?
- Crew 判断条件前为什么要先收割异步任务?
- 三条硬约束(不能第一个/不能全是/不能异步)背后的共同道理是什么?
- ConditionalTask 和 Flow 的 router 有何本质区别?
✋ 10 分钟动手
P=lib/crewai/src/crewai
# 1. ConditionalTask 全文(很短,通读)
sed -n '1,69p' $P/tasks/conditional_task.py
# 2. Crew 里的跳过决策
sed -n '1590,1605p' $P/crew.py
sed -n '186,217p' $P/crews/utils.py
# 3. 三条硬约束
sed -n '773,812p' $P/crew.py