Day 59 / 共 60 天 · 阶段10 运行·生产·收官

crewai CLI:从 create 到 deploy 的完整生命周期

59 天我们都在读"库"(怎么定义 Agent/Task/Crew、怎么执行)。但一个真实项目还需要"工具链":怎么创建项目骨架、跑起来、训练、回放调试、测试、重置记忆、部署上线。这就是 crewai 命令行。今天读 crewai_cliclick 组织的命令组、create 怎么生成 @CrewBase 脚手架、run 怎么在 uv 子进程里跑你的 Crew、train/replay/test 各自的角色、以及 CLI 为什么被拆成独立包、crewai.cli 的兼容 shim 怎么工作。学完你就掌握了 CrewAI 项目的完整落地路径。

📍 你在 60 天里的位置(阶段10 运行·生产·收官 · D59-60)
阶段9 进阶生态 D55-58 D59 CLI 与部署 D60 收官串讲 🏁
💡 先用一个类比兜住今天 CLI 就像装修队的一整套工具箱。库(前 58 天)是"建材",你得有工具才能盖房子:create 是打地基放线(生成项目结构)、run 是通水通电(真跑起来)、train 是精装调试(让 Agent 学得更好)、replay 是回看监控录像(从某一步重跑)、test 是竣工验收、deploy 是交房入住(上线)。而且这套工具箱有意思——它自己不干活,而是调 uv run 在你项目的独立环境里干活,就像工头不亲自砌墙,而是指挥工人在工地干。
L01

痛点:库写好了,怎么变成能交付的项目

🤔 痛点你学会了 Agent/Task/Crew,可写个 .py 手动 python main.py 跑,离"产品"还很远:项目结构怎么规范(配置放哪、依赖怎么锁)?团队协作怎么统一环境?怎么快速试不同 LLM provider?训练出的参数怎么保存、怎么从中间步骤重跑调试、怎么部署到云上让别人调用?这些"工程化"的活,不该每个人自己发明一遍。
💡 一句话本质 crewai CLI 用 click 把一组子命令组织成一棵命令树,覆盖项目全生命周期:create(脚手架)→ install(装依赖)→ run(执行)→ train/replay/test(调优与验证)→ reset-memories(清状态)→ deploy(上线)。核心设计:CLI 本身只做"参数解析 + 组装命令",真正的执行统统交给 uv run 在你项目的隔离环境里跑——CLI 与你的运行环境解耦。
命令作用本质
crewai create crew X生成项目骨架拷模板 + 改名字
crewai run跑 Crew/Flowuv run run_crew 子进程
crewai train N -f x.pkl训练 N 轮存参数uv run train
crewai replay -t <id>从某任务重跑用 task_id 定位
crewai test评测uv run test
crewai deploy create部署到 AMP调云端 API
L02

click 命令组:一棵命令树

顶层是一个 click group,子命令挂在它下面(crewai_cli/cli.py:101):

# crewai_cli/cli.py:101
@click.group()
@click.version_option(_get_cli_version())      # crewai --version
def crewai() -> None:
    """Top-level command group for crewai."""

# :138 一个子命令的样子
@crewai.command()
@click.argument("type", required=False, default=None, type=click.Choice(["crew", "flow"]))
@click.argument("name", required=False, default=None)
@click.option("--provider", type=str, help="The provider to use for the crew")
@click.option("--classic", is_flag=True, help="Use classic Python/YAML project structure")
def create(type, name, provider, skip_provider=False, classic=False, declarative=False):
    """Create a new crew, or flow."""
    ...

入口在 pyproject 里声明(lib/crewai/pyproject.toml:147):

# pyproject.toml:147
[project.scripts]
crewai = "crewai_cli.cli:crewai"      # 命令 crewai → 指向 crewai() 这个 group
@click.group()crewai 变成一个"命令组"——它本身不干活,而是能挂子命令。crewai create/crewai run 都是它的孩子。
@click.version_option自动加 --version。click 帮你处理这些通用脚手架,不用手写。
@click.argument / @click.option声明式定义位置参数和选项。type=click.Choice([...]) 限定取值,输错自动报错并提示。
[project.scripts]★装包时,pip/uv 会据此生成一个叫 crewai 的可执行文件,运行它就调 crewai_cli.cli:crewai。这就是"为什么终端能敲 crewai"。
💡 为什么用 click 而不是手写 argparse/sys.argv?手写参数解析要处理子命令分发、类型校验、help 文本、错误提示——全是重复劳动且易出 bug。click装饰器声明式地描述"这个命令有哪些参数",自动生成 help、自动校验、自动分发。让加一个新命令只需写一个带装饰器的函数,可维护性和一致性都远胜手写。
L03

create:生成 @CrewBase 脚手架

