使用Novita和CrewAI构建多智能体系统

使用Novita和CrewAI构建多智能体系统

随着AI行业逐渐认识到大语言模型(LLM)的推理能力,我们决定赋予其更强的代理能力,让它们能够执行更复杂的操作。这类大语言模型被称为智能体(agents)

由此,单智能体系统应运而生。但很快人们发现,让单个智能体承担多项任务并不是构建代理系统的最优方案。更好的做法是使用多个智能体,每个智能体负责执行专属任务,协同完成用户定义的目标。

然而,新的问题也随之出现:管理多个智能体远比管理单个智能体复杂。为解决这一问题,市面上出现了多个多智能体系统框架,其中最具潜力的之一便是CrewAI。

CrewAI能够帮助开发者构建多智能体系统。通过CrewAI,你可以管理智能体、任务以及整个工作流。在本文中,我们将结合CrewAI与Novita平台提供的丰富大语言模型,构建实用且智能的多智能体系统。

什么是多智能体系统?我们为什么需要它?

自大语言模型(LLM)问世以来,人们探索了多种方法提升其自主性。其中最流行的方案之一是论文《语言模型中推理与行动的协同》中提出的ReAct模式。LangChain等框架很快实现了ReAct,为LLM赋予了代理能力。

AutoGPT等工具则更进一步,构建了更强大的AI智能体,仅通过文本提示即可执行用户所需的几乎所有任务。尽管这些单智能体系统展现出巨大潜力,但也存在诸多局限:单个智能体通常依赖单一LLM处理所有任务,由此会引发几个关键问题:

  • 上下文限制:单个智能体需要使用同一个上下文窗口管理所有任务。随着工作流推进,智能体最终会耗尽上下文空间。
  • 复杂度升高:随着任务量增加,单个智能体的推理难度会大幅提升,更容易出现幻觉或执行失败。
  • 单点故障:单个智能体负责所有任务,没有冗余备份。如果它无法处理某项任务,整个系统就会完全失效。
  • 缺乏并行能力:任务只能串行处理,没有内置支持让智能体并行工作以提升效率。

多智能体系统正是为解决这些痛点而生。与依赖单个智能体处理多项任务不同,多智能体系统将职责分配给多个协同工作的智能体。由于每个智能体只需管理自己的工作流部分,整体上下文负载降低,从而避免了上下文长度超限等问题。

多智能体架构还支持针对不同场景使用不同模型:部分模型可针对特定任务微调,部分可搭配预设提示词,部分可配备专用工具,还有部分可针对推理场景优化。这种多样性不仅提升了系统性能,还支持并行处理,让多智能体系统比单智能体系统速度更快、效率更高。

构建多智能体系统需要两个核心组件:提供大语言模型的模型供应商,以及管理整个代理工作流的多智能体框架。在本文中,我们将以Novita作为模型供应商,CrewAI作为首选框架。

为什么你需要CrewAI这类框架

构建多智能体系统的复杂度远高于构建单智能体系统。例如,你可以通过函数调用为Novita上的任意模型配备工具,轻松将其转化为智能体。但一旦开始连接多个智能体,复杂度会急剧上升:你需要管理多个动态组件,包括:

  • 通信与任务委派:你需要协调智能体之间的通信方式,以及任务在智能体之间的流转规则。
  • 工具管理:如果没有框架,你需要手动管理工具定义和JSON schema,这很快就会变得混乱不堪。
  • 代理架构与模式:多智能体框架通常内置了对成熟代理模式的支持。如果从零开始构建,你需要持续跟进这些不断演进的模式并自行实现。
  • 可观测性:如果没有框架,你还需要自行搭建工具来监控、调试和可视化智能体的交互与运行表现。

多智能体框架可以帮助管理所有这些复杂度。在本文中,我们将选择CrewAI作为我们的框架。相比其他框架,CrewAI有多重优势:它以简洁性著称,使用直观的概念对多智能体系统进行建模;它成熟且经过充分验证;同时能为开发者提供出色的使用体验。

了解CrewAI的核心概念

