Day 05 / 共 60 天 · 阶段1 入门与心智

Crew 组装:把员工和派工单编成团队

前两天分别认识了 Agent(员工)和 Task(派工单)。今天把它们装进一个容器——Crew。Crew 持有 agents + tasks + process(排班规矩),外加一堆团队级配置(记忆、缓存、回调、manager)。我们看它的核心字段,以及两种 process——sequential(流水线)hierarchical(组长派活)——分别怎么把任务派发出去。源码在全仓最大的 crew.py(2400 行)。

📍 你在 60 天里的位置(阶段1:入门与心智 · 共 6 天)
D01 项目全景 D02 装环境跑通 D03 Agent D04 Task D05 Crew D06 kickoff 旅程 阶段2 Agent 深入
💡 先用一个类比兜住今天 Crew 是一个项目组agents 是组员名单,tasks 是待办清单,process 是"怎么干"的规矩:sequential = 流水线,一张单子接一张,前一个的产出喂给后一个;hierarchical = 配一个组长(manager),由组长决定把哪张单子派给谁、还能追问返工。再加上团队公共资源:共享记忆(memory)、缓存(cache)、开跑前后的回调钩子。今天认识这个容器,明天(D06)看它按一下 kickoff 的完整旅程。
L01

痛点:一堆 agent 和 task 摆在那,谁来调度

🤔 痛点你造了 3 个 agent、5 个 task,然后呢?谁决定先干哪个、把上一个的结果传给下一个、任务失败了怎么办、多个任务能不能并行、团队要不要共享一份记忆?如果这些调度逻辑要你自己手写 for 循环+传参+异常处理,很快就乱成一团。
💡 本质:Crew = 团队容器 + 调度器Crew 一手持有资源(agents/tasks),一手持有调度策略(process),并提供统一入口 kickoff()。它是 FlowTrackable + BaseModelcrew.py:159)。
# crew.py:159
class Crew(FlowTrackable, BaseModel):
    """..."""
BaseModel又是 Pydantic 模型:字段自动校验、可序列化、可复制(crew.copy())。
FlowTrackable混入类,让 Crew 能被 Flow(阶段7 的事件驱动编排)追踪——即"Crew 可以作为 Flow 里的一个步骤"。呼应 D01 说的两条主线可以嵌套。
大白话Agent 是人、Task 是事,Crew 是"把人和事装在一起、并规定怎么干"的项目组。你只管填名单和待办,调度交给它。
L02

三大核心字段:agents / tasks / process

整个 Crew 的骨架就这三个(crew.py:220):

# crew.py:220
tasks: list[Task] = Field(default_factory=list)
agents: Annotated[
    list[BaseAgent],
    BeforeValidator(_resolve_agents),        # 校验时把各种 agent 引用解析成对象
] = Field(default_factory=list)
process: Process = Field(default=Process.sequential)   # ★默认流水线
tasks: list[Task]待办清单,有序。sequential 流程严格按这个顺序执行。
agents: list[BaseAgent]组员名单。注意类型是 BaseAgent(D03 的契约),所以适配的外部 agent 也能进来。
process★排班规矩,默认 Process.sequential。它的取值来自 D01 见过的 11 行枚举 process.py:只有 sequentialhierarchicalconsensual 还是 TODO)。
default_factory=list小细节:用工厂函数而非 [] 当默认,避免"可变默认参数被多个实例共享"的经典 Python 坑。
📝 process 枚举本体(就这么小)
# process.py:4
class Process(str, Enum):
    sequential = "sequential"
    hierarchical = "hierarchical"
    # TODO: consensual = 'consensual'
继承 str 让它既是枚举又能当字符串用(process="sequential" 也行)。
L03

团队级配置字段群

核心三件之外,一批字段配置"整个团队的公共行为"(crew.py:218 起):

