介绍框架组合方案快速为 AI Agent 搭建前端界面。实用的开发工具组合,程序员可直接应用到项目。
在本文中,你将学习如何使用 LangGraph、CopilotKit 和 Tavily 构建一个以 AI 智能体为原生核心、支持人在回路能力的研究画布应用。
在开始之前,以下是本文将涵盖的内容:
使用 LangGraph Studio 构建并可视化 LangGraph AI 智能体
使用 CopilotKit 为 LangGraph AI 智能体构建 UI
下面是我们将要构建的应用预览。
简单来说,AI 智能体是能够利用人工智能执行任务、做出决策并与其环境交互的自主软件程序。
在本文的场景中,它们是能够开展研究、处理信息,并在执行过程中与人类互动,以确保可靠性和可信度的系统。
你可以在 CopilotKit 文档中阅读更多关于 AI 智能体的内容。
CopilotKit 是一个开源的全栈框架,用于构建可与用户交互的智能体和 Copilot。它能让你的智能体控制应用、告知用户自己正在做什么,并生成完全自定义的 UI。
查看 CopilotKit 的 GitHub ⭐️
为了充分理解本教程,你需要具备 React 或 Next.js 的基础知识。
我们还将使用以下工具:
Python——一种使用 LangGraph 构建 AI 智能体的流行编程语言;请确保你的计算机上已经安装了它。
LangGraph——一个用于创建和部署 AI 智能体的框架。它还有助于定义智能体要执行的控制流和操作。
OpenAI API Key——使我们能够使用 GPT 模型执行各种任务;在本教程中,请确保你拥有 GPT-4 模型的访问权限。
Tavily AI——一个搜索引擎,使 AI 智能体能够在应用内开展研究并获取实时知识。
CopilotKit——一个开源 Copilot 框架,用于构建自定义 AI 聊天机器人、应用内 AI 智能体和文本区域。
Docker——一个用于在容器中开发、交付和运行应用的平台。
在本节中,你将学习如何使用 Docker 构建并启动 LangGraph 智能体,以及如何使用 LangGraph Studio 可视化其工作流。
首先,克隆以 AI 智能体为原生核心的研究画布应用仓库,其中包含基于 Python 的 LangGraph 智能体代码:
git clone https://github.com/CopilotKit/open-research-ANA.git
该仓库包含两个文件夹:智能体和前端。要启动智能体,请进入 agent 目录。
cd agent
然后使用 pip 安装智能体的所有依赖项。
pip install -r requirements.txt
接下来,在 agent 目录中创建一个 .env 文件,然后将 OpenAI、Tavily 和 LangSmith API Key 添加到环境变量中。
OPENAI_API_KEY=your_key
TAVILY_API_KEY=your_key
LANGSMITH_API_KEY=your_key
如果打开 agent/graph.py 文件,你会看到其中定义了一个用于执行研究工作流的 MasterAgent 类。
它使用有向图(StateGraph)管理 LangGraph AI 智能体节点、工具执行和人工反馈之间的状态与转换。
该工作流旨在通过收集数据、提出大纲并撰写各个章节来协助生成研究报告,同时允许用户通过使用 CopilotKit 集成的前端提供反馈。
要启动 LangGraph AI 智能体,请打开 Docker 应用并运行以下命令:
langgraph up
LangGraph API 服务器启动后,使用所提供的 LangGraph Studio 链接进入 LangGraph Studio。请记下输出中的 API URL(例如 http://localhost:8123)。稍后我们将使用它,通过 CopilotKit Cloud 将智能体连接到前端。
之后,LangGraph 智能体将在 LangGraph Studio 中打开,你可以对其进行可视化,如下所示。
要测试 LangGraph 智能体,请向 messages 状态变量添加一条消息,然后单击 Submit 按钮。
随后,智能体将沿着相连节点所定义的工作流处理输入,并在线程中回复你的消息,如下所示。
在继续之前,我们先来讨论智能体式 Copilot 中的一个关键概念:人在回路(Human-in-the-Loop,HITL)。HITL 允许智能体在执行过程中请求人工输入或审批,从而提高 AI 系统的可靠性和可信度。
你可以在 CopilotKit 文档中阅读更多关于人在回路的内容。
在本例中,你可以通过单击其中一个节点并勾选 Interrupt After 复选框,为智能体添加 HITL,如下所示。
然后向 messages 状态变量添加另一条消息,例如“research about AI models”,并单击 Submit 按钮。智能体将开始研究 AI 模型。研究完成后,它会要求你审阅各个章节,并提供反馈或说明你希望进行的具体修改,如下所示。
向 messages 状态变量添加一条“yes”消息,然后单击 Submit 按钮。智能体将处理这条消息,并为 AI 模型报告提供大纲提案。随后,它会询问你是否希望批准该大纲,或者是否需要进行任何修改,如下所示。
回复“I would like to approve the outline”消息,然后单击 Submit 按钮。随后,智能体将编写一份按不同章节组织的 AI 模型报告,并完成研究流程,如下所示。
现在,我们已经了解了如何使用 LangGraph Studio 可视化并测试 LangGraph AI 智能体。接下来看看如何添加一个用于与其交互的前端 UI。
在本节中,你将学习如何使用 CopilotKit Cloud,将 LangGraph AI 智能体连接到 CopilotKit 前端 UI。
要为 LangGraph AI 智能体创建隧道,使 Copilot Cloud 能够连接到它,请使用以下命令。还记得我让你在启动智能体时记下的 API URL 吗?请使用其中提供的端口号。在我的示例中,端口号是 8123。
npx copilotkit@latest dev --port 8123
选择一个项目,隧道就会启动并连接到 Copilot Cloud,如下所示。
然后进入 frontend 文件夹。
cd frontend
之后,使用 pnpm 安装前端依赖项。
pnpm install
接下来,在 frontend 目录中创建一个 .env 文件,然后将 OpenAI、Copilot Cloud 和 LangSmith API Key 添加到环境变量中。
OPENAI_API_KEY=your_openai_key
LANGSMITH_API_KEY=your_langsmith_key
NEXT_PUBLIC_COPILOT_CLOUD_API_KEY=your_copilot_cloud_key
然后使用以下命令启动应用。
pnpm run dev
访问 http://localhost:3000/,你应该会看到 LangGraph AI 智能体前端已经启动并正常运行。
现在,让我们看看如何使用 CopilotKit 为 LangGraph AI 智能体构建 UI。
要设置 CopilotKit Provider,必须使用 <CopilotKit> 组件包裹应用中需要感知 Copilot 的部分。对于大多数使用场景,适合用 CopilotKit Provider 包裹整个应用,例如在 layout.tsx 中进行设置,如下面的 frontend/src/app/layout.tsx 文件所示:
import { CopilotKit } from "@copilotkit/react-core";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className="h-full">
{/* CopilotKit component for integrating an AI agent */}
<body className={`${lato.variable} ${noto.className} antialiased h-full`}>
<CopilotKit
{/* Pass the Copilot Cloud API key from environment variables */}
publicApiKey={process.env.NEXT_PUBLIC_COPILOT_CLOUD_API_KEY}
{/* Disable the development console (set to true for debugging) */}
showDevConsole={false}
{/* Specify the LangGraph agent name */}
agent="agent"
>
<TooltipProvider>
<ResearchProvider>
{children}
</ResearchProvider>
</TooltipProvider>
</CopilotKit>
</body>
</html>
);
}
要设置 Copilot UI,首先需要在根组件(通常是 layout.tsx)中导入默认样式。
import "@copilotkit/react-ui/styles.css";
Copilot UI 内置了多种 UI 模式,你可以从 CopilotPopup、CopilotSidebar、CopilotChat 和 Headless UI 中选择自己喜欢的一种。
在本例中,我们将使用 src/components/chat.tsx 文件中定义的 CopilotChat。
// Indicate that this component runs on the client side (Next.js directive)
'use client'
// 从 CopilotKit React UI 库中导入 CopilotChat 组件
import { CopilotChat } from "@copilotkit/react-ui";
// 从本地库文件中导入聊天配置的常量值
import { INITIAL_MESSAGE, MAIN_CHAT_INSTRUCTIONS, MAIN_CHAT_TITLE } from "@/lib/consts";
// @ts-expect-error -- ignore: 抑制因类型定义缺失/不正确而产生的 TypeScript 错误
import { CopilotChatProps } from "@copilotkit/react-ui/dist/components/chat/Chat";
// 定义 Chat 组件,接收类型为 CopilotChatProps 的 props
export default function Chat(props: CopilotChatProps) {
return (
// 使用自定义配置渲染 CopilotChat 组件
<CopilotChat
instructions={MAIN_CHAT_INSTRUCTIONS} // 传入预定义的聊天行为指令
labels={{
title: MAIN_CHAT_TITLE, // 使用常量设置聊天窗口标题
initial: INITIAL_MESSAGE, // 设置聊天中显示的初始消息
}}
className="h-full w-full font-noto" // 应用全高、全宽和 Noto 字体的自定义 CSS 类
{...props} // 展开传递给组件的其他 props(例如覆盖项或自定义配置)
/>
);
}
随后,在 src/app/page.tsx 文件中导入并使用聊天组件。接着,聊天界面会渲染在前端 UI 中,如下所示。
CoAgents 维护着一个共享状态,将 UI 与智能体的执行过程无缝连接起来。这个共享状态系统可以让你:
你可以在 CopilotKit 文档中进一步了解 CoAgents 的共享状态。
要在 UI 与 LangGraph AI 智能体之间创建共享状态,首先需要定义智能体状态,并将其发送到前端,如 agent/graph.py 文件所示。
# 用于处理工具执行并更新研究状态的异步方法
async def tool_node(self, state: ResearchState, config: RunnableConfig):
# 自定义配置,在工具执行期间禁止向前端发送消息
config = copilotkit_customize_config(config, emit_messages=False)
msgs = [] # 用于存储工具消息的列表
tool_state = {} # 用于存储工具执行后更新状态的字典
# 处理最后一条消息(假定为 AIMessage)中的每个工具调用
for tool_call in state["messages"][-1].tool_calls:
tool = self.tools_by_name[tool_call["name"]] # 按名称查找工具
# 临时简化消息结构,供工具访问
state['messages'] = {'HumanMessage' if type(message) == HumanMessage else 'AIMessage': message.content for message in state['messages']}
tool_call["args"]["state"] = state # 将状态注入工具参数
# 异步运行工具,并获取更新后的状态和消息
new_state, tool_msg = await tool.ainvoke(tool_call["args"])
tool_call["args"]["state"] = None # 执行完成后从参数中清除状态
# 将工具结果追加为 ToolMessage
msgs.append(ToolMessage(content=tool_msg, name=tool_call["name"], tool_call_id=tool_call["id"]))
# 使用研究数据构建更新后的工具状态
tool_state = {
"title": new_state.get("title", ""),
"outline": new_state.get("outline", {}),
"sections": new_state.get("sections", []),
"sources": new_state.get("sources", {}),
"proposal": new_state.get("proposal", {}),
"logs": new_state.get("logs", []),
"tool": new_state.get("tool", {}),
"messages": msgs
}
# 将更新后的状态发送到前端
await copilotkit_emit_state(config, tool_state)
return tool_state # 返回更新后的状态
然后,在 src/components/research-context.tsx 文件中使用 CopilotKit 的 useCoAgent Hook,将 LangGraph AI 智能体的状态共享给前端 UI。useCoAgent Hook 可以在应用与智能体之间双向共享状态。
// 表明该组件在客户端运行(Next.js 指令)
'use client'
// 导入用于上下文、状态和副作用的 React 工具
import { createContext, useContext, useState, ReactNode, useEffect } from 'react';
// 从共享库中导入 ResearchState 类型
import type { ResearchState } from '@/lib/types';
// 导入 CopilotKit 中用于管理智能体状态的 Hook
import { useCoAgent } from "@copilotkit/react-core";
// 导入用于与本地存储交互的自定义 Hook
import useLocalStorage from "@/lib/hooks/useLocalStorage";
// 定义上下文值的结构
interface ResearchContextType {
state: ResearchState; // 当前研究状态
setResearchState: (newState: ResearchState | ((prevState: ResearchState) => ResearchState)) => void; // 用于更新研究状态的函数
sourcesModalOpen: boolean; // 用于切换来源模态框的布尔值
setSourcesModalOpen: (open: boolean) => void; // 用于设置模态框状态的函数
runAgent: () => void; // 用于触发智能体执行的函数
}
// 创建用于共享研究状态的上下文,初始值为 undefined
const ResearchContext = createContext<ResearchContextType | undefined>(undefined);
// 定义 ResearchProvider 组件,使用上下文包裹子组件
export function ResearchProvider({ children }: { children: ReactNode }) {
// 用于控制来源模态框可见性的状态
const [sourcesModalOpen, setSourcesModalOpen] = useState<boolean>(false);
// 使用 CopilotKit 的 useCoAgent Hook 管理智能体状态和执行
const { state: coAgentState, setState: setCoAgentsState, run } = useCoAgent<ResearchState>({
name: 'agent', // 智能体名称(与后端配置一致)
initialState: {}, // 智能体的初始空状态
});
// 使用自定义 Hook 管理本地存储中的研究状态,初始值为 null
// @ts-expect-error -- force null: 抑制因初始值为 null 而产生的 TypeScript 错误
const [localStorageState, setLocalStorageState] = useLocalStorage<ResearchState>('research', null);
// 用于同步智能体状态与本地存储的副作用
useEffect(() => {
// 检查智能体状态或本地存储是否为空
const coAgentsStateEmpty = Object.keys(coAgentState).length < 1;
const localStorageStateEmpty = localStorageState == null || Object.keys(localStorageState).length < 1;
// 如果本地存储中有数据,但智能体状态为空,则初始化智能体状态
if (!localStorageStateEmpty && coAgentsStateEmpty) {
setCoAgentsState(localStorageState);
return;
}
// 如果智能体状态中有数据,但本地存储为空,则保存到本地存储
if (!coAgentsStateEmpty && localStorageStateEmpty) {
setLocalStorageState(coAgentState);
return;
}
// 如果两者都存在但内容不同,则使用智能体状态更新本地存储
if (!localStorageStateEmpty && !coAgentsStateEmpty && JSON.stringify(localStorageState) !== JSON.stringify(coAgentState)) {
setLocalStorageState(coAgentState);
return;
}
}, [coAgentState, localStorageState, setCoAgentsState, setLocalStorageState]); // 该副作用的依赖项
// 向子组件提供上下文值
return (
<ResearchContext.Provider value={{
state: coAgentState, // 来自 CopilotKit 的当前研究状态
setResearchState: setCoAgentsState as ResearchContextType['setResearchState'], // 研究状态的设置函数(通过类型转换确保类型兼容)
setSourcesModalOpen, // 用于切换来源模态框的函数
sourcesModalOpen, // 当前模态框状态
runAgent: run // 用于运行智能体的函数
}}>
{children} // 在 Provider 内渲染子组件
</ResearchContext.Provider>
);
}
// 用于访问研究上下文的自定义 Hook
export function useResearch() {
// 获取上下文值
接下来,在聊天 UI 中渲染智能体的状态。这有助于以一种更符合当前上下文的方式向用户展示智能体状态。为此,可以在 src/app/page.tsx 文件中使用 useCoAgentStateRender Hook。
// ...
import {useCoAgentStateRender} from "@copilotkit/react-core";
// ...
export default function HomePage() {
// ...
useCoAgentStateRender<ResearchState>(
{
name: "agent",
render: ({ state }) => {
if (state.logs?.length > 0) {
return <Progress logs={state.logs} />;
}
return null;
},
},
[researchState]
);
// ...
}
然后导航到 http://localhost:3000/,在聊天中添加"research AI models",然后按"Enter"。您应该会看到 LangGraph AI 智能体状态在聊天 UI 中被渲染,如下所示。
为了允许 LangGraph 智能体在聊天 UI 中执行期间请求人类输入或批准,请在 src/app/page.tsx 文件中使用名为 review_proposal 的 CopilotKit useCopilotAction hook。
// ...
import { useCopilotAction } from "@copilotkit/react-core";
// ...
export default function HomePage() {
// ...
// 使用 CopilotKit 中的 useCopilotAction hook 定义自定义操作
useCopilotAction({
name: "review_proposal", // 操作的唯一名称
description:
"Prompt the user to review structure proposal. Right after proposal generation", // 操作目的的描述
available: "remote", // 表示操作可远程使用(例如,由后端或智能体触发)
parameters: [], // 此操作不需要参数
// @ts-expect-error -- null element is legit: 为返回 null 时禁止 TypeScript 错误
renderAndWaitForResponse: (
{ respond, status } // 用于渲染 UI 并等待用户响应的函数
) =>
status !== "complete" ? ( // 检查操作是否仍在进行中
<ProposalViewer // 渲染自定义 ProposalViewer 组件
onSubmit={(
approved,
proposal // 用户提交审查时的回调
) =>
respond?.({
// 将响应发送回 CopilotKit
...proposal, // 展开提案对象(假设包含结构详情)
approved, // 添加批准状态(true/false)
})
}
/>
) : null, // 操作完成时返回 null(隐藏 UI)
});
// ...
}
然后导航到 http://localhost:3000/。一旦 LangGraph 智能体完成了关于 AI 模型的研究,它就会要求您批准提案,如下所示。
选择您想要的部分,添加一些备注,然后单击"批准提案"按钮。LangGraph AI 智能体将开始编写关于 AI 模型的研究报告。
为了流式传输研究报告内容,请在 src/app/page.tsx 文件中使用在 src/lib/hooks/useStreamingContent.ts 文件中定义的 useStreamingContent hook。
import { useStreamingContent } from "@/lib/hooks/useStreamingContent";
export default function HomePage() {
// ...
const streamingSection = useStreamingContent(researchState);
// ...
return (
// ...
{/* 文档查看器 */}
<DocumentsView
sections={sections ?? []}
streamingSection={streamingSection}
selectedSection={sections?.find((s) => s.id === selectedSectionId)}
onSelectSection={setSelectedSectionId}
/>
// ...
)
}
您应该会看到研究内容在右侧流式传输,如下所示。
我们在本教程中涉及了很多内容。我希望您学到了如何使用 CopilotKit 为您的应用程序构建 AI 智能体助手的 UI,以及如何实时执行状态更改并实现人类参与循环的概念。
在 GitHub 上查看完整源代码
在 Twitter 上关注 CopilotKit 并打个招呼,如果您想构建一些有趣的东西,加入 Discord 社区。
某些评论可能仅对已登录的访问者可见。登录以查看所有评论。
如需进一步操作,您可以考虑拉黑此人和/或举报滥用。