要使用CrewAI构建多智能体系统,你需要熟悉该框架引入的几个核心概念。以下是本文将会用到的主要概念的简要介绍:

  • 智能体(Agents)
  • 任务(Tasks)
  • 团队(Crews)
  • 流程(Processes)
  • 工作流(Flows)

智能体

智能体是CrewAI的核心,代表由LLM驱动的实际AI工作者。CrewAI中的每个智能体都由三个要素定义:角色、引导其决策的目标,以及塑造其个性的背景故事。这些要素都以发送给LLM的文本提示词形式呈现。智能体还可以配备工具,供其选择执行特定操作。CrewAI中智能体的核心目标是完成分配的任务。

任务

任务定义了需要完成的工作。在CrewAI中,你可以创建任务,既可以直接将其分配给特定智能体,也可以让符合任务要求的智能体主动认领。每个任务通常包含清晰的描述、预期输出,以及其他相关参数,例如可选的负责处理该任务的智能体。

团队

这是CrewAI的核心概念。在CrewAI中,一组协同工作以完成任务的智能体被称为一个团队(crew)。团队同时定义了其中智能体的工作流。

流程

流程是预定义的工作流,用于指导团队如何执行任务。CrewAI支持两种主要流程类型:

  • 顺序流程:团队中的智能体一次处理一个任务。
  • 分层流程:在此流程中,一个智能体担任管理者,负责协调其他智能体并按需委派任务。

工作流(Flows)

流程是预定义的工作流,而CrewAI的工作流(Flows)则赋予开发者设计自定义智能体工作流的灵活性。一个工作流可以包含智能体、任务,甚至整个团队,支持更复杂的交互逻辑。

使用Novita构建团队

了解了CrewAI的核心概念后,我们接下来逐步演示如何构建一个团队。在本例中,我们将创建一个旨在帮助用户快速构建最小可行产品(MVP)的团队,该团队由三个专业智能体组成:

我们的团队将采用顺序流程运行。

安装与配置

方案确定后,我们首先开始配置环境。第一步是安装CrewAI及其内置工具支持:

pip install 'crewai[tools]'

接下来,你需要获取Novita API密钥。获取后,将其作为NOVITA_API_KEY添加到环境变量中:

export NOVITA_API_KEY=your_api_key_here

完成上述步骤后,我们就可以开始构建团队了。

创建大语言模型实例

首先导入所需的依赖项:

import os
from crewai import Agent, Task, Crew, Process, LLM
from crewai_tools import FileWriterTool

接下来,我们创建FileWriterTool的实例,编码员智能体将使用它向磁盘写入文件:

file_writer_tool = FileWriterTool()

工具准备就绪后,现在可以为每个智能体初始化对应的大语言模型:

architect_llm = LLM(
   model="novita/moonshotai/kimi-k2-instruct",
   temperature=0.5,
   api_base="https://api.novita.ai/v3/openai",
   api_key=os.environ['NOVITA_API_KEY']
)

coder_llm = LLM(
   model="novita/qwen/qwen3-coder-480b-a35b-instruct",
   temperature=0.4,
   api_base="https://api.novita.ai/v3/openai",
   api_key=os.environ['NOVITA_API_KEY']
)

reviewer_llm = LLM(
   model="novita/moonshotai/kimi-k2-instruct",
   temperature=0.5,
   api_base="https://api.novita.ai/v3/openai",
   api_key=os.environ['NOVITA_API_KEY']
)

这三个LLM实例对应我们方案中规划的模型,均托管在Novita平台,通过CrewAI访问。底层CrewAI使用LiteLLM连接和管理这些模型。

定义智能体

现在创建三个智能体:

architect = Agent(
   role="Software Architect for MVP Projects",
   goal="Define a basic system structure and simple feature set to guide MVP development",
   backstory="""You specialize in quickly outlining software architectures and project scopes for
minimum viable products. Your plans help guide coders with enough structure to get started while staying lean.""",
   llm=architect_llm,
   verbose=True
)

