第 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:脚手架速览
# 创建经典 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 为例,生成的目录结构如下:
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 # 任务定义安装依赖并运行:
cd my_news_crew
crewai install # 用 uv 安装 pyproject 声明的依赖
crewai run # 执行本项目(自动识别 crew 或 flow)17.3 agents.yaml 与 tasks.yaml 详解
先删掉示例内容,写一个真实的科技快讯 Crew。YAML 中的键名对应 Agent/Task 的构造参数:
# 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# 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 编排粘合起来:
# 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:
# 方式一:代码内执行(可传入插值变量)
result = MyNewsCrew().crew().kickoff(inputs={"topic": "AI 编程助手"})
print(result.raw)# 方式二:crewai run 会找 main.py 并按 pyproject.toml 的声明执行
crewai run分工原则一句话总结:YAML 管“是谁、做什么”,Python 管“用什么工具、什么模型、怎么组队”。
配置分离后还有个额外红利:同一套配置可以批量服务多输入。配合 kickoff_for_each,一次跑一批主题:
# 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 项目同理):
#!/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. 想复现"某个中间任务之后"的行为来调试,应该用哪个命令?
🛠️ 动手实践
- 用
--classic脚手架新建一个"周报整理 Crew",把本章示例改造为读取inputs={"week": "2025-W23"}的版本,YAML 中至少出现两处插值。 - 在 agents.yaml 中故意把某个 agent 的键名写错(与
@agent方法返回的 config 取值不一致),观察报错信息,然后修复——体会配置错误的典型排查路径。 - 分别用
crewai run与代码内kickoff()跑同一个项目,再用crewai replay从第二个任务回放一次,对比三种方式的日志差异。
单个 Crew 已经工程化了,下一步是把多个 Crew 组织成完整业务系统:第 18 章 · 多 Crew 编排与复用。