create 支持 crew/flow、经典/JSON/声明式多种模板(crewai_cli/cli.py:155):

# crewai_cli/cli.py:155
def create(type, name, provider, skip_provider=False, classic=False, declarative=False):
    """Create a new crew, or flow."""
    dmn_mode = is_dmn_mode_enabled()
    if not type:
        if dmn_mode:
            raise click.UsageError("TYPE is required when CREWAI_DMN is set. ...")
        ...      # 交互式补问 type/name
    # 最终委托给 create_crew.py / create_flow.py 拷贝模板、替换项目名

生成的结构正是 Day 55 学的 @CrewBase 布局:

my_crew/
├── pyproject.toml            # 依赖 + [project.scripts] run_crew/train/...
├── src/my_crew/
│   ├── crew.py               # @CrewBase 类:@agent/@task/@crew
│   ├── main.py               # 入口:kickoff()
│   └── config/
│       ├── agents.yaml       # 角色/目标/背景(Day 55 的花名册)
│       └── tasks.yaml        # 任务描述(任务单)
type = crew | flow两种项目形态:Crew(Day 19 的团队编排)或 Flow(阶段7 事件驱动)。用 click.Choice 限定,选错即报错。
--provider选 LLM 供应商(OpenAI/Anthropic/...)。生成时把对应配置写进模板,省得你手配。
--classic / --declarative经典 Python/YAML 结构 vs 声明式/JSON 结构。不同团队偏好不同——CLI 都给你留了模板。
交互式补问没传 type/name 时(非 DMN 模式)会交互询问。对新手友好;DMN(自动化)模式则强制显式传参、不交互。
大白话create 的本质就是"拷一份模板项目、把里面的占位名替换成你的项目名"。它不神奇,但它把 Day 55 那套 @CrewBase + YAML 的最佳实践固化成默认起点——你不用从零想"项目该怎么摆"。
L04

run:CLI 不亲自跑,交给 uv 子进程

run 命令解析参数后转交 run_crewcrewai_cli/cli.py:523):

# crewai_cli/cli.py:523
def run(trained_agents_file, definition, inputs):
    """Run the Crew or Flow."""
    if trained_agents_file is not None and definition is not None:
        raise click.UsageError("--filename can only be used when running crews")
    run_crew(trained_agents_file=trained_agents_file, definition=definition, inputs=inputs)

而真正执行是在 uv 环境里跑一个脚本(crewai_cli/run_crew.py:740):

# crewai_cli/run_crew.py:740
def _execute_uv_script(script_name, *, entity_type, trained_agents_file=None):
    """Execute a project script through uv."""
    command = ["uv", "run", script_name]                       # 例:uv run run_crew
    env = build_env_with_all_tool_credentials()                # 注入工具鉴权环境变量
    if trained_agents_file:
        env[CREWAI_TRAINED_AGENTS_FILE_ENV] = trained_agents_file
    try:
        subprocess.run(command, capture_output=False, text=True, check=True, env=env)  # ★子进程
    except subprocess.CalledProcessError as e:
        _handle_run_error(e, entity_type)                      # 转成友好错误提示
    except Exception as e:
        click.echo(f"An unexpected error occurred: {e}", err=True)
run 只做参数校验比如"--filename 只能配合 crew 用"这种互斥检查。真活儿它不干,转交 run_crew。职责单一。
["uv", "run", script_name]★核心:CLI 起一个 uv 子进程跑你项目的 run_crew 脚本(pyproject 里定义的入口)。你的 Crew 在项目自己的虚拟环境里执行。
build_env_with_all_tool_credentials把各种工具的鉴权(API key 等)作为环境变量注入子进程。凭据通过 env 传递,不落代码。
capture_output=False不捕获输出 → 子进程的 stdout/stderr 直接透传到你终端。所以 verbose 日志你能实时看到。
check=True + 异常处理子进程非零退出就抛 CalledProcessError,转成友好提示(如"没装 crewai 包")。
控制流:crewai run 如何隔离地跑起你的 Crew crewai run 参数校验(全局安装) _execute_uv_script 拼 [uv,run,run_crew]+env subprocess(uv) 项目隔离虚拟环境 你的 Crew kickoff() stdout/stderr 透传回终端(capture_output=False) · 非零退出→友好报错
图注:CLI(全局装)只当启动器,真正的执行落在项目自己的 uv 隔离环境里——两者解耦。
💡 设计取舍①:为什么 CLI 要起子进程跑,而不是直接 import 你的 Crew 执行? 直接 import 执行:CLI 进程和你的 Crew 共用一个 Python 环境。问题:CLI 装在全局(uv tool install),而你的项目有自己锁定的依赖版本——两者极易依赖冲突(CLI 要 pydantic v2,你项目锁了别的版本…)。子进程 + uv run:让 Crew 在项目自己的隔离环境里跑,CLI 只当"启动器"。代价是多一次进程启动开销、输出要透传,但换来了"CLI 全局装一次、各项目环境互不干扰"。隔离性 > 一点点性能——生产工具的正确选择。
L05