coder = Agent(
   role="Developer for MVP Projects",
   goal="Implement the MVP using simple, clear code, and write all necessary code files using the FileWriter tool",
   backstory="""You're a practical developer focused on speed. You prioritize working code over polish.
Simulate any runtime behavior if needed, and make sure to keep code clear and modular.""",
   llm=coder_llm,
   verbose=True,
   tools=[file_writer_tool]
)

reviewer = Agent(
   role="Code Reviewer for MVP Projects",
   goal="Read the code files, provide helpful feedback, and write improvements as diffs into a markdown file",
   backstory="""You're a fast but thoughtful reviewer. You check code for clarity, obvious bugs, and
provide improvements as diffs. Instead of modifying code directly, you 
save your suggestions in a markdown file
for easy inspection.""",
   llm=reviewer_llm,
   verbose=True,
   tools=[file_writer_tool]
)

每个智能体都被分配了特定的角色、目标和背景故事。

定义任务

接下来为智能体创建任务:

architect_task = Task(
   description="""
Create a basic plan for building a simple MVP version of a {project}.
Focus on the core features, basic file structure, and tech stack.
Make the structure minimal but enough to get a working demo.
""",
   expected_output="""
A simple architectural overview with:
- Project goals
- Key components or files
- Basic data flow or structure
""",
   agent=architect,
   output_file="architecture.md"
)

coder_task = Task(
   description="""
Based on the architect's plan, implement the core parts of the {project} MVP.
Keep it lean and functional. Use the FileWriter tool to save all your code files.
If you need to simulate behavior (e.g. without a real interpreter), do so with clear comments or mocked logic.
""",
   expected_output="""
Working code files saved using the FileWriter tool that implement the key features defined in the plan.
Code should be readable and logically structured.
""",
   agent=coder,
   context=[architect_task]
)

review_task = Task(
   description="""
Review the code files created for the {project} MVP. Read each file using the FileRead tool.
Suggest improvements using diffs. Do not modify the original code files directly.
Instead, save all your suggested diffs into a markdown file using the FileWriter tool.
""",
   expected_output="""
A markdown file containing diff-style suggestions for each file reviewed.
""",
   agent=reviewer,
   context=[coder_task],
   output_file="code_review_diffs.md"
)

创建团队

定义好智能体、对应的大语言模型以及任务后,现在我们将所有组件整合起来,组装成团队:

code_crew = Crew(
   agents=[architect, coder, reviewer],
   tasks=[architect_task, coder_task, review_task],
   process=Process.sequential,
   verbose=True
)

所有配置完成后,现在可以运行团队了:

if __name__ == "__main__":
   code_crew.kickoff(inputs={"project": "Todo App"})

现在我们已经有了一个功能完整的团队,能够根据用户定义的规格生成MVP项目。在上面的示例中,我们要求团队构建一个简单的待办事项应用。

其工作原理如下:

  1. 架构师智能体首先设计软件架构,并将方案保存到名为architecture.md的文件中。
  2. 编码员智能体随后获取架构师的输出,创建必要的目录和代码文件,实现待办事项应用。
  3. 最后,审核员智能体会审查生成的代码,通过差异对比形式提出改进建议,并将反馈保存到名为code_review_diffs.md的文件中。

这种基于团队的开发工作流功能强大且模块化,但也有一些局限:

  • 硬编码提示词:将提示词直接嵌入代码后,后续的维护和优化难度会大幅提升。
  • 结构分散:智能体和任务以内联方式定义,随着项目规模扩大,脚本会变得混乱且难以导航。

在下一节中,我们将介绍如何通过将智能体定义和任务提示词外置到YAML文件来优化结构。

构建基于类的团队

CrewAI支持创建基于类的团队。我们接下来将之前的代码更新为这种结构。首先创建一个名为config的文件夹,该文件夹将包含定义智能体和任务配置的YAML文件。

我们首先创建名为agents.yaml的文件,作为智能体配置文件:

architect:
 role: >
   Software Architect for MVP Projects
 goal: >
   Define a basic system structure and simple feature set to guide MVP development
 backstory: >
   You specialize in quickly outlining software architectures and project scopes for minimum viable products.
  Your plans help guide coders with enough structure to get started while staying lean.

