Skip to content

第 17 章 · CLI 工程化与 YAML 配置项目

本章目标:会用 crewai create 脚手架生成 crew/flow 项目,理解 config/agents.yaml + tasks.yaml 的配置结构与变量插值规则,掌握 @CrewBase / @agent / @task / @crew 装饰器体系,并能使用 crewai run 等常用命令完成开发闭环。

17.1 为什么要把角色定义搬进 YAML

前十六章的示例都把 role/goal/backstory 写在 Python 字符串里。项目一大就会暴露三个问题:

  • 提示词调优要反复改代码、重新部署;
  • 非程序员(产品、运营)无法参与提示词迭代;
  • 角色与代码逻辑耦合,难以复用与 diff 审查。

CrewAI 的答案是配置与代码分离:角色的"人设"放进 YAML,编排逻辑留在 Python。CLI 脚手架一键生成这套结构。

17.2 crewai create:脚手架速览

bash
# 创建经典 Python/YAML 结构的 crew 项目
crewai create crew my_news_crew --classic

# 创建 flow 项目(内含示例 crew)
crewai create flow my_pipeline

# 其他脚手架
crewai create tool my_tool      # 自定义工具仓库骨架
crewai create skill my-skill    # 技能包骨架

JSON-first 是新默认

从新版本起,crewai create crew 默认生成 JSON-first 项目(crew.jsonc + agents/*.jsonc);想要本教程使用的传统 crew.py + config/*.yaml 结构,需加 --classic 标志。两种形态官方都会继续支持。

my_news_crew --classic 为例,生成的目录结构如下:

text
my_news_crew/
├── pyproject.toml          # 项目依赖与元数据(uv 管理)
├── README.md
├── .env                    # API Key 等环境变量
└── src/my_news_crew/
    ├── __init__.py
    ├── main.py             # 入口:kickoff() / plot() / train() 等
    ├── crew.py             # Crew 定义(装饰器体系)
    └── config/
        ├── agents.yaml     # 角色人设
        └── tasks.yaml      # 任务定义

安装依赖并运行:

bash
cd my_news_crew
crewai install            # 用 uv 安装 pyproject 声明的依赖
crewai run                # 执行本项目(自动识别 crew 或 flow)

17.3 agents.yaml 与 tasks.yaml 详解

先删掉示例内容,写一个真实的科技快讯 Crew。YAML 中的键名对应 Agent/Task 的构造参数

yaml
# src/my_news_crew/config/agents.yaml
tech_researcher:
  role: "科技资讯研究员"
  goal: "围绕 {topic} 找出本周最值得关注的 3 条技术新闻"
  backstory: "你深耕科技媒体行业八年,嗅觉敏锐,只相信一手信源。"
  llm: "openai/deepseek-chat"        # 可选:也可统一在 crew.py 里注入
  max_iter: 15
  verbose: true

news_writer:
  role: "快讯撰稿人"
  goal: "把研究素材改写成适合微信公众号的快讯"
  backstory: "你是资深新媒体编辑,擅长把硬核技术写成通俗短文。"
  verbose: true
yaml
# src/my_news_crew/config/tasks.yaml
research_task:
  description: "调研主题「{topic}」本周的重要技术新闻,筛选 3 条。"
  expected_output: >
    3 条新闻要点,每条包含标题、一句话摘要、信息来源。
  agent: tech_researcher

writing_task:
  description: >
    基于调研结果撰写一篇中文科技快讯。
    要求口语化、有钩子开头,正文不超过 500 字。
  expected_output: "一篇 Markdown 格式的公众号快讯,含标题。"
  agent: news_writer

变量插值是 YAML 配置的灵魂:花括号 {topic} 会被 kickoff(inputs={"topic": ...}) 的同名键替换。这让同一个 Crew 可以批量服务不同输入。注意三点:

  • inputs 里没有的插值键会直接报错,YAML 里不要留无人喂值的占位符;
  • {xxx} 只做纯文本替换,不能嵌表达式;
  • expected_output 使用 YAML 的 > 折叠语法可以多行书写,最终拼成一段字符串。

17.4 @CrewBase 装饰器体系

crew.py 把 YAML 配置和 Python 编排粘合起来:

python
# src/my_news_crew/crew.py
import os
from crewai import Agent, Task, Crew, Process, LLM
from crewai.project import CrewBase, agent, task, crew
from crewai_tools import SerperDevTool   # 示例工具(需 pip install crewai-tools)

@CrewBase                                  # 标记:自动加载 config/ 下的 YAML
class MyNewsCrew:
    """科技快讯 Crew"""

    agents_config = "config/agents.yaml"   # 显式声明配置路径(默认即此)
    tasks_config = "config/tasks.yaml"

    @agent                                 # 每个方法对应 YAML 里的一个键
    def tech_researcher(self) -> Agent:
        return Agent(
            config=self.agents_config["tech_researcher"],
            tools=[SerperDevTool()],       # 工具仍在代码里装配
            llm=LLM(
                model="openai/deepseek-chat",
                base_url="https://api.deepseek.com/v1",
                api_key=os.getenv("DEEPSEEK_API_KEY"),
            ),
        )

    @agent
    def news_writer(self) -> Agent:
        return Agent(
            config=self.agents_config["news_writer"],
            llm=LLM(
                model="openai/deepseek-chat",
                base_url="https://api.deepseek.com/v1",
                api_key=os.getenv("DEEPSEEK_API_KEY"),
            ),
        )

    @task
    def research_task(self) -> Task:
        return Task(config=self.tasks_config["research_task"])

    @task
    def writing_task(self) -> Task:
        return Task(config=self.tasks_config["writing_task"])

    @crew                                  # 组装入口
    def crew(self) -> Crew:
        return Crew(
            agents=self.agents,            # @CrewBase 自动收集全部 @agent 方法
            tasks=self.tasks,              # 自动收集全部 @task 方法
            process=Process.sequential,
            verbose=True,
        )

运行方式有两种——直接用类,或走 CLI:

python
# 方式一:代码内执行(可传入插值变量)
result = MyNewsCrew().crew().kickoff(inputs={"topic": "AI 编程助手"})
print(result.raw)
bash
# 方式二:crewai run 会找 main.py 并按 pyproject.toml 的声明执行
crewai run

分工原则一句话总结:YAML 管“是谁、做什么”,Python 管“用什么工具、什么模型、怎么组队”

配置分离后还有个额外红利:同一套配置可以批量服务多输入。配合 kickoff_for_each,一次跑一批主题:

python
# batch_run.py —— 同一 Crew 批量处理多个插值输入
from my_news_crew.crew import MyNewsCrew

# 列表里每个字典都会作为一次独立的 kickoff 输入
batch_inputs = [
    {"topic": "AI 编程助手"},
    {"topic": "向量数据库"},
    {"topic": "开源大模型推理优化"},
]
results = MyNewsCrew().crew().kickoff_for_each(batch_inputs)
for inp, res in zip(batch_inputs, results):
    print(f"== {inp['topic']} ==\n{res.raw}\n")

kickoff_for_each 会逐个(或按并发配置)执行并返回结果列表,是内容工厂类场景的常用入口;单条失败不影响已完成的批次产物,便于断点补跑。

17.5 main.py 与命令族

脚手架生成的 main.py 通常长这样(flow 项目同理):

python
#!/usr/bin/env python
from my_news_crew.crew import MyNewsCrew

def run():
    MyNewsCrew().crew().kickoff(inputs={"topic": "AI 编程助手"})

def train():
    """训练入口:crewai train 时被调用"""
    inputs = {"topic": "AI 编程助手"}
    MyNewsCrew().crew().train(n_iterations=5, filename="trained_data.pkl", inputs=inputs)

if __name__ == "__main__":
    run()

日常最高频的 CLI 命令一览:

命令用途
crewai run运行当前项目(0.103+ 自动识别 crew 或 flow)
crewai install按 pyproject.toml 安装依赖
crewai test -n 5 -m <model>多轮运行并用 LLM 给输出质量打分
crewai train -n 10人审迭代式训练,产出 human_feedback 数据
crewai replay -t <task_id>从指定任务回放后续执行(调试神器)
crewai log-tasks-outputs查看最近一次 kickoff 的各任务输出
crewai reset-memories -a清空记忆/知识存储

版本差异提示

新版 CLI 将旧命令收敛为资源组形式(如旧的 crewai tool create x 改为 crewai create tool x),snake_case 参数(--n_iterations)改为 kebab-case(--n-iterations)。旧写法目前仍兼容但会打印弃用警告,新项目请一律用新形式。

17.6 本章小结

  • 配置分离解决提示词迭代的工程痛点:人设进 YAML、工具模型进 Python;
  • crewai create crew xxx --classic 生成经典 YAML 项目;新默认是 JSON-first(crew.jsonc);
  • YAML 键名对应构造参数,{var}kickoff(inputs={...}) 插值,缺失即报错;
  • @CrewBase 自动加载 config/ 并收集 @agent / @task 方法,@crew 返回组装好的 Crew;
  • 高频命令:run / install / test / train / replay / log-tasks-outputs / reset-memories

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. tasks.yaml 里写了 {topic} 占位符,运行时如何提供它的值?

2. 关于 crewai create crew 的默认行为,正确的是?

3. @CrewBase 类中 self.agents 和 self.tasks 是什么?

4. 想复现"某个中间任务之后"的行为来调试,应该用哪个命令?

🛠️ 动手实践

  1. --classic 脚手架新建一个"周报整理 Crew",把本章示例改造为读取 inputs={"week": "2025-W23"} 的版本,YAML 中至少出现两处插值。
  2. 在 agents.yaml 中故意把某个 agent 的键名写错(与 @agent 方法返回的 config 取值不一致),观察报错信息,然后修复——体会配置错误的典型排查路径。
  3. 分别用 crewai run 与代码内 kickoff() 跑同一个项目,再用 crewai replay 从第二个任务回放一次,对比三种方式的日志差异。

单个 Crew 已经工程化了,下一步是把多个 Crew 组织成完整业务系统:第 18 章 · 多 Crew 编排与复用