train / replay / test:调优与验证三兄弟

train 也是起 uv 子进程,但多了参数校验(crewai_cli/train_crew.py:6):

# crewai_cli/train_crew.py:6
def train_crew(n_iterations: int, filename: str) -> None:
    """Train the crew by running a command in the UV environment."""
    command = ["uv", "run", "train", str(n_iterations), filename]
    try:
        if n_iterations <= 0:
            raise ValueError("The number of iterations must be a positive integer.")
        if not filename.endswith(".pkl"):
            raise ValueError("The filename must not end with .pkl")   # 注意这条校验(见边界)
        result = subprocess.run(command, capture_output=False, text=True, check=True)
        ...
    except subprocess.CalledProcessError as e:
        click.echo(f"An error occurred while training the crew: {e}", err=True)

replay 用 task_id 从中间步骤重跑(crewai_cli/cli.py:274):

# crewai_cli/cli.py:254
@crewai.command()
@click.option("-t", "--task_id", ...)                    # 从哪个任务重放
@click.option("-f", "--trained_agents_file", ...)
def replay(task_id, trained_agents_file):
    """Replay the crew execution from a specific task."""
    ...
train N filename.pkl跑 N 轮,把学到的建议/参数存进 pkl 文件(Day 24 训练)。之后 run/replay 可用 -f 加载它。
n_iterations <= 0 校验训练轮数必须为正。CLI 层就拦下无意义输入,不用等子进程跑起来才报错。
replay -t <task_id>★调试神器:Crew 跑到一半某步出错,不用从头再跑(贵!),用 task_id 从那一步重放。呼应 Day 24 replay。
test(:477) 跑 N 轮评测、给 Crew 表现打分。上线前的"竣工验收"。

👶 小白:train 和 test 有啥区别?

👨‍🏫 老师:train 是"让 Agent 变得更好"——多轮跑 + 人工反馈,把经验存成 pkl,下次带着经验干活。test 是"考核 Agent 现在多好"——不改它,只跑几轮打分评估。一个是练,一个是考。replay 则是"从录像的某一帧继续"——调 bug 用,避免重跑整条流水线。

L06

reset-memories 与 deploy:清状态与上线

reset-memories 用一堆 flag 精细控制清什么(crewai_cli/cli.py:311):

# crewai_cli/cli.py:311
@crewai.command()
@click.option("-m", "--memory", is_flag=True, help="Reset MEMORY")
@click.option("-kn", "--knowledge", is_flag=True, help="Reset KNOWLEDGE storage")
@click.option("-a", "--all", is_flag=True, help="Reset ALL memories")
def reset_memories(...):                                  # :342
    """Reset the crew memories (long, short, entity, ...)."""

deploy 本身又是一个子命令组(crewai_cli/cli.py:572):

# crewai_cli/cli.py:572
@crewai.group()
def deploy() -> None:
    """Deploy the Crew CLI group."""

@deploy.command(name="create")                            # :577 crewai deploy create
@click.option("-y", "--yes", is_flag=True, help="Skip the confirmation prompt")
@click.option("--skip-validate", is_flag=True, help="Skip the pre-deploy validation checks.")
def deploy_create(...):
    ...   # 打包 + 调 CrewAI AMP 云端 API 部署
reset-memories 分类 flag短期/长期/实体记忆、知识库分开清。因为"清全部"太粗暴——调试时你可能只想清短期记忆保留知识库。
@crewai.group() 嵌套deploy 自己又是 group,下面挂 create/list/logs 等。命令树可以多层——把相关操作聚成子命名空间。
login / logout(:549/:560) 部署前要登录 CrewAI AMP(云平台)。CLI 管理 token(TokenManager),凭据本地存。
--skip-validate部署前默认做校验(配置齐不齐、能不能跑)。老手可跳过,但默认帮你把关。
数据结构:crewai 命令树 crewai create install run train replay test reset-memories deploy(group) create logs 一个 group 根节点,下挂子命令;deploy 是子 group,可再挂 create/logs(多层命令树)
图注:crewai 命令树——顶层 group + 一排子命令 + deploy 这个可再嵌套的子 group。
L07

CLI 被拆成独立包 + 兼容 shim

CLI 从 crewai.cli 搬到了独立包 crewai_cli,老导入靠一个 import shim 兜住(crewai/cli/__init__.py):

# crewai/src/crewai/cli/__init__.py
"""Deprecated: use ``crewai_cli`` instead.
The CLI was extracted into the standalone ``crewai-cli`` package. Legacy
``from crewai.cli.X import Y`` imports are intercepted here and resolved to
the corresponding ``crewai_cli.X`` module so downstream code keeps working."""