coder:
 role: >
   Developer for MVP Projects
 goal: >
   Implement the MVP using simple, clear code, and write all necessary code files using the FileWriter tool
 backstory: >
   You're a practical developer focused on speed. You prioritize working code over polish.
  Simulate any runtime behavior if needed, and make sure to keep code clear and modular.

reviewer:
 role: >
   Code Reviewer for MVP Projects
 goal: >
   Read the code files, provide helpful feedback, and write improvements as diffs into a markdown file
 backstory: >
   You're a fast but thoughtful reviewer. You check code for clarity, obvious bugs, and provide improvements
  as diffs. Instead of modifying code directly, you save your suggestions in a markdown file for easy inspection.

该文件定义了每个智能体的角色、目标和背景故事。

接下来,我们创建tasks.yaml文件来存储任务配置:

请将所有建议的差异内容通过FileWriter工具保存到Markdown文件中。

architect_task:
 description: >
   Create a basic plan for building a simple MVP version of a {project}.
  Focus on the core features, basic file structure, and tech stack.
  Make the structure minimal but enough to get a working demo.
 expected_output: >
   A simple architectural overview with:
  - Project goals
  - Key components or files
  - Basic data flow or structure
 agent: architect
 output_file: architecture.md

coder_task:
 description: >
   Based on the architect's plan, implement the core parts of the {project} MVP.
  Keep it lean and functional. Use the FileWriter tool to save all your code files.
  If you need to simulate behavior (e.g. without a real interpreter), do so with clear comments or mocked logic.
 expected_output: >
   Working code files saved using the FileWriter tool that implement the key features defined in the plan.
  Code should be readable and logically structured.
 agent: coder
 context:
   - architect_task

review_task:
 description: >
   Review the code files created for the {project} MVP. Read each file using the FileRead tool.
  Suggest improvements using diffs. Do not modify the original code files directly.
  Instead, save all your suggested diffs into a markdown file using the FileWriter tool.
 expected_output: >
   A markdown file containing diff-style suggestions for each file reviewed.
 agent: reviewer
 context:
   - coder_task
 output_file: code_review_diffs.md

接下来,我们创建基于类的团队。为此,我们需要定义一个类,并使用CrewAI提供的@CrewBase装饰器对其进行装饰:

@CrewBase
class CodeCrew():
   """Three-agent code crew: architect, coder, reviewer"""

   @agent
   def architect(self) -> Agent:
       pass

   @agent
   def coder(self) -> Agent:
       pass

   @agent
   def reviewer(self) -> Agent:
       pass

   @task
   def architect_task(self) -> Task:
       pass

   @task
   def code_task(self) -> Task:
       pass

   @task
   def review_task(self) -> Task:
       pass

   @crew
   def crew(self) -> Crew:
       pass

在这个结构中:

  • @agent装饰器用于定义生成智能体的方法。
  • @task装饰器用于标记生成任务的方法。
  • @crew装饰器用于指定组装整个团队的方法。

现在,我们来看完整的实现代码:

import os
from crewai import Agent, Crew, Process, Task, LLM
from crewai.project import CrewBase, agent, crew, task
from crewai.agents.agent_builder.base_agent import BaseAgent
from typing import List

from crewai_tools import FileWriterTool

# Instantiate tool
file_writer_tool = FileWriterTool()

