探索 CrewAI(2)——寻找执行入口

224 阅读6分钟

导语

上一篇在这里:《探索 CrewAI——简介、安装和测试(1)》

上一篇,我们已经通过如下命令,创建了一个新的项目工程。

crewai create crew my_ai_crew

并且,通过下面的命令,测试运行成功。

# 进入工程所在目录
cd my_ai_crew
# 运行Agent
crewai run

接下来,看一下当我们执行crewai run来运行Agent的时候,究竟发生了什么。

先说结论

  • 执行这个脚本:/root/miniconda3/envs/CrewAI/bin/crewai
  • 执行这个脚本里的run函数:/root/miniconda3/envs/CrewAI/lib/python3.10/site-packages/crewai/cli/cli.py
  • 执行这个脚步里的 run_crew 函数:/root/miniconda3/envs/CrewAI/lib/python3.10/site-packages/crewai/cli/run_crew.py
  • 通过UV,根据项目文件 my_ai_crew/pyproject.toml,调用 my_ai_crew.main:run
  • 最终执行到 my_ai_crew/src/my_ai_crew/main.py 里的run函数
  • 调用 MyAiCrew().crew().kickoff(inputs=inputs) ,创建Agents/Tasks/Crew等,完成调用。

下面是详细的分析过程。

关于 crewai 命令

第一步,查看 crewai 这个命令行工具在哪里。

which crewai
# 输出 /root/miniconda3/envs/CrewAI/bin/crewai

可以看到,这个工具位于conda虚拟环境“CrewAI”中。这个是上一篇介绍过的,通过conda命令新增的虚拟环境。

第二步,看看 crewai 里的代码逻辑。

打开这个文件:/root/miniconda3/envs/CrewAI/bin/crewai ,可以看到内容如下:

注:下面的①②③是我手动添加到,为了方便后续的解读。

#!/root/miniconda3/envs/CrewAI/bin/python
# -*- coding: utf-8 -*-
import re
import sys
## ①
from crewai.cli.cli import crewai
​
if __name__ == '__main__':
    ## ②
    sys.argv[0] = re.sub(r'(-script.pyw|.exe)?$', '', sys.argv[0])
    ## ③
    sys.exit(crewai())

①、导入语句

这个导入语句 from crewai.cli.cli import crewai 表示从之前安装的crewai包的命令行接口模块(cli.py)中导入主命令函数 crewai。

那么,crewai包的安装路径在哪里?可以通过下面命令来查看。

pip show crewai | grep -aw "^Location" --color
# 输出如下
# Location: /root/miniconda3/envs/CrewAI/lib/python3.10/site-packages

结合上述信息可得知,最终是从下面的cli.py 中导入的。

/root/miniconda3/envs/CrewAI/lib/python3.10/site-packages/crewai/cli/cli.py

其中:

  • crewai - 主包名
  • cli - 子包名(第一个)
  • cli - 模块名(第二个,即 cli.py 文件)
  • crewai - 从 cli.py 文件中导入的函数名

打开文件查看详情,可以看到有 crewai() 函数的定义。

②、规范化命令行参数。

sys.argv[0] = re.sub(...) 这行代码的作用是清理脚本文件名(sys.argv[0]),移除可能存在的跟平台相关的特定后缀(如 -script.pyw.exe),这样的目的是,确保在不同操作系统上保持一致的文件名。

以下是几个简单的示例:

# 可能的输入和处理后的输出:
"crewai-script.pyw" -> "crewai"
"crewai.exe"        -> "crewai"
"crewai"            -> "crewai"

这样做有几个好处:

  • 跨平台兼容性

    • Windows 上可能有 .exe 后缀
    • Python 包装脚本可能有 -script.pyw 后缀
  • 标准化脚本名称

    • 移除不同平台特定的后缀
    • 保持脚本名称的一致性
  • 避免问题

    • 某些程序可能依赖脚本名称
    • 后缀可能导致路径解析问题

③、调用 cli.py 中的 crewai() 函数

当这个脚本/root/miniconda3/envs/CrewAI/bin/crewai 调用 crewai() 函数时,会调用到 cli.py 里对应的函数。

cli.py 里,使用了 Click 框架,新增了命令组 crewai(),并且注册了很多子命令。如下所示:

命令组核心内容:

@click.group()
@click.version_option(get_version("crewai"))
def crewai():
    """Top-level command group for crewai."""

注册的多个子命令:

@crewai.command()
def create(...):    # 创建 crew 或 flow
    
@crewai.command()
def version(...):   # 显示版本信息
​
@crewai.command()
def train(...):     # 训练 crew
​
@crewai.command()
def run(...):       # 运行 crew
​
# ... 其他子命令

所以,当我们在命令行执行crewai run 时,Click 框架解析命令行参数(得到 run)然后会执行 cli.py 里的其中一个子命令run