_PREFIX = "crewai.cli"; _TARGET = "crewai_cli"
warnings.warn("crewai.cli is deprecated; import from crewai_cli instead.", DeprecationWarning)

class _ShimFinder(importlib.abc.MetaPathFinder):
    def find_spec(self, fullname, path, target=None):
        if fullname != _PREFIX and not fullname.startswith(_PREFIX + "."):
            return None
        mapped = _TARGET + fullname[len(_PREFIX):]     # crewai.cli.X → crewai_cli.X
        module = importlib.import_module(mapped)
        ...   # 造一个 spec 指向已导入的 crewai_cli 子模块

_finder = _ShimFinder()
if _finder not in sys.meta_path:
    sys.meta_path.insert(0, _finder)                   # 装进 import 系统的查找链
拆成独立包把体积大、依赖多(click、TUI、部署 API)的 CLI 从核心库剥离。想用库的人不必被迫装一堆 CLI 依赖。
MetaPathFinder shim★老代码 from crewai.cli.X import Y 不会立刻崩——这个自定义 finder 拦截该导入、重定向crewai_cli.X。平滑迁移。
DeprecationWarning重定向的同时打警告,提醒开发者改成新导入。兼容但不纵容:给迁移窗口,也给明确信号。
sys.meta_path.insert(0)把 finder 插到导入查找链最前面,抢在默认机制前处理 crewai.cli.*。这是 Python import 系统的高级扩展点。
💡 设计取舍②:为什么拆包还要费劲写 shim,而不直接删掉旧路径? 直接删:干净,但所有 from crewai.cli... 的老项目/教程/第三方代码立刻全崩——破坏性升级,用户骂声一片。写 shim:老导入继续能用(只是打个 deprecation 警告),给生态一个平滑迁移期。代价是多维护一段 import 魔法、以及"两条路径并存"的短期复杂。这是成熟库的通用做法——向后兼容是承诺,破坏性变更要给迁移路径。呼应 Day 58 里 A2AConfig 标 Deprecated 却仍保留的同一原则。
L08

边界 + 今日小结

⚠️ 边界①:train 的文件名校验看着别扭 train_crewtrain_crew.py:19)里写着 if not filename.endswith(".pkl"): raise ValueError("The filename must not end with .pkl")——条件和错误信息读起来"反直觉"(判断"不以 .pkl 结尾"却说"不能以 .pkl 结尾")。实际语义是要求你传的名字符合约定。读源码遇到这种"信息像是拧过"的地方,别急着照字面理解,要以运行时行为为准——真实 CLI 会告诉你正确格式。这也提醒:错误文案是最容易和逻辑脱节的地方。
⚠️ 边界②:crewai run 报"没装 crewai 包" CLI(crewai-cli)可以单独 uv tool install,但它不含运行 Crew 所需的 crewai 核心库。run_crew.py(:40)专门准备了提示:uv tool install --force 'crewai[tools]',并提醒 zsh 下要加引号(否则 crewai[tools] 被当成通配符)。CLI 装了 ≠ 能跑 Crew——run 依赖项目环境里有完整 crewai。这是"CLI 与运行环境解耦"的副作用,第一次跑常踩。

🧠 今天你应该能回答

  • crewai CLI 覆盖了项目哪些生命周期阶段?
  • 为什么终端能敲 crewai?([project.scripts] 入口)
  • 为什么 CLI 起 uv run 子进程而不是直接 import 执行?
  • train / test / replay 各自解决什么问题?
  • deploy 为什么是个子 group?命令树能几层?
  • CLI 为什么拆成独立包?import shim 怎么兼容老导入?

✋ 10 分钟动手

P=lib/cli/src/crewai_cli
sed -n '101,170p' $P/cli.py           # group + create 骨架
sed -n '497,575p' $P/cli.py           # run / update / login / deploy group
sed -n '740,768p' $P/run_crew.py      # _execute_uv_script 子进程执行
sed -n '6,33p'    $P/train_crew.py    # train 参数校验 + subprocess
sed -n '1,75p'    ../../crewai/src/crewai/cli/__init__.py   # 兼容 shim
# 亲手走一遍生命周期
crewai create crew demo && cd demo && crewai install && crewai run
明日预告 · Day 60(收官):最后一天!把 60 天的知识串成一张地图(Agent→Task→Crew→执行循环→工具→记忆→Flow→LLM→生态),横向对比六大 Agent 框架(CrewAI/LangGraph/AutoGPT/OpenHands/eino/SuperAGI)的定位与取舍,并给出继续学习与求职的实战建议(衔接 agent-career)。
← Day 58 a2a 协作协议 Day 60 · 收官串讲 🏁 →