@CrewBase
class CodeCrew():

   agents: List[BaseAgent]
   tasks: List[Task]

   agents_config = 'config/agents.yaml'
   tasks_config = 'config/tasks.yaml'
  
   @agent
   def architect(self) -> Agent:
       llm = LLM(
           model="novita/moonshotai/kimi-k2-instruct",
           temperature=0.5,
           api_base="https://api.novita.ai/v3/openai",
           api_key=os.environ['NOVITA_API_KEY']
       )
       return Agent(
           config=self.agents_config['architect'],
           llm=llm,
           verbose=True
       )

   @agent
   def coder(self) -> Agent:
       llm = LLM(
           model="novita/qwen/qwen3-coder-480b-a35b-instruct",
           temperature=0.4,
           api_base="https://api.novita.ai/v3/openai",
           api_key=os.environ['NOVITA_API_KEY']
       )
       return Agent(
           config=self.agents_config['coder'],
           llm=llm,
           verbose=True,
           tools=[file_writer_tool]
       )

   @agent
   def reviewer(self) -> Agent:
       llm = LLM(
           model="novita/moonshotai/kimi-k2-instruct",
           temperature=0.5,
           api_base="https://api.novita.ai/v3/openai",
           api_key=os.environ['NOVITA_API_KEY']
       )
       return Agent(
           config=self.agents_config['reviewer'],
           llm=llm,
           verbose=True,
       )

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

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

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

   @crew
   def crew(self) -> Crew:
       return Crew(
           agents=self.agents,
           tasks=self.tasks,
           process=Process.sequential,
           verbose=True
       )

这段代码定义了所有必要的方法,包含智能体和任务的属性,同时指定了配置文件路径,供智能体和任务方法加载各自配置使用。

现在,你可以将以下代码添加到脚本中运行团队:

def main():
   code_crew = CodeCrew()
   code_crew.crew().kickoff(inputs={"project": "Todo App"})
if __name__ == '__main__':
   main()

使用Novita构建工作流(Flows)

之前我们构建了一个采用顺序流程的团队,现在我们来探索如何构建遵循自定义工作流的工作流(Flow)。

我们的工作流设计

我们想要设计一个客户支持多智能体系统,由一个主导智能体负责分类客户问题,系统根据分类结果判断哪个专业智能体最适合处理该问题。该架构包含四个智能体:

  • 客户智能体:这是主导智能体,负责接收客户问题并将其分类为以下三类之一:账单类技术类通用类
  • 账单智能体:处理所有与账单相关的问题。
  • 技术智能体:处理技术类问题。
  • 通用智能体:处理不属于上述类别的任何问题。

客户支持工作流

定义工作流状态

CrewAI中的工作流具有共享状态,该状态对工作流内的所有智能体和团队都可见。对于我们的客户支持工作流,状态包含问题标签和用户查询内容。

我们首先导入所需的模块并定义状态:

import json
import os
from typing import Literal, Dict, Any
from pydantic import BaseModel, ValidationError
from crewai import Agent, LLM
from crewai.flow.flow import Flow, start, router, listen

# Define the flow state
class SupportState(BaseModel):
   issue: Literal["billing", "technical", "general"] = "general"
   message: str = ""

创建工作流

CrewAI中的工作流基于图结构,采用事件驱动系统处理节点之间的转换。要创建我们的工作流,首先需要一个用于分类问题的节点,随后是一个路由器节点,根据分类结果决定将请求路由到哪个处理节点。

class CustomerSupportFlow(Flow[SupportState]):

   @start()
   def classify_issue(self):
       pass

   @router(classify_issue)
   def route_based_on_issue(self) -> str:
       print(f"📨 Routing to agent for issue type: {self.state.issue}")
       if self.state.issue == "billing":
           return "billing"
       if self.state.issue == "technical":
           return "technical"
       else:
           return "general"       

   @listen("billing")
   def handle_billing(self):
       pass

   @listen("technical")
   def handle_technical(self):
       pass

   @listen("general")
   def handle_general(self):
       pass

使用@start()装饰的方法是工作流的入口点,本例中负责问题分类。使用@router()装饰的方法会在classify_issue之后执行,用于决定将请求路由到哪个智能体。其他方法采用事件驱动机制,在收到路由请求时做出响应。

实现工作流方法

现在实现每个方法,并为每个方法分配对应的智能体:

