装环境 + 亲手跑通第一个 crew
昨天画了地图,今天把车开起来。我们要:用 uv 装好 CrewAI、配一个模型 key、把 D01 那段 30 行的最小团队真的 kickoff 跑出结果;然后借这次运行,第一次钻进源码看看——你调 crew.kickoff(inputs=...) 后,程序在真正干活之前偷偷做了哪些准备(回调、事件、{占位符} 插值),最后返回的 CrewOutput 里到底装了什么。
kickoff() 就像按下咖啡机的"开始"键。你只按了一下,但机器在出咖啡之前先做了一串准备动作:预热、检查水位、磨豆——这些就是 prepare_kickoff。而你在配方里写的 {sector} 是个"待填占位",机器开始前会把它替换成你这次真正要的"tech"——这就是 inputs 插值。今天既动手按键,也拆开看它按键前那几秒干了啥。痛点:装 AI 库最烦的是依赖冲突
pip 装经常卡在版本冲突、编译失败、装了半小时还报错。CrewAI 官方直接把这条路铺好了:它指定用 uv 来装。# README.md:194
Ensure you have Python >=3.10 <3.14 installed on your system.
CrewAI uses UV for dependency management and package handling,
offering a seamless setup and execution experience.
Python >=3.10 <3.14和昨天根 pyproject 里 requires-python 完全一致——先确认你的 Python 在这个区间。UV for dependency management★uv 是 Rust 写的 Python 包管理器,比 pip 快一个数量级,还能自动建虚拟环境、锁定版本。CrewAI 官方脚手架、CI 全用它。三行命令:从零到能 import crewai
README 给的安装命令很短(README.md:199 / README.md:205):
# 0) 如果还没有 uv:装它(官方一键脚本)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 1) 建一个项目并进入
mkdir my-first-crew && cd my-first-crew
uv venv # 建虚拟环境
source .venv/bin/activate # 激活(Windows 用 .venv\Scripts\activate)
# 2) 装 CrewAI(README:199)
uv pip install crewai
# 2') 想要一堆现成工具,就装 [tools] 附加项(README:205)
uv pip install 'crewai[tools]'
uv pip install crewai装核心框架,对应昨天说的 lib/crewai 那个包。只学源码,这一条就够。'crewai[tools]'方括号里的 tools 是"可选附加组(extras)",会顺带装上 crewai-tools(搜索/爬虫等现成工具)。依赖更重,阶段5 再装不迟。[embeddings]README:218 还提到:如果报 No module named 'tiktoken',装 crewai[embeddings]。这就是昨天说的"按需安装"——附加组把重依赖分开了。pip install crewai 默认就把搜索工具、向量库、tokenizer 全拉下来,那么只想跑个最小 demo 的人也要等几分钟、下几百 MB。CrewAI 用 extras(crewai[tools])把重依赖拆成按需选装——这与 D01 看到的"monorepo 分包"和"Memory 惰性导入"是同一条设计哲学:默认保持最轻,重的东西谁用谁装。代价是新手偶尔会遇到"少了个模块"的报错,但 README 的 Troubleshooting 直接给出了对应的 extras。配一个 LLM key(agent 得有"大脑")
Agent 靠一个大模型思考。CrewAI 默认通过 litellm 兼容各家模型,最省事的是设一个环境变量。用 OpenAI 举例:
# 方式 A:直接设环境变量
export OPENAI_API_KEY="sk-..."
# 方式 B:写进项目根的 .env 文件(CrewAI 会自动读取)
echo 'OPENAI_API_KEY=sk-...' >> .env
# 想用别家?litellm 风格的 model 名即可,比如:
# llm="gpt-4o" → 读 OPENAI_API_KEY
# llm="anthropic/claude-3-5-sonnet-latest" → 读 ANTHROPIC_API_KEY
# llm="ollama/llama3" → 本地,无需 key
llm 字段可以只写一个字符串模型名,CrewAI 内部会把字符串解析成一个 LLM 对象(阶段8 会看 llm.py 的解析逻辑)。今天用默认 gpt-4o 或你手头任意一个能用的模型都行。👶 小白:我没有任何付费 key,能跑通今天的例子吗?
👨🏫 老师:能。装个 Ollama 拉一个本地小模型(如 ollama pull llama3),然后给 Agent 传 llm="ollama/llama3",完全本地、零成本。结果质量会差些,但"跑通流程"的目的达到了。
30 行的最小 crew:写下来、跑起来
把 D01 结尾那段(改编自 README.md:511)存成 main.py,补上打印:
# main.py
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",
llm="gpt-4o", # 或 "ollama/llama3"
verbose=True)
analysis_task = Task(
description="Analyze {sector} sector data for the past {timeframe}",
expected_output="Detailed market analysis with a confidence score",
agent=analyst)
crew = Crew(
agents=[analyst],
tasks=[analysis_task],
process=Process.sequential,
verbose=True)
result = crew.kickoff(inputs={"sector": "tech", "timeframe": "1W"})
print("===== 最终结果 =====")
print(result) # 触发 CrewOutput.__str__
print("raw 长度:", len(result.raw))
# 跑它
uv run python main.py
verbose=True 刷出一堆彩色日志:Agent 在"思考"、给出 Final Answer,最后 print(result) 打出那份分析。恭喜——你的第一个多智能体团队跑起来了。剩下几讲,我们拆开这一声 kickoff()。{sector} 这种大括号是 CrewAI 的占位符,不是 Python f-string
注意 description="Analyze {sector} sector..." 没有 f 前缀。它是普通字符串,里面的 {sector} 由 CrewAI 在 kickoff 时用 inputs 替换(L06 看源码)。新手常犯两个错:① 误写成 f-string 导致 NameError: sector is not defined(因为定义 Task 时还没有值);② kickoff 时忘了传对应的 key,导致占位符没被替换、模型看到字面量 {sector}。kickoff 真正干活前,先做一串准备
crew.kickoff() 并不是立刻开始跑任务。它先调 prepare_kickoff(...),再按 process 分流(crew.py:1021):
# crew.py:1021
inputs = prepare_kickoff(self, inputs, input_files) # ① 先准备
if self.process == Process.sequential:
result = self._run_sequential_process() # ② 流水线:我们这次走这里
elif self.process == Process.hierarchical:
result = self._run_hierarchical_process() # 有组长派活(D05/阶段4 讲)
else:
raise NotImplementedError(...)
for after_callback in self.after_kickoff_callbacks: # ③ 跑完后的回调
result = after_callback(result)
result = self._post_kickoff(result)
self.usage_metrics = self.calculate_usage_metrics() # ④ 统计 token 用量
return result
准备阶段 prepare_kickoff 干的事在 crews/utils.py:249。挑三处关键动作:
# crews/utils.py:289 —— 先跑 before_kickoff 回调(可改写 inputs)
for before_callback in crew.before_kickoff_callbacks:
if normalized is None:
normalized = {}
normalized = before_callback(normalized)
# crews/utils.py:309 —— 发出"kickoff 开始"事件,记录事件 id
started_event = CrewKickoffStartedEvent(crew_name=crew.name, inputs=normalized)
crew._kickoff_event_id = started_event.event_id
future = crewai_event_bus.emit(crew, started_event)
# crews/utils.py:331 —— ★把 inputs 插进任务/agent 的文本里
crew._inputs = normalized
crew._interpolate_inputs(normalized)
before_kickoff_callbacks你注册的"开跑前钩子",可以在这里最后修改一遍 inputs(比如补默认值)。emit(CrewKickoffStartedEvent)★发一条"我要开跑了"的事件到事件总线。日志、遥测、监听器都靠它。CrewAI 全程事件驱动,阶段4 专讲。_interpolate_inputs★核心:把 {sector} 这类占位符用真实值替换。下一讲 L06 打开它。_task_output_handler.reset()(:318)清空上次运行留下的任务输出缓存,保证这次是干净的开始。kickoff 本身还要处理流式(stream)、检查点恢复(from_checkpoint)、异常兜底、token 统计等一堆事。如果把"回调+事件+插值+重置"也堆在同一个函数里,kickoff 会变成几百行的巨兽。抽出 prepare_kickoff 的好处:① kickoff 主干只剩"准备→按 process 分流→收尾"三步,一眼看懂;② 准备逻辑能被 kickoff / kickoff_async / kickoff_for_each 复用(阶段4 会看到这些变体)。代价是读代码要多跳一个文件——但换来了主流程的可读性和复用。inputs 插值:{sector} 是怎么变成 "tech" 的
L04 那个坑的答案就在这里。_interpolate_inputs 遍历所有 task 和 agent,把你传的 inputs 灌进它们的文本字段(crew.py:2098):
# crew.py:2098
def _interpolate_inputs(self, inputs: dict[str, Any]) -> None:
"""Interpolates the inputs in the tasks and agents."""
[
task.interpolate_inputs_and_add_conversation_history(inputs) # 每个任务
for task in self.tasks
]
for agent in self.agents:
agent.interpolate_inputs(inputs) # 每个 agent
对每个 task 插值把 task 的 description、expected_output 里的 {sector}/{timeframe} 替换成 "tech"/"1W"。所以模型最终看到的是"Analyze tech sector data for the past 1W"。对每个 agent 插值agent 的 role/goal/backstory 里也能写占位符,同样会被替换。让你能用同一套 agent 模板跑不同输入。列表推导 [ ... for task in ]小细节:这里用列表推导只是为了"对每个 task 都执行一次",返回值没用到(注释也说 interpolate 返回 None)。等价于普通 for 循环。add_conversation_history方法名后半段暗示:插值的同时还会拼接对话历史(多轮/对话式场景用),今天用不到,记个名。inputs={"sector":"tech","timeframe":"1W"};task.description 原本是 "Analyze {sector} sector data for the past {timeframe}"。插值后变成 "Analyze tech sector data for the past 1W"——这句才是真正送进模型的 prompt 的一部分。没传 sector 这个 key 会怎样?占位符 {sector} 原样保留,模型就会看到奇怪的字面量 {sector},输出跑偏。这正是 L04 坑②的机理。读懂返回值 CrewOutput + 今日小结
kickoff 最终返回一个 CrewOutput。它不是一个普通字符串,而是一个装了多种视图的对象(crews/crew_output.py:13):
# crews/crew_output.py:13
class CrewOutput(BaseModel):
raw: str = Field(description="Raw output of crew", default="") # 纯文本结果
pydantic: BaseModel | None = ... # 若任务设了 output_pydantic,这里是结构化对象
json_dict: dict[str, Any] | None = ... # 若设了 output_json,这里是 dict
tasks_output: list[TaskOutput] = ... # ★每个任务各自的输出(多任务时按顺序)
token_usage: UsageMetrics = ... # 本次跑掉多少 token
# crews/crew_output.py:55 —— 所以 print(result) 有优先级
def __str__(self) -> str:
if self.pydantic: return str(self.pydantic) # 有结构化就打结构化
if self.json_dict: return str(self.json_dict)
return self.raw # 否则打纯文本
result.raw(纯文本)、result.tasks_output[0].raw(第一个任务的输出)、result.token_usage(花了多少 token)、result["confidence"](若有结构化输出,__getitem__ 会去 pydantic/json_dict 里取键)。D04 讲 Task 时会看到怎么让输出变成结构化的 pydantic/json。🧠 今天你应该能回答
- 为什么官方用 uv 装?
crewaivscrewai[tools]区别?(快/稳;后者带现成工具,依赖更重) - Agent 的大脑怎么配?(
llm="gpt-4o"字符串 + 环境变量 key,或本地 ollama) {sector}是 f-string 吗?(不是!是 CrewAI 占位符,kickoff 时按 inputs 替换)- kickoff 在跑任务前先做了什么?(prepare_kickoff:before 回调 → 发事件 → 插值 → 重置)
- 为什么把准备逻辑抽成 prepare_kickoff?(主流程可读 + 被多种 kickoff 变体复用)
- CrewOutput 有哪些视图?print 打哪个?(raw/pydantic/json_dict/tasks_output/token_usage;优先 pydantic)
✋ 10 分钟动手
# 1. 装好并跑通最小 crew
uv venv && source .venv/bin/activate
uv pip install crewai
export OPENAI_API_KEY=sk-... # 或用 ollama
uv run python main.py
# 2. 亲手验证"占位符必须匹配 inputs"这个坑
# 删掉 kickoff 里的 "sector" 这个 key,再跑,观察模型是否看到字面量 {sector}
# 3. 读 kickoff 主干与准备逻辑
sed -n '1021,1038p' lib/crewai/src/crewai/crew.py
sed -n '289,332p' lib/crewai/src/crewai/crews/utils.py
Agent(role=..., goal=..., backstory=..., llm=...) 这几行,背后是一个有几十个字段的大模型类。明天打开 agent/core.py 和它的父类 BaseAgent,搞清楚"一个 Agent 到底由什么构成",以及 role/goal/backstory 为什么是三件套。