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

项目全景:CrewAI 这套代码长什么样

欢迎来到 CrewAI 60 天源码深度之旅。第一天不写代码、不背 API,只做一件事:把整个仓库在脑子里画出一张地图。CrewAI 是一个"多智能体协作"框架——你给它几个有角色的 AI(Agent),派给它们几件活(Task),把它们编成一个团队(Crew),然后一声 kickoff() 让团队跑起来。今天我们就从最外层的目录结构,一路看到 __init__.py 到底把哪些类摆在了门口。

📍 你在 60 天里的位置(阶段1:入门与心智 · 共 6 天)
D01 项目全景 D02 装环境跑通 D03 Agent D04 Task D05 Crew D06 kickoff 旅程 阶段2 Agent 深入
💡 先用一个类比兜住今天 把 CrewAI 想成一家小型广告公司Agent 是员工(每人一个岗位/角色);Task 是派工单(一件要交付的活);Crew 是这个项目组(把员工和工单编在一起);Process 是排班规矩(是流水线一个接一个,还是有个组长派活);Flow 是更上层的"事件驱动流程编排"(哪个环节做完触发下一环节)。今天先认全这几位主角住在仓库的哪个房间,后面 59 天再逐个深挖。
L01

痛点:为什么需要"多智能体"这样一个框架

🤔 痛点你已经会用一个大模型(LLM)聊天、写代码了。但真做一个稍复杂的自动化——比如"调研某行业 → 写分析报告 → 再润色成新闻稿"——用一个巨大的 prompt 硬塞给一个模型,会出现:① 角色混乱(一会儿是研究员一会儿是编辑);② 步骤没法拆开重试;③ 工具、记忆、上下文全揉在一起难维护。你需要的是把大任务拆成几个有明确职责的小角色,让它们按规矩协作
💡 本质:CrewAI = 给 LLM 加"组织结构"CrewAI 干的事,是把"一个模型干所有事"变成"一个团队分工干活"。它提供两条主线:Crew(团队)——让多个角色自主协作,偏"自治";Flow(流程)——用事件驱动精确控制每一步,偏"可控"。README 开头原话就是这么定位的(README.md:63):
# README.md:63(原文节选)
CrewAI is an open-source Python framework with high-level abstractions
and low-level APIs for building production-ready multi-agent workflows.
It gives developers autonomous agent collaboration through Crews and
precise, event-driven control through Flows.
high-level abstractions高层抽象:Agent / Task / Crew 这些"一句话能说清"的概念,让你几行代码搭出团队。
low-level APIs低层 API:需要精细控制时(自定义执行循环、工具、LLM 适配)也能钻下去。这正是 60 天要挖的部分。
Crews vs Flows★两条路线:Crew 强调"智能体自主协作",Flow 强调"事件驱动的确定性编排"。阶段7 会专门对比选型。
大白话一个人(单模型)能干活,但一个分好工的团队更靠谱、更好维护、更好调试。CrewAI 就是帮你把 LLM 组织成团队的那套脚手架。
L02

打开仓库:这是一个 monorepo(多包同仓)

克隆下来第一眼你会发现:核心代码不在根目录,而在 lib/ 下面分成了好几个独立的包。这叫 monorepo——多个能各自发布的 Python 包住在同一个 git 仓库里,用 uv workspace 统一管理。根 pyproject.toml 第一行就点明它自己只是个"工作区壳子"(pyproject.toml:1):

# pyproject.toml:1
name = "crewai-workspace"
# ...
requires-python = ">=3.10,<3.14"

