详细教程展示如何用CrewAI和CopilotKit构建餐厅查询Agent,涵盖后端Agent逻辑与前端集成的完整工作流。
CopilotKit 现已支持 Angular
Angular 支持 · 使用 Angular 构建智能体应用与生成式 UI · 现已推出
在本文中,你将学习如何使用 CrewAI、CopilotKit 和 Serper 构建一个全栈餐厅查找 AI 智能体,并结合人在回路(human-in-the-loop)能力。
在正式开始之前,以下是我们将介绍的内容:
什么是 CrewAI 智能体?
构建、运行和部署 CrewAI 智能体
使用 Copilot Cloud 和 CopilotKit 为 CrewAI 智能体添加前端 UI
下面是我们将要构建的应用预览。
假设你正在参与一个大型团队项目,比如构建应用或游戏。你有一个团队,其中每个人都有明确的职责。
一个人擅长编写后端代码,另一个人负责设计 UI,还有人负责测试,等等。
同样,你可以将 CrewAI 智能体想象成一支由智能自动化助手组成的团队——每个助手都有自己的角色——它们相互协作,共同解决问题或完成任务。
你可以在 CrewAI 文档中进一步了解 CrewAI 智能体。
CopilotKit 是一个开源的全栈框架,用于构建可与用户交互的智能体和 Copilot。它能让智能体控制你的应用、传达自己正在执行的操作,并生成完全自定义的 UI。
欢迎查看 CopilotKit 的 GitHub ⭐️
要充分理解本教程,你需要对 React 或 Next.js 有基本的了解。
我们还将使用以下内容:
Python——一种常用于通过 LangGraph 构建 AI 智能体的流行编程语言;请确保你的计算机上已经安装了 Python。
OpenAI API——让我们能够使用 GPT 模型执行各种任务;对于本教程,请确保你有权使用 GPT-4 模型。
CopilotKit——一个开源 Copilot 框架,可用于构建自定义 AI 聊天机器人、应用内 AI 智能体和文本输入区域。
CrewAI——一个 Python 框架,使开发者能够以高度简洁的方式创建自主 AI 智能体,同时对底层细节进行精确控制。
SerperTool——一个使用 serper.dev API 的工具,可根据用户提供的查询获取并展示最相关的搜索结果。
在本节中,你将学习如何使用 CrewAI 包构建并运行 CrewAI 智能体。然后,你将学习如何通过 GitHub 将 AI 智能体部署到 CrewAI 企业平台。
首先,克隆 restaurant finder crew 仓库,其中包含基于 Python 的 CrewAI Crew 智能体代码:
git clone https://github.com/TheGreatBonnie/restaurant-finder.git
该仓库包含一个 CrewAI Crew 智能体,其结构如下:
restaurant-finder/├── .gitignore├── pyproject.toml├── README.md├── .env└── src/ └── restaurant_finder_agent/ ├── init.py ├── main.py ├── crew.py ├── tools/ │ ├── custom_tool.py │ └── init.py └── config/ ├── agents.yaml └── tasks.yaml
下面是 research finder CrewAI Crew 中的核心文件:
agents.yaml——定义 AI 智能体及其角色
tasks.yaml——设置智能体任务和工作流
.env——存储 API 密钥和环境变量
main.py——项目入口点和执行流程
crew.py——Crew 编排与协调
tools/——自定义智能体工具目录
接下来,在根目录中创建一个 .env 文件。然后,将 OpenAI 和 Serper API 密钥添加到环境变量中。
OPENAI_API_KEY=your-openai-api-key
SERPER_API_KEY=your-serper-api-key
然后,使用 CrewAI 安装 research finder CrewAI Crew 的所有依赖项。如果尚未安装 CrewAI 包,请按照 CrewAI 文档中的安装指南进行操作。
crewai install
要运行 restaurant CrewAI crew 智能体,请在命令行中执行以下命令。
crewai run
Crew 启动后,将使用 Serper 网络搜索工具查找旧金山的餐厅。随后,它会整理一份餐厅列表并征求你的反馈,如下所示。
在终端中回复“Looks good”消息并按 Enter 键。项目文件夹中应该会保存一个包含旧金山餐厅推荐的文件,如下所示。
要部署 restaurant crew,请将 restaurant finder crew 代码推送到 GitHub 仓库,如下所示。
然后登录 CrewAI。在控制面板中,将你的 GitHub 账户与 CrewAI 进行配置,使其能够访问 restaurant finder 仓库。
接下来,选择 restaurant finder 仓库。然后将 OpenAI 和 Serper API 密钥添加到环境变量中,如下所示,并单击 Deploy 按钮。
restaurant finder crew 应该会立即开始部署,如下所示。请注意,首次部署 crew 最长可能需要 10 分钟。
Crew 部署完成后,将其打开,然后获取它的 URL 和 bearer token。该 crew 的 URL 和 bearer token 将用于在 Copilot Cloud 中注册 crew。
现在,我们已经学习了如何构建、运行和部署 CrewAI crew 智能体,接下来看看如何添加一个前端 UI 与它进行聊天。
在本节中,你将学习如何使用 Copilot Cloud 和 CopilotKit 为 CrewAI crew 智能体添加前端 UI。
要注册 CrewAI crew 智能体,请前往 Copilot Cloud,登录并单击 Get Started 按钮。
然后,将你的 OpenAI API 密钥添加到“Provide OpenAI API Key”部分,如下所示。
接下来,向下滚动到 remote endpoints 部分,并单击 Add New 按钮。
然后,在弹出的模态窗口中选择 remote endpoint。之后,添加你的 CrewAI 智能体端点 URL、bearer token、名称和描述,如下所示。然后单击 Save Endpoint 按钮。
保存 crew 端点后,复制 Copilot Cloud Public API Key,如下所示。
要构建 CrewAI 智能体前端 UI,首先克隆 restaurant finder UI 仓库,其中包含一个 Next.js 项目的代码:
git clone https://github.com/TheGreatBonnie/restaurant-finder-ui.git
接下来,在根目录中创建一个 .env 文件。然后,将你的 CrewAI 智能体名称和 Copilot Cloud Public API Key 添加到环境变量中。
NEXT_PUBLIC_AGENT_NAME=restaurant_finder
NEXT_PUBLIC_CPK_PUBLIC_API_KEY=your-copilot-cloud-api-key
之后,使用 pnpm 安装前端依赖项。
pnpm install
然后,使用以下命令启动应用。
pnpm run dev
访问 http://localhost:3000/,你应该会看到 restaurant finder CrewAI 智能体前端已经启动并正常运行。
现在,让我们看看如何使用 CopilotKit 为 CrewAI 智能体构建前端 UI。
要设置 CopilotKit Provider,必须使用 <CopilotKit> 组件包裹应用中需要感知 Copilot 的部分。对于大多数使用场景,适合用 CopilotKit provider 包裹整个应用,例如在 layout.tsx 中进行设置,如下面的 src/app/layout.tsx 文件所示。
// Import CopilotKit React UI specific styles
import "@copilotkit/react-ui/styles.css";
// Import the CopilotKit component for AI integration
import { CopilotKit } from "@copilotkit/react-core";
// Define metadata for the application
export const metadata: Metadata = {
// Set the page title
title: "CopilotKit Crew Demo",
// Set the page description for SEO and previews
description: "Talk to your Crew",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className="h-full">
<body className={` antialiased h-full`}>
{/* CopilotKit wrapper for AI functionality */}
<CopilotKit
// Hide the development console in production
showDevConsole={false}
// Set the agent name from environment variables
agent={process.env.NEXT_PUBLIC_AGENT_NAME}
// Set the public API key from environment variables
publicApiKey={process.env.NEXT_PUBLIC_CPK_PUBLIC_API_KEY}>
{children}
</CopilotKit>
</body>
</html>
);
}
要启动 CrewAI 智能体、渲染 crew 状态和进度、处理人工反馈并以流式方式传输智能体的响应,你需要创建一个 CrewQuickstart 组件,如 src/components/CrewQuickstart.tsx 文件所示。
"use client";
```typescript
// 从 CopilotKit 导入 crew 和聊天功能所需的 hooks 和类型
import {
CrewsAgentState,
useCoAgent,
useCopilotChat,
useCopilotAdditionalInstructions,
} from "@copilotkit/react-core";
// 导入 React hooks 用于状态和副作用处理
import { useEffect, useState } from "react";
// 导入用于聊天功能的消息类型
import { MessageRole, TextMessage } from "@copilotkit/runtime-client-gql";
// 导入用于可调整大小面板的 UI 组件
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from "./ui/resizable";
// 导入用于窗口尺寸检测的自定义 hook
import { useWindowSize } from "@/hooks/useWindowSize";
// 定义组件的 props 接口
interface CrewQuickstartProps {
crewName: string; // crew/智能体的名称
inputs: Array<string>; // 用户输入字段名称数组
}
// 导出带有类型化 props 的主组件
export const CrewQuickstart: React.FC<CrewQuickstartProps> = ({
crewName,
inputs,
}: {
crewName: string;
inputs: Array<string>;
}) => {
// 用于追踪初始聊天消息是否已发送的状态
const [initialMessageSent, setInitialMessageSent] = useState(false);
// 从自定义 hook 获取移动端检测信息
const { isMobile } = useWindowSize();
// 面板布局方向的状态(桌面端水平,移动端垂直)
const [direction, setDirection] = useState<"horizontal" | "vertical">(
"horizontal"
);
// 根据移动端状态更新布局方向的效果钩子
useEffect(() => {
setDirection(isMobile ? "vertical" : "horizontal");
}, [isMobile]);
// 使用 CopilotKit 的 useCoAgent hook 设置 crew/智能体和自定义状态
const { state, setState, run } = useCoAgent<
CrewsAgentState & {
result: string; // crew 执行的最终结果
inputs: Record<string, string>; // 存储用户输入的对象
}
>({
name: crewName, // crew 的名称
initialState: {
inputs: {}, // 初始空输入对象
result: "Crew result will appear here...", // 默认结果消息
},
});
// 通过移除非字母数字字符来清理 crewName 以用于显示
const agentName = crewName.replace(/[^a-zA-Z0-9]/g, " ");
// 渲染组件 UI
return (
// 占满全部宽度和高度的容器 div
<div className="w-full h-full relative">
{/* 用于布局的可调整大小的面板组 */}
<ResizablePanelGroup direction={direction} className="w-full h-full">
{/* 用于聊天的左侧/主面板(此版本为空) */}
<ResizablePanel defaultSize={60} minSize={30}>
{/* 聊天组件的占位符 */}
</ResizablePanel>
{/* 用于调整面板大小的句柄 */}
<ResizableHandle withHandle />
{/* 用于 crew 状态/结果的右侧面板 */}
<ResizablePanel defaultSize={40} minSize={25}>
{/* 带有样式的可滚动容器 */}
<div className="h-full overflow-y-auto bg-gray-50 dark:bg-gray-900 p-3">
<div className="flex flex-col h-full">
{/* 带有 crew 名称的页头 */}
<div className="flex items-center justify-between mb-2">
<h1 className="text-lg font-medium text-gray-800 dark:text-gray-200">
{agentName}
</h1>
</div>
{/* 内容区域 */}
<div className="h-full">
{/* 结果的样式容器 */}
<div className="text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 rounded-md shadow-sm p-4 h-full overflow-y-auto prose dark:prose-invert max-w-none">
{/* 结果内容的占位符 */}
</div>
</div>
</div>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
);
};
创建 CrewQuickstart 组件后,需要将其导入到主页面中,如下所示在 src/app/page.tsx 文件中。
"use client";
import React from "react";
// 从 components 目录导入自定义 CrewQuickstart 组件
import { CrewQuickstart } from "@/components/CrewQuickstart";
export default function Home() {
return (
<div className="w-full h-full relative">
{/* 使用特定的 props 渲染 CrewQuickstart 组件 */}
<CrewQuickstart
crewName="Restaurant Finder" // crew/智能体的名称
inputs={["location"]} // 指定所需用户输入的数组
/>
</div>
);
}
要设置你的 Copilot UI,首先在根组件中导入默认样式(通常是 layout.tsx)。
import "@copilotkit/react-ui/styles.css";
Copilot UI 提供多种内置 UI 模式;从 CopilotPopup、CopilotSidebar、CopilotChat 或无头 UI 中选择你喜欢的。
在这个例子中,我们将使用在 src/components/Chat.tsx 文件中定义的 CopilotChat。
// 在 Next.js 中将此组件声明为客户端组件
"use client";
import React from "react";
// 从 CopilotKit 的 React UI 包中导入特定的类型和组件
import { CopilotKitCSSProperties, CopilotChat } from "@copilotkit/react-ui";
function Chat() {
return (
<div
className="h-full relative overflow-y-auto"
style={
{
// 为 CopilotKit 的主要颜色定义自定义 CSS 变量
"--copilot-kit-primary-color": "#4F4F4F",
} as CopilotKitCSSProperties // 用于 CopilotKit 特定 CSS 属性的类型断言
}>
{/* 用于聊天界面的 CopilotChat 组件 */}
<CopilotChat
// instructions 属性为 AI 助手提供指导
instructions={
"You are assisting the user as best as you can. Answer in the best way possible given the data you have."
}
// 聊天界面的自定义标签
labels={{
// 聊天页头中显示的标题
title: "Your Assistant",
// 聊天开始时显示的初始消息
initial:
"Hi! 👋 Please provide the location you want to find a restaurant before we get started.",
}}
// 使用 Tailwind CSS 的聊天组件样式类
className="h-full flex flex-col"
// 自定义图标配置
icons={{
// 在加载状态期间显示的自定义加载图标
spinnerIcon: (
// 带有动画脉动点的 span 元素
<span className="h-5 w-5 text-gray-500 animate-pulse">...</span>
),
}}
/>
</div>
);
}
export default Chat;
聊天组件随后在 src/components/CrewQuickstart.tsx 文件中导入并使用。聊天被渲染到前端 UI 上,如下所示。
要启动你的 CrewAI 智能体,需要发送初始欢迎消息、定义收集用户输入的操作、定义确认用户输入的效果,以及定义在智能体运行前确保添加用户输入的指令,如下所示在 src/components/CrewQuickstart.tsx 文件中。
"use client";
// 导入 CopilotKit hooks 和类型用于 crew 和聊天功能
import {
CrewsAgentState,
useCoAgent, // 用于管理 crew 状态和执行的 hook
useCopilotChat, // 用于聊天功能的 hook
useCopilotAdditionalInstructions, // 用于添加 crew 指令的 hook
useCopilotAction, // 用于定义 crew 操作的 hook
} from "@copilotkit/react-core";
// 导入用于状态和副作用的 React hooks
import { useEffect, useState } from "react";
// 导入用于聊天的消息类型
import { MessageRole, TextMessage } from "@copilotkit/runtime-client-gql";
// 定义组件的 props 接口
interface CrewQuickstartProps {
crewName: string; // crew/智能体的名称
inputs: Array<string>; // 用户需要提供的输入字段名称数组
}
// 导出带有类型化 props 的 CrewQuickstart 组件
export const CrewQuickstart: React.FC<CrewQuickstartProps> = ({
crewName,
inputs,
}: {
crewName: string;
inputs: Array<string>;
}) => {
// 用于追踪初始欢迎消息是否已发送的状态
const [initialMessageSent, setInitialMessageSent] = useState(false);
// 使用 useCoAgent hook 设置带有自定义状态的 crew
const { state, setState, run } = useCoAgent<
CrewsAgentState & {
result: string; // 存储最终的 crew 执行结果
inputs: Record<string, string>; // 将用户提供的输入存储为键值对
}
>({
name: crewName, // 从 props 设置 crew 名称
initialState: {
inputs: {}, // 初始为空的输入对象
result: "Crew result will appear here...", // 默认结果占位符
},
});
// 从 useCopilotChat hook 获取聊天功能
const { appendMessage, isLoading } = useCopilotChat();
// 定义需要输入的指令,在 crew 执行前必须调用
const instructions =
"INPUTS ARE ABSOLUTELY REQUIRED. Please call getInputs before proceeding with anything else.";
// 组件挂载时发送初始欢迎消息的 Effect
useEffect(() => {
if (initialMessageSent || isLoading) return; // 如果已发送或正在加载,则跳过
setTimeout(async () => {
// 向聊天窗口追加欢迎消息
await appendMessage(
new TextMessage({
content: "Hi, Please provide your inputs before we get started.",
role: MessageRole.Developer, // 归属于开发者角色
})
);
setInitialMessageSent(true); // 标记消息已发送
}, 0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 空依赖数组:在挂载时运行一次
// 提供输入后在聊天中确认输入的 Effect
useEffect(() => {
if (!initialMessageSent && Object.values(state?.inputs || {}).length > 0) {
// 如果输入存在且未发送初始消息,则在聊天中显示输入
appendMessage(
new TextMessage({
role: MessageRole.Developer,
content: "My inputs are: " + JSON.stringify(state?.inputs),
})
).then(() => {
setInitialMessageSent(true); // 追加后标记为已发送
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialMessageSent, state?.inputs]); // 依赖于消息状态和输入
// 添加带有条件可用性的指令
useCopilotAdditionalInstructions({
instructions, // 上面定义的指令
available:
Object.values(state?.inputs || {}).length > 0 ? "enabled" : "disabled", // 仅在提供输入时启用
});
// 定义操作以从用户收集输入
useCopilotAction({
name: "getInputs", // 操作名称
followUp: false, // 无后续操作
description:
"This action allows Crew to get required inputs from the user before starting the Crew.",
renderAndWaitForResponse({ status }) {
// 渲染表单并等待提交
if (status === "inProgress" || status === "executing") {
return (
// 用于收集输入的表单
<form
className="flex flex-co
一条初始消息和用于收集用户输入的表单组件在前端 UI 中呈现,如下所示。
要渲染你的 CrewAI AI 智能体状态和进度,你需要定义一个 CrewStateRenderer 组件来实时可视化 AI 智能体的步骤和任务状态,如下所示的 src/components/CrewStateRenderer.tsx 文件中所示。
"use client";
// 导入 CopilotKit 类型用于 Crew 状态管理
import {
CrewsAgentState, // 整体 Crew 状态的类型
CrewsResponseStatus, // Crew 执行状态的类型
CrewsTaskStateItem, // 任务项的类型
CrewsToolStateItem, // 工具项的类型
} from "@copilotkit/react-core";
// 导入 React hooks 和工具函数
import { useEffect, useMemo, useRef, useState } from "react";
// 导入 ReactMarkdown 用于渲染 markdown 内容
import ReactMarkdown from "react-markdown";
/**
* 实时渲染你的 Crew 的步骤和任务。
* @param state - Crew 的当前状态
* @param status - Crew 的当前执行状态
*/
function CrewStateRenderer({
state,
status,
}: {
state: CrewsAgentState; // 包含步骤和任务的 Crew 状态
status: CrewsResponseStatus; // 状态如 "inProgress" 或 "complete"
}) {
// 状态,用于跟踪渲染器是折叠还是展开
const [isCollapsed, setIsCollapsed] = useState(true);
// Ref 用于访问内容 div 以便滚动
const contentRef = useRef<HTMLDivElement>(null);
// Ref 用于跟踪之前的项目数以检测新项目
const prevItemsLengthRef = useRef<number>(0);
// 状态,用于跟踪要高亮的项目(新添加的)
const [highlightId, setHighlightId] = useState<string | null>(null);
// 用于合并和排序步骤和任务的记忆化计算
const items = useMemo(() => {
if (!state) return []; // 如果没有状态则返回空数组
// 合并步骤和任务,按时间戳排序
return [...(state.steps || []), ...(state.tasks || [])].sort(
(a, b) =>
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime() // 按时间戳升序排序
);
}, [state]); // 状态改变时重新计算
// 用于高亮新项目并自动滚动的 Effect
useEffect(() => {
if (!state) return; // 如果没有状态则跳过
if (items.length > prevItemsLengthRef.current) {
// 检查是否添加了新项目
const newestItem = items[items.length - 1]; // 获取最新项目
setHighlightId(newestItem.id); // 高亮它
setTimeout(() => setHighlightId(null), 1500); // 1.5 秒后清除高亮
// 如果展开则自动滚动到底部
if (contentRef.current && !isCollapsed) {
contentRef.current.scrollTop = contentRef.current.scrollHeight;
}
}
prevItemsLengthRef.current = items.length; // 更新之前的长度
}, [items, isCollapsed, state]); // 依赖于项目、折叠状态和 Crew 状态
// 如果未提供状态则加载状态
if (!state) {
return <div>Loading crew state...</div>;
}
// 如果折叠、为空且未在进行中则隐藏组件
if (isCollapsed && items.length === 0 && status !== "inProgress") return null;
// 渲染 UI
return (
<div className="mt-2 text-sm">
{/* 切换标题 */}
<div
className="flex items-center cursor-pointer" // Flex 布局和指针光标
onClick={() => setIsCollapsed(!isCollapsed)} // 切换折叠状态
>
<span className="mr-1">{isCollapsed ? "▶" : "▼"}</span>{" "}
<span className="text-gray-700">
{status === "inProgress" ? "Crew is analyzing..." : "Crew analysis"} {" "}
</span>
</div>
{/* 内容区域,仅在展开时显示 */}
{!isCollapsed && (
<div
ref={contentRef} // 用于滚动的引用
className="max-h-[200px] overflow-auto border-l border-gray-200 pl-2 ml-1 mt-1">
{items.length > 0 ? ( // 检查是否有要渲染的项目
items.map((item) => {
// 遍历排序后的项目
const isTool = (item as CrewsToolStateItem).tool !== undefined; // 检查项目是否为工具
const isHighlighted = item.id === highlightId; // 检查项目是否应高亮
return (
<div
key={item.id} // 每个项目的唯一键
className={`mb-2 ${is
CrewStateRenderer 组件随后被导入到 CrewQuickstart 组件中。