# crew.py:218
name: str | None = Field(default="crew")
cache: bool = Field(default=True)                 # 团队级工具结果缓存(默认开)
verbose: bool = Field(default=False)              # 详细日志
memory: ... = Field(default=False, ...)           # ★共享记忆:True 用默认 Memory()
manager_llm: ... = Field(default=None, ...)       # hierarchical 时给自动 manager 用的模型
manager_agent: ... = Field(default=None, ...)     # ★自定义 manager(组长)
step_callback: ... = Field(default=None, ...)     # 每一步后的回调
task_callback: ... = Field(default=None, ...)     # 每个任务后的回调
before_kickoff_callbacks: list = Field(default_factory=list, ...)  # 开跑前钩子(D02 见过)
after_kickoff_callbacks: list = Field(default_factory=list, ...)   # 跑完后钩子
字段作用
memory=True开启团队共享记忆,agent 之间/多轮之间能记事(阶段6 深挖;注意它拖 lancedb,见 D01 惰性导入)
cache=True相同工具调用复用结果,默认开,省钱
manager_agent / manager_llmhierarchical 流程的组长(L05)
before/after_kickoff_callbacks开跑前改 inputs、跑完后改结果(D02 的 prepare_kickoff 里被调用)
step_callback / task_callback细粒度观测:每步/每任务后触发,做日志或干预
这些几乎都有默认值,最小用法(D01 那段)只填 agentstasks 就能跑。memory 默认是 False——记忆是要显式开的,呼应 D01 "重能力默认关"的一贯设计。
数据结构:Crew 持有的东西 Crew 核心三件 agents[] tasks[] process 团队级配置 memory cache manager_agent/llm before/after_kickoff_callbacks 红=必填核心,蓝=公共资源/开关,紫=hierarchical/钩子(可选)
图注:Crew = 核心三件(agents/tasks/process)+ 一圈团队级公共配置。最小用法只需前两件。
L04

sequential:最朴素的流水线

kickoff 时按 process 分流(D02 L05 见过)。sequential 的实现只有一行转发(crew.py:1475):

# crew.py:1475
def _run_sequential_process(self) -> CrewOutput:
    """Executes tasks sequentially and returns the final output."""
    return self._execute_tasks(self.tasks)      # 直接把全部任务丢给主循环
直接调 _execute_taskssequential 没有额外准备——因为每个 task 自带 agent(D04 讲过),不需要"分配执行者"这一步。主循环按 tasks 顺序一个个跑。
前一个喂后一个L06 会看到:执行时前面任务的输出被当作后面任务的 context 传入。这就是"流水线"——上一道工序的产出是下一道的原料。
大白话sequential = 排好队一个接一个干,前面的成果自动往后传。90% 的场景用它就够。
L05

hierarchical:配一个组长来派活

hierarchical 在跑之前,多一步"创建组长"(crew.py:1479 / crew.py:1484):

# crew.py:1479
def _run_hierarchical_process(self) -> CrewOutput:
    self._create_manager_agent()               # ★先造/配组长
    return self._execute_tasks(self.tasks)

# crew.py:1484
def _create_manager_agent(self) -> None:
    if self.manager_agent is not None:          # 你自带了组长
        self.manager_agent.allow_delegation = True
        manager = self.manager_agent
        if manager.tools:                       # ★边界:组长不该拿工具
            manager.tools = []
            raise Exception("Manager agent should not have tools")
    else:                                        # 没自带 → 用 manager_llm 自动造一个
        self.manager_llm = create_llm(self.manager_llm)
        i18n = get_i18n(prompt_file=self.prompt_file)
        manager = Agent(
            role=i18n.retrieve("hierarchical_manager_agent", "role"),
            goal=i18n.retrieve("hierarchical_manager_agent", "goal"),
            backstory=i18n.retrieve("hierarchical_manager_agent", "backstory"),
            tools=AgentTools(agents=self.agents).tools(),   # 给它"委派工具"
            allow_delegation=True, llm=self.manager_llm, verbose=self.verbose)
        self.manager_agent = manager
    manager.crew = self