@crewai.command()
def run():
    """Run the Crew."""
    click.echo(f"Running the Crew")
    run_crew()   # 会调用这个函数

进一步跟踪,发现 run_crew()函数是在这个文件定义的:

# /root/miniconda3/envs/CrewAI/lib/python3.10/site-packages/crewai/cli/run_crew.py
# 为方便理解,下面的逻辑有简化
def run_crew() -> None:
  command = ["uv", "run", "run_crew"]
  pyproject_data = read_toml()  # 读取 pyproject.toml 配置文件
  """在 UV 环境中运行 crew"""
  subprocess.run(command, capture_output=False, text=True, check=True)

uv: UV 包管理器的命令

run: UV 的子命令,用于运行 Python 脚本

run_crew: 要执行的具体脚本名

实际执行效果,相当于直接在命令行中执行:

# 相当于在终端执行:
uv run run_crew

那么,上面的命令又到底干了什么呢?跟项目根目录的文件 pyproject.toml 有关。打开这个项目配置文件看看:

# cat pyproject.toml
[project.scripts]
my_ai_crew = "my_ai_crew.main:run"
run_crew = "my_ai_crew.main:run"
train = "my_ai_crew.main:train"
replay = "my_ai_crew.main:replay"
test = "my_ai_crew.main:test"

可以看看第4行run_crew的配置,可以得知执行uv run run_crew时,会调用 my_ai_crew 目录下的 main.py 文件的 run 函数。

可以打开 main.py 文件进一步看看,发生了什么事情。

main.py

cat study_crewai/my_ai_crew/src/my_ai_crew/main.py

可以看到

def run():
    """
    Run the crew.
    """
    inputs = {
        'topic': 'AI LLMs',
        'current_year': str(datetime.now().year)
    }
    
    try:
        MyAiCrew().crew().kickoff(inputs=inputs)
    except Exception as e:
        raise Exception(f"An error occurred while running the crew: {e}")

这里的逻辑很简单,创建了一个 MyAiCrew 实例,获取其关联的 crew(AI 代理团队),并使用提供的 inputs 参数启动(kickoff)这个团队开始执行任务。

进一步查看里面的实现

# my_ai_crew/src/my_ai_crew/crew.py
@CrewBase
class MyAiCrew():
  """MyAiCrew crew"""
  # 以下几个函数省略具体的实现
  @agent
  def researcher(self) -> Agent:
  @agent
  def reporting_analyst(self) -> Agent:
  @task
  def research_task(self) -> Task:
  @task
  def reporting_task(self) -> Task:
  
  @crew
  def crew(self) -> Crew:
    """Creates the MyAiCrew crew"""
    # To learn how to add knowledge sources to your crew, check out the documentation:
    # https://docs.crewai.com/concepts/knowledge#what-is-knowledge
​
    return Crew(
      agents=self.agents, # Automatically created by the @agent decorator
      tasks=self.tasks, # Automatically created by the @task decorator
      process=Process.sequential,
      verbose=True,
      # process=Process.hierarchical, # In case you wanna use that instead https://docs.crewai.com/how-to/Hierarchical/
    )

简单来说:

这个文件定义了一个 MyAiCrew 类,它是基于 CrewAI 框架的来实现。并且通过装饰器 @CrewBase@agent@task@crew,它分别定义了研究员(researcher)和分析师(reporting_analyst)两个 AI 代理,以及相应的研究任务和报告任务。

这个类的核心在于通过 YAML 配置文件(agents.yamltasks.yaml)来管理代理和任务的具体设置,并在 crew() 方法中将它们组装成一个完整的工作流。当调用 kickoff() 时,代理们会按照 Process.sequential(顺序执行)的方式完成指定的任务,整个过程支持详细的日志输出(verbose=True)。

总结一下

到此,入口调用链路很清晰了。

总结一下:当用户执行 crewai run时:

  • 执行这个脚本:/root/miniconda3/envs/CrewAI/bin/crewai
  • 执行这个脚本里的run函数:/root/miniconda3/envs/CrewAI/lib/python3.10/site-packages/crewai/cli/cli.py
  • 执行这个脚步里的 run_crew 函数:/root/miniconda3/envs/CrewAI/lib/python3.10/site-packages/crewai/cli/run_crew.py
  • 通过UV,根据项目文件 my_ai_crew/pyproject.toml,调用 my_ai_crew.main:run
  • 最终执行到 my_ai_crew/src/my_ai_crew/main.py 里的run函数
  • 调用 MyAiCrew().crew().kickoff(inputs=inputs) ,创建Agents/Tasks/Crew等,完成调用。

下一步

下一篇,来探索一下,crewai框架是如何调用大模型LLM来交互和决策的。 敬请期待。