class CustomerSupportFlow(Flow[SupportState]):

   @start()
   def classify_issue(self) -> Dict[str, Any]:
       intake_agent = Agent(
           role="User Intake Agent",
           goal="Classify user support issue and summarize the message",
           backstory="You classify the user query into billing, technical, or general categories.",
           llm=LLM(
               model="novita/qwen/qwen2.5-vl-72b-instruct",
               temperature=0.3,
               api_base="https://api.novita.ai/v3/openai",
               api_key=os.environ["NOVITA_API_KEY"]
           ),
           verbose=True
       )
      
       prompt = f"""
           A user submitted this message: "{self.state.message}"

           Your task:
           1. Identify whether the issue is "billing", "technical", or "general".
           2. Rephrase or extract the message clearly.

           Respond in valid JSON format like the example below:

           {{
               "issue": "billing",
               "message": "The user has been charged twice for their subscription and is requesting a refund."
           }}
       """

       output = intake_agent.kickoff(prompt, response_format=SupportState)
      
       try:
           # Parse the JSON string
           print(output.raw)
           parsed_json = json.loads(output.raw)

           # Validate with Pydantic
           validated = SupportState(**parsed_json)

           # Save to state
           self.state.issue = validated.issue
           self.state.message = validated.message

       except (json.JSONDecodeError, ValidationError) as e:
           print("❌ Failed to parse or validate response:", e)
           self.state.issue = "general"

   @router(classify_issue)
   def route_based_on_issue(self) -> str:
       print(f"📨 Routing to agent for issue type: {self.state.issue}")
       if self.state.issue == "billing":
           return "billing"
       if self.state.issue == "technical":
           return "technical"
       else:
           return "general"       

   @listen("billing")
   def handle_billing(self):
       agent = Agent(
           role="Billing Agent",
           goal="Handle billing questions, refunds, and invoice issues",
           backstory="You resolve billing-related queries effectively.",
           llm=LLM(
               model="novita/meta-llama/llama-3.3-70b-instruct",
               temperature=0.5,
               api_base="https://api.novita.ai/v3/openai",
               api_key=os.environ["NOVITA_API_KEY"]
           ),
           verbose=True
       )
       result = agent.kickoff(self.state.message)
       print("💰 Billing Agent Response:", result)

   @listen("technical")
   def handle_technical(self):
       agent = Agent(
           role="Technical Support Agent",
           goal="Help users resolve technical issues",
           backstory="You're a technical expert providing troubleshooting support.",
           llm=LLM(
               model="novita/meta-llama/llama-3.1-8b-instruct",
               temperature=0.5,
               api_base="https://api.novita.ai/v3/openai",
               api_key=os.environ["NOVITA_API_KEY"]
           ),
           verbose=True
       )
       result = agent.kickoff(self.state.message)
       print("🛠 Technical Agent Response:", result)

   @listen("general")
   def handle_general(self):
       agent = Agent(
           role="General Support Agent",
           goal="Provide helpful answers to non-technical and non-billing questions",
           backstory="You're friendly and well-informed about our services.",
           llm=LLM(
               model="novita/minimaxai/minimax-m1-80k",
               temperature=0.5,
               api_base="https://api.novita.ai/v3/openai",
               api_key=os.environ["NOVITA_API_KEY"]
           ),
           verbose=True
       )
       result = agent.kickoff(self.state.message)
       print("📋 General Agent Response:", result)

classify_issue方法使用Pydantic确保模型返回有效的响应。配置完成后,我们可以使用以下代码运行工作流:

def run():
   flow = CustomerSupportFlow()
   flow.plot("CustomerSupportFlowPlot")

   # Example input message
   example_input = {
       "message": "Hello, I was charged twice for my subscription and need a refund."
   }

   flow.kickoff(inputs=example_input)

if __name__ == "__main__":
   run()

总结

构建多智能体系统需要两个核心组件:提供丰富模型选择的模型供应商,以及用于编排智能体交互的多智能体框架。在本文中,我们展示了Novita和CrewAI如何高效扮演这两个角色。想要了解更多关于CrewAI的信息,可以查阅其官方文档。如果你希望为团队试验更多模型,可以访问Novita的LLM playground

代码仓库(含所有配置文件)- https://github.com/novitalabs/Novita-CollabHub/tree/main/examples/novita-crewai

关于Novita AI

Novita AI是一个AI云平台,为开发者提供便捷的API来部署AI模型,同时提供高性价比、稳定可靠的GPU云服务,支持AI应用的构建与扩展。