两种来源组长要么是你传的 manager_agent,要么框架用 manager_llm 自动生成一个——所以 hierarchical 时这两个至少要给一个(否则 :699 附近会报错)。
allow_delegation=True组长的核心能力就是委派——把任务分给合适的组员。这个开关必须开。
role/goal/backstory 来自 i18n自动组长的人设从翻译文件取(hierarchical_manager_agent),所以支持多语言,且行为一致。
tools=AgentTools(...).tools()给组长的不是普通工具,而是"把活委派给某个组员"的委派工具。组长靠它调度。
manager.tools 非空则报错边界:组长不该持有执行类工具——它的职责是"派活"不是"干活"。检测到就清空并抛异常。
💡 设计取舍①:为什么组长要"没有工具、只有委派权"? 如果组长既能自己用工具干活、又能派活,它很容易"越权亲自下场",团队就退化成一个大 agent,分工失效。源码用一条硬规则——manager 不许有 tools,检测到就 raise——把组长的角色锁死在"纯调度"。这是把"职责单一"从口头约定变成代码强制的典型:宁可报错也不允许角色越界。代价是灵活性下降(你不能让组长顺手干点活),回报是 hierarchical 的行为可预测、分工清晰。
L06

_execute_tasks:两种流程共用的派发主循环

不管哪种 process,最后都汇到同一个主循环 _execute_taskscrew.py:1519)。看它的骨架:

# crew.py:1543(裁剪)
for task_index, task in enumerate(tasks):
    exec_data, task_outputs, last_sync_output = prepare_task_execution(
        self, task, task_index, start_index, task_outputs, last_sync_output)
    if exec_data.should_skip:                       # 断点恢复时跳过已完成的
        continue
    if isinstance(task, ConditionalTask):           # 条件任务:不满足就跳过
        skipped = self._handle_conditional_task(...)
        if skipped: task_outputs.append(skipped); continue

    if task.async_execution:                        # ★异步任务:丢线程池,先不等
        context = self._get_context(task, [last_sync_output] if last_sync_output else [])
        future = task.execute_async(agent=exec_data.agent, context=context, tools=exec_data.tools)
        futures.append((task, future, task_index))
    else:                                            # 同步任务
        if futures:                                  # 先把攒着的异步任务结果收回来
            task_outputs.extend(self._process_async_tasks(futures, was_replayed)); futures.clear()
        context = self._get_context(task, task_outputs)   # ★前面任务的输出当上下文
        task_output = task.execute_sync(agent=exec_data.agent, context=context, tools=exec_data.tools)
        task_outputs.append(task_output)
        self._process_task_result(task, task_output)
if futures:
    task_outputs.extend(self._process_async_tasks(futures, was_replayed))
return self._create_crew_output(task_outputs)        # ★汇成 CrewOutput
prepare_task_execution每个任务执行前先"备料":确定用哪个 agent(sequential 用 task.agent,hierarchical 由 manager 定)、准备工具、判断要不要跳过。
_get_context(task, task_outputs)★流水线的灵魂:把已完成任务的输出组装成 context 传给当前任务。呼应 D04 的 context 字段。
task.execute_sync(...)调用 D04 讲过的执行入口,把活真正交给 agent。返回 TaskOutput 收集起来。
async_execution 分支异步任务不阻塞:先 execute_async 拿 Future 攒着,等遇到下一个同步任务或全部结束时统一收割(_process_async_tasks)。阶段3 D17 深挖。
ConditionalTask条件任务:根据前面结果判断要不要跑,跳过则记一个"跳过输出"。阶段3 D18 讲。
_create_crew_output★所有 TaskOutput 收齐 → 汇成一个 CrewOutput 返回。这就是 D02 见过的最终返回值的来源。
控制流:_execute_tasks 主循环 取下一个 task 备料:定 agent / 判断跳过 / 取上下文 同步 execute_sync → 收 TaskOutput 异步 execute_async → 攒 Future 循环回到顶部;异步 Future 在遇到同步任务/结束时收割 全部完成 → CrewOutput
图注:主循环逐个处理 task,同步立即执行、异步攒着并发;前序输出经 context 流向后序任务。
L07

边界 + 今日小结

⚠️ 边界:hierarchical 忘了给 manager,会怎样? 选了 process=Process.hierarchical,却既没给 manager_agent 也没给 manager_llm,源码在校验阶段就报错(crew.py:699 附近):"Attribute manager_llm or manager_agent is required when using hierarchical process."。另一个坑(L05 已见):给的 manager_agent 带了 tools → 直接 raise Exception("Manager agent should not have tools")。还有 crew.py:708:如果你把 manager_agent 也塞进了 agents 列表里(重复),同样会报错——组长不能同时是普通组员。记住:hierarchical 三条铁律——必须有 manager、manager 不能带工具、manager 不能进 agents 名单。
💡 设计取舍②:两种 process 为什么共用 _execute_tasks,只在"谁来定 agent"上分叉? sequential 和 hierarchical 看似差别很大,但源码没有为它们各写一套循环。区别被收敛到一个点:任务的执行者从哪来——sequential 用 task.agent(你预先指定),hierarchical 由 manager 动态委派。其余(上下文传递、异步收割、结果汇总、条件/断点处理)完全复用同一个 _execute_tasks。好处:调度逻辑只有一份,改一处两种流程都受益,bug 面小。代价是 _execute_tasks 里要兼容两种"agent 来源",读起来分支多一点。这是"把差异下沉到最小的一个决策点、其余共享"的经典抽象手法。

👶 小白:我该用 sequential 还是 hierarchical?

👨‍🏫 老师:任务顺序你心里清楚、能一条条排好 → sequential(简单、可控、省一个 manager 的模型开销)。任务怎么分配需要"临场判断"、或者你希望有个组长统筹调度和返工 → hierarchical。新手先用 sequential 打通,理解了再上 hierarchical。阶段4(D20/D21)会把两者掰开揉碎。

🧠 今天你应该能回答

  • Crew 的三大核心字段?(agents / tasks / process)
  • process 有哪几种?默认哪种?(sequential 默认 / hierarchical;consensual 还是 TODO)
  • memory 默认开吗?(不开,False,要显式开——重能力默认关)
  • sequential 怎么把结果往后传?(前序 TaskOutput 经 _get_context 变成后序任务的 context)
  • hierarchical 的 manager 从哪来、有什么限制?(自带或 manager_llm 自动造;不能有工具、不能进 agents 名单、必须存在)
  • 两种 process 共用什么?(_execute_tasks 主循环,只在"谁来定执行 agent"上分叉)

✋ 10 分钟动手

# 1. 看核心字段与两种 process 实现
sed -n '218,257p' lib/crewai/src/crewai/crew.py
sed -n '1475,1509p' lib/crewai/src/crewai/crew.py

# 2. 看派发主循环
sed -n '1543,1588p' lib/crewai/src/crewai/crew.py

# 3. 故意触发 hierarchical 的边界报错,加深印象
uv run python -c "
from crewai import Agent, Task, Crew, Process
a=Agent(role='R', goal='G', backstory='B', llm='gpt-4o')
t=Task(description='do X', expected_output='Y', agent=a)
# 没给 manager_llm/manager_agent,看它报什么
Crew(agents=[a], tasks=[t], process=Process.hierarchical).kickoff()
"
明日预告 · Day 06:阶段1 收官!明天把 D01~D05 全部串起来——完整走一遍 crew.kickoff() 的旅程:从 prepare_kickoff → 分流 process → _execute_tasks 派发 → task.execute_sync → agent.execute_task → executor 循环 → 打包 TaskOutput → 汇成 CrewOutput。一张大图看懂"按一下按钮,数据在五大主角间怎么流动"。
← Day 04 Task 是什么 Day 06 · 一次 kickoff 旅程 →