# pyproject.toml:231
[tool.uv.workspace]
# members 指向 lib/ 下的各个子包
name = "crewai-workspace"根包不是你 pip install crewai 装到的那个,它只是把 lib/ 里的子包"聚在一起开发"的容器。
requires-python >=3.10,<3.14支持的 Python 版本区间。60 天里你只要有 3.10~3.13 任一个就行。
[tool.uv.workspace]★用 uv(新一代 Python 包管理器)的 workspace 机制:一次 uv sync 就把 lib 下所有子包按依赖装好、互相以本地源码引用。
💡 设计取舍①:为什么拆成 monorepo,而不是一个大包? 朴素做法是所有代码放一个 crewai 包里。CrewAI 选择拆成 crewai / crewai-core / crewai-tools / crewai-files / crewai-cli / crewai-devtools 多个包。好处:① 按需安装——只想用核心就装 crewai,要一堆现成工具才装 crewai-tools,不背无关依赖;② 解耦发布——工具库更新频繁,核心库要稳,分开就能各自迭代。代价:目录变深、新手要先建立"哪个东西在哪个包"的地图(就是今天要干的事)。这是"用一点结构复杂度,换依赖清爽和迭代自由"的典型权衡。
L03

lib/ 下六个包,各管一摊

ls lib/ 能看到六个子包。每个子包里都有自己的 pyproject.toml,第一行的 name 就是它发布到 PyPI 的名字:

# 在仓库根执行,逐个看子包名
$ for d in lib/*/; do grep -m1 '^name' "$d/pyproject.toml"; done
name = "crewai-cli"        # lib/cli
name = "crewai-core"       # lib/crewai-core
name = "crewai-files"      # lib/crewai-files
name = "crewai-tools"      # lib/crewai-tools
name = "crewai"            # lib/crewai      ← ★ 60 天主战场
name = "crewai-devtools"   # lib/devtools
包(目录)负责什么60 天里的分量
crewai(lib/crewai)框架本体:Agent / Task / Crew / Flow / LLM / 记忆 / 工具接口★★★ 绝大部分时间在这
crewai-core更底层的公共基座(版本、通用工具),被 crewai 依赖★ 偶尔溯源
crewai-tools大量现成工具(搜索、文件、爬虫等),可选安装★★ 工具系统那周
crewai-files文件输入/多模态文件处理★ 用到再看
crewai-cli(lib/cli)命令行脚手架 crewai create/run★ 收官那周
crewai-devtools开发者工具/调试辅助· 了解即可
大白话记住一句就够:学框架看 lib/crewai/src/crewai/,要现成工具看 lib/crewai-tools/其它四个是配角,用到再翻。
L04

走进 src/crewai:顶层目录导览

主战场是 lib/crewai/src/crewai/ls 一下,把关键文件/目录对号入座(这就是 60 天大纲的物理地址):

$ ls lib/crewai/src/crewai/
__init__.py      # ★ 门面:决定 import crewai 能拿到什么(L05 细看)
agent/           # Agent 本体(core.py 是主类)—— 阶段1/2
agents/          # 执行器/解析器:crew_agent_executor.py parser.py step_executor.py
task.py          # ★ Task 模型(单文件,1400+ 行)—— D04/阶段3
tasks/           # Task 的周边:task_output.py conditional_task.py guardrail...
crew.py          # ★ Crew 模型(2400 行,最大的文件)—— D05/阶段4
crews/           # crew_output.py 等结果对象
process.py       # ★ Process 枚举(就 11 行!sequential / hierarchical)
flow/            # Flow 事件驱动编排 —— 阶段7
llm.py  llms/    # LLM 抽象与各 provider 适配 —— 阶段8
memory/  rag/  knowledge/   # 记忆与知识 —— 阶段6
tools/  mcp/     # 工具系统与 MCP —— 阶段5
events/          # 事件总线/监听器(贯穿始终)
cli/  project/  security/  a2a/  telemetry/  # 生态/进阶 —— 阶段9/10
数据结构:src/crewai 目录 → 60 天阶段地图 crewai(框架本体) agent/ agents/ 阶段1·2 task.py tasks/ 阶段3 crew.py process.py 阶段4 tools/ mcp/ 阶段5 memory/ rag/ 阶段6 flow/ state/ 阶段7 llm.py llms/ 阶段8 cli/ a2a/ security/ 阶段9·10 events/ 事件总线贯穿全程(阶段4 专讲);__init__.py 是所有这些的"门面"
图注:一张图记住"哪个阶段挖哪个目录"。今天只需认路,不必读懂内容。
L05

__init__.py:import crewai 到底给了你什么

当你写 from crewai import Agent, Crew, Task,这些名字从哪来?答案是包门面 __init__.py。它开头就把主角们从各自的模块里"请到门口"(__init__.py:8):

# __init__.py:8
from crewai.agent.core import Agent          # ← 注意:Agent 在 agent/core.py,不是 agent/agent.py
from crewai.context import ExecutionContext
from crewai.crew import Crew
from crewai.crews.crew_output import CrewOutput
from crewai.flow.flow import Flow
from crewai.knowledge.knowledge import Knowledge
from crewai.llm import LLM
from crewai.llms.base_llm import BaseLLM
from crewai.process import Process
from crewai.task import Task
from crewai.tasks.task_output import TaskOutput
Agent ← agent.core★新手最常踩:源码里 没有 agent/agent.pyAgent 类真正住在 agent/core.py。大纲里写"agent/agent.py"是习惯叫法,实际以 core.py 为准(D03 会打开它)。
Crew ← crewCrew 在单文件 crew.py(2400 行,全仓最大)。
Task ← taskTask 在单文件 task.py
Flow / Process / LLM ...另一条主线 Flow、流程枚举 Process、模型抽象 LLM/BaseLLM 也都在门口就位。

接着有一个很有意思的惰性导入机制——Memory 不在开头 import,而是等你第一次访问 crewai.Memory 才加载(__init__.py:58):

# __init__.py:53
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
    "Memory": ("crewai.memory.unified_memory", "Memory"),
}

# __init__.py:58
def __getattr__(name: str) -> Any:
    """Lazily import heavy modules (e.g. Memory → lancedb) on first access."""
    if name in _LAZY_IMPORTS:
        module_path, attr = _LAZY_IMPORTS[name]
        mod = importlib.import_module(module_path)   # 用到时才真正 import
        val = getattr(mod, attr)
        globals()[name] = val                        # 缓存到模块全局,下次直接拿
        return val
    raise AttributeError(f"module 'crewai' has no attribute {name!r}")
💡 设计取舍②:为什么 Memory 要"惰性导入"? 注释说得明白:Memory → lancedb。记忆功能背后拖着 lancedb 这类重量级向量库,加载慢、依赖多。如果在 import crewai 时就 import Memory,那么每个人——哪怕根本不用记忆功能——都要为这份重依赖付启动时间。源码用模块级 __getattr__(Python 的 PEP 562)实现"按名字延迟加载":只有真的用到 crewai.Memory 时才 import,且用 globals()[name]=val 缓存避免重复。取舍点:牺牲一点"门面代码的直观性",换来所有轻量用户的秒级启动。这是库设计里非常常见的"冷启动优化"。

最后 __all__ 明确列出对外公开的名字(__init__.py:187):LLM, Agent, BaseLLM, Crew, CrewOutput, Entity, ExecutionContext, Flow, Knowledge, LLMGuardrail, Memory, PlanningConfig, Process, RuntimeState, Task, TaskOutput, __version__。这一串就是 CrewAI 官方认可的"公共 API 表面"。

L06

五大主角与它们的关系

把 L05 请出来的名字连成关系,就是整个框架的心智模型。当前版本号也在门面里写着:__version__ = "1.15.2"__init__.py:51)。

控制流:一次协作里,五大主角如何联动 Crew(团队) agents: [Agent, ...] tasks: [Task, ...] process: 排班规矩 kickoff() 一声令下 CrewOutput(最终结果) 每个 Task 派给一个 Agent 干
图注:Crew 装着 agents + tasks + process;kickoff() 按 process 调度 agent 逐个完成 task,最终汇成 CrewOutput。
📝 真实值:一段最小可运行的团队(改编自 README:511)
from crewai import Agent, Task, Crew, Process

analyst = Agent(
    role="Senior Market Analyst",
    goal="Conduct deep market analysis with expert insight",
    backstory="You're a veteran analyst known for identifying subtle market patterns")

analysis_task = Task(
    description="Analyze {sector} sector data for the past {timeframe}",
    expected_output="Detailed market analysis with confidence score",
    agent=analyst)

crew = Crew(agents=[analyst], tasks=[analysis_task],
            process=Process.sequential, verbose=True)
result = crew.kickoff(inputs={"sector": "tech", "timeframe": "1W"})  # → CrewOutput
这段代码就用到了今天认识的全部主角。明天(D02)我们真的把它跑起来。
L07

边界 + 今日小结

⚠️ 边界/坑:源码里为什么大量 try: model_rebuild() except 翻到 __init__.py:152 附近,你会看到一长串 Task.model_rebuild(...) / Crew.model_rebuild(...) 被包在一个大 try/except (ImportError, PydanticUserError) 里。这是因为这些类是 Pydantic 模型且彼此循环引用(Crew 里有 Agent、Agent 里有 crew、Task 里有 agent……)。Pydantic 需要在所有类都定义完之后,回头"重建"每个模型来解析这些前向引用(forward ref)。__init__.py 里手工触发 model_rebuild吞掉可能的异常(只打 warning,见 __init__.py:178),是为了"即使某个可选依赖缺失也别让 import crewai 直接崩"。坑点:这也意味着如果你遇到"forward ref unresolved"类报错,根源往往在导入顺序,而不是你的业务代码——记住有这么个机制,排查时就不会慌。

👶 小白:我 pip install crewai 装的,跟这个 lib/crewai 是一个东西吗?

👨‍🏫 老师:是同一份代码。你 pip 装的 crewai 包,源码就是 lib/crewai/src/crewai/。root 的 crewai-workspace 只是开发时用来同时管理多个子包的"工作区",不会被发布。所以 60 天读的都是你实际会 import 的真代码。

🧠 今天你应该能回答

  • CrewAI 解决什么问题?(把一个 LLM 干所有事 → 变成有角色分工的团队协作)
  • 它的两条主线是什么?(Crew 自主协作 / Flow 事件驱动可控)
  • 为什么是 monorepo?拆了哪几个包?(按需安装+解耦发布;crewai/core/tools/files/cli/devtools)
  • 60 天主战场目录在哪?(lib/crewai/src/crewai/
  • Agent 类的真实文件是?(agent/core.py,不是 agent.py)
  • Memory 为什么惰性导入?(它拖着 lancedb 重依赖,不用就别拖慢启动)
  • 五大主角关系?(Crew 装 agents+tasks+process,kickoff 后产出 CrewOutput)

✋ 10 分钟动手

# 1. 看清 monorepo 分了几个包
ls lib/ && for d in lib/*/; do grep -m1 '^name' "$d/pyproject.toml"; done

# 2. 数一数三大主角文件多大(感受复杂度)
wc -l lib/crewai/src/crewai/agent/core.py \
      lib/crewai/src/crewai/task.py \
      lib/crewai/src/crewai/crew.py \
      lib/crewai/src/crewai/process.py

# 3. 读门面导出的公共 API
sed -n '8,21p;187,205p' lib/crewai/src/crewai/__init__.py
明日预告 · Day 02:光看地图不过瘾。明天我们用 uv 把环境装好,配一个 LLM key,然后亲手 kickoff 今天那段最小 crew,看着它真的跑出一份分析——顺便认识 inputs 插值和 CrewOutput 的结构。
← 60 天总目录 Day 02 · 装环境 + 跑第一个 crew →