AI驱动的应用程序已经不仅仅局限于自主代理执行任务。一种新的方法涉及人在回路中,允许用户提供反馈、审查结果并指导AI的下一步行动。这类代理被称为CoAgents。
TL;DR(长文概要)在本教程中,你将学习如何利用LangGraph、CopilotKit和Tavily来构建一个Perplexity克隆。
开始建吧!
什么是代理助手?
代理型副驾(AG)是CopilotKit如何将LangGraph代理集成到你的应用程序中的方式。
CoAgents,是 CopilotKit 构建代理本机应用的方式!
简单来说,它将通过执行多个搜索来响应用户请求,并实时将搜索状态和结果流回给客户端。
此处省略内容
先决条件要完全理解本教程,你需要了解一些 React 或 Next.js 的基础知识。
我们还将使用以下内容:
- Python - 一种流行的编程语言,用于使用LangGraph构建AI代理;请确保已安装在您的计算机上。
- LangGraph - 一个用于创建和部署AI代理的框架。它还有助于定义代理执行的控制流程和操作。
- OpenAI API Key - 用于使用GPT模型执行各种任务,如生成文本和回答问题;对于本教程,请确保您能够使用GPT-4模型。
- Tavily AI - 一个搜索引擎,帮助AI代理在应用程序内进行研究并访问实时知识。
- CopilotKit - 一个开源的副驾驶框架,用于构建定制的AI聊天机器人、应用程序内的AI代理和文本区域。
- Shad Cn UI - 提供一组可在应用程序内使用的可重用UI组件。
或者
三个星号 (sān gè xīnghào)
(此处省略部分内容) (chǔcǐ shěngjué bù chǎn fēn yùn)
如何使用LangGraph和CopilotKit打造AI智能体接下来,你将学习如何使用LangGraph和CopilotKit创建一个AI助手。
首先,克隆 CopilotKit CoAgents starter 仓库(点击这里查看:https://github.com/CopilotKit/CopilotKit/tree/main/examples/coagents-starter)。ui
目录包含 Next.js 应用程序的前端部分,而 agent
目录则包含应用程序的 CoAgent。
在名为 agent
的目录中,使用 Poetry 安装项目的依赖项。
cd agent
poetry install
点击进入全屏,点击退出全屏
在代理文件夹内创建一个.env
文件,并将你的OpenAI和Tavily AI API密钥复制到该文件中:
OPENAI_API_KEY= # 开放AI的API密钥
TAVILY_API_KEY= # TAVILY的API密钥
全屏模式(按此进入/退出)
将下面这段代码复制到 agent.py
文件中:
"""
这是AI的主要入口点。
它定义了工作流图和代理的入口点。
"""
# pylint: disable=line-too-long, unused-import
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
from ai_researcher.state import AgentState
from ai_researcher.steps import steps_node
from ai_researcher.search import search_node
from ai_researcher.summarize import summarize_node
from ai_researcher.extract import extract_node
def route(state):
"""路由到研究步骤。"""
if not state.get("steps", None):
return END
current_step = next((step for step in state["steps"] if step["status"] == "pending"), None)
if not current_step:
return "summarize_node"
if current_step["type"] == "search":
return "search_node"
raise ValueError(f"未知步骤类型{current_step['type']}:")
# 定义一个新的工作流图
workflow = StateGraph(AgentState)
workflow.add_node("steps_node", steps_node)
workflow.add_node("search_node", search_node)
workflow.add_node("summarize_node", summarize_node)
workflow.add_node("extract_node", extract_node)
# 对话流程
workflow.set_entry_point("steps_node")
workflow.add_conditional_edges(
"steps_node",
route,
["summarize_node", "search_node", END]
)
workflow.add_edge("search_node", "extract_node")
workflow.add_conditional_edges(
"extract_node",
route,
["summarize_node", "search_node"]
)
workflow.add_edge("summarize_node", END)
memory = MemorySaver()
graph = workflow.compile(checkpointer=memory)
切换到全屏模式,退出全屏
上述代码片段定义了LangGraph代理的工作流程。它从steps_node
开始,搜索结果,然后总结它们,并提取关键信息。
接下来创建一个demo.py
文件,并包含以下代码片段。
"""示例代码"""
import os
from dotenv import load_dotenv
load_dotenv()
from fastapi import FastAPI
import uvicorn
from copilotkit.integrations.fastapi import add_fastapi_endpoint
from copilotkit import CopilotKitSDK, LangGraphAgent
from ai_researcher.agent import graph
app = FastAPI()
sdk = CopilotKitSDK(
agents=[
LangGraphAgent(
name="ai_researcher",
description="研究代理。",
graph=graph,
)
],
)
add_fastapi_endpoint(app, sdk, "/copilotkit")
# 新增健康检查路由
@app.get("/health")
def status_check():
"""状态检查。"""
return {"status": "ok"}
def main():
"""启动uvicorn服务器。"""
port = int(os.getenv("PORT", "8000"))
uvicorn.run("ai_researcher.demo:app", host="0.0.0.0", port=port, reload=True)
全屏 。
退出全屏 。
上面的代码创建了一个FastAPI端点来托管LangGraph代理,并将它连接到CopilotKit SDK。
你可以从 GitHub 仓库 复制剩余的代码以创建 CoAgent。在接下来的部分里,你将学习如何为 Perplexity 的克隆版构建用户界面并使用 CopilotKit 处理搜索请求的过程。
……
使用 Next.js 构建应用界面在这个部分,我将一步步引导您完成构建应用程序用户界面的步骤。
首先,通过运行如下代码片段来建立一个Next.js的TypeScript项目:
# 👉🏻 进入 ui 文件夹
npx create-next-app ./
点击全屏,点击退出全屏
在新创建的项目中安装 ShadCn UI 库,运行以下代码片段即可。
npx shadcn@latest init
使用 npx 初始化 shadcn 项目
进入全屏,退出全屏
接下来,在 Next.js 项目的根目录下创建一个名为 components
的文件夹,然后将此 GitHub 仓库中的 ui
文件夹 复制到该文件夹中。Shadcn 可以让您通过命令行轻松安装各种 组件(如折叠面板),以方便地将它们添加到您的应用程序中。
除了 Shadcn 提供的组件之外,你还需要创建一些代表应用程序界面不同部分的几个组件。在 components
文件夹内运行以下代码片段,以添加这些组件到 Next.js 项目。
执行 touch ResearchWrapper.tsx ResultsView.tsx HomeView.tsx # 更新文件时间戳或创建空文件
执行 touch AnswerMarkdown.tsx Progress.tsx SkeletonLoader.tsx # 更新文件时间戳或创建空文件
进入全屏,退出全屏
将下面的代码复制到 app/page.tsx
文件中。
"use client";
import { ResearchWrapper } from "@/components/ResearchWrapper";
import { ModelSelectorProvider, useModelSelectorContext } from "@/lib/model-selector-provider";
import { ResearchProvider } from "@/lib/research-provider";
import { CopilotKit } from "@copilotkit/react-core";
import "@copilotkit/react-ui/styles.css";
export default function ModelSelectorWrapper() {
return (
<CopilotKit runtimeUrl={useLgc ? "/api/copilotkit-lgc" : "/api/copilotkit"} agent="ai_researcher">
<ResearchProvider>
<ResearchWrapper />
</ResearchProvider>
</CopilotKit>
);
}
进入全屏 退出全屏
在上面的代码片段中,ResearchProvider
是一个自定义的 React 上下文提供器,它共享用户的搜索查询及结果,使得它们对应用程序中的所有组件可用。ResearchWrapper
组件包含了应用的核心元素并管理用户界面的显示。
在项目根目录创建一个名为lib
的文件夹,并将research-provider.tsx
文件放入该文件夹中,然后将以下代码粘贴到该文件中:
import { createContext, useContext, useState, ReactNode, useEffect } from "react";
type ResearchContextType = {
researchQuery: string;
setResearchQuery: (query: string) => void;
researchInput: string;
setResearchInput: (input: string) => void;
isLoading: boolean;
setIsLoading: (loading: boolean) => void;
researchResult: ResearchResult | null;
setResearchResult: (result: ResearchResult) => void;
};
type ResearchResult = {
answer: string;
sources: string[];
}
const ResearchContext = createContext<ResearchContextType | undefined>(undefined);
export const ResearchProvider = ({ children }: { children: ReactNode }) => {
const [researchQuery, setResearchQuery] = useState<string>("");
const [researchInput, setResearchInput] = useState<string>("");
const [researchResult, setResearchResult] = useState<ResearchResult | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(() => {
if (!researchQuery) {
setResearchResult(null);
setResearchInput("");
}
}, [researchQuery, researchResult]);
return (
<ResearchContext.Provider
value={{
researchQuery,
setResearchQuery,
researchInput,
setResearchInput,
isLoading,
setIsLoading,
researchResult,
setResearchResult,
}}
>
{children}
</ResearchContext.Provider>
);
};
export const useResearchContext = () => {
const context = useContext(ResearchContext);
if (context === undefined) {
throw new Error("useResearchContext 必须在 ResearchProvider 组件内使用");
}
return context;
};
进入全屏,退出全屏
这些状态会被声明和保存到ResearchContext
中,以确保它们在应用程序中多个组件之间能够被妥善管理。
创建一个 ResearchWrapper
组件,如下:
import { HomeView } from "./HomeView";
import { ResultsView } from "./ResultsView";
import { AnimatePresence } from "framer-motion";
import { useResearchContext } from "@/lib/research-provider";
export function ResearchWrapper() {
const { researchQuery, setResearchInput } = useResearchContext();
return (
<>
<div className="flex flex-col items-center justify-center relative z-10">
<div className="flex-1">
{researchQuery ? (
<AnimatePresence
key="results"
onExitComplete={() => {
setResearchInput("");
}}
mode="wait"
>
<ResultsView key="results" />
</AnimatePresence>
) : (
<AnimatePresence key="home" mode="wait">
<HomeView key="home" />
</AnimatePresence>
)}
</div>
<footer className="text-xs p-2">
<a
href="https://copilotkit.ai"
target="_blank"
rel="noopener noreferrer"
className="text-slate-600 font-medium hover:underline"
>
本页面由 CopilotKit 🪁 提供支持
</a>
</footer>
</div>
</>
);
}
全屏/退出全屏
The ResearchWrapper
组件初始化渲染 HomeView
组件作为默认视图,并当提供搜索查询时显示 ResultView
。使用 useResearchContext
钩子使我们能够获取 researchQuery
状态并根据需要更新视图。
最后,创建HomeView
组件来展示应用程序的主页界面。
"use client";
import { useEffect, useState } from "react";
import { Textarea } from "./ui/textarea";
import { cn } from "@/lib/utils";
import { Button } from "./ui/button";
import { CornerDownLeftIcon } from "lucide-react";
import { useResearchContext } from "@/lib/research-provider";
import { motion } from "framer-motion";
import { useCoAgent } from "@copilotkit/react-core";
import { TextMessage, MessageRole } from "@copilotkit/runtime-client-gql";
import type { AgentState } from "../lib/types";
import { useModelSelectorContext } from "@/lib/model-selector-provider";
const MAX_INPUT_LENGTH = 250;
export function HomeView() {
const { setResearchQuery, researchInput, setResearchInput } =
useResearchContext();
const { model } = useModelSelectorContext();
const [isInputFocused, setIsInputFocused] = useState(false);
const {
run: runResearchAgent,
} = useCoAgent<AgentState>({
name: "ai_researcher",
initialState: {
model,
},
});
const handleResearch = (query: string) => {
setResearchQuery(query);
runResearchAgent(() => {
return new TextMessage({
role: MessageRole.User,
content: query,
});
});
};
const suggestions = [
{ label: "2024年与2023年电动汽车的销量对比", icon: "🚙" },
{ label: "世界最富有的人", icon: "💰" },
{ label: "世界人口", icon: "🌍 " },
{ label: "西雅图与纽约的天气对比", icon: "⛅️" },
];
return (
<motion.div
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.4 }}
className="h-screen w-full flex flex-col gap-y-2 justify-center items-center p-4 lg:p-0"
>
<h1 className="text-4xl font-extralight mb-6">
您想了解什么?
</h1>
<div
className={cn(
"w-full bg-slate-100/50 border shadow-sm rounded-md transition-all",
{
"ring-1 ring-slate-300": isInputFocused,
}
)}
>
<Textarea
placeholder="有什么想了解的?"
className="bg-transparent p-4 resize-none focus-visible:ring-0 focus-visible:ring-offset-0 border-0 w-full"
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
value={researchInput}
onChange={(e) => setResearchInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleResearch(researchInput);
}
}}
maxLength={MAX_INPUT_LENGTH}
/>
<div className="text-xs p-4 flex items-center justify-between">
<div
className={cn("transition-all duration-300 mt-4 text-slate-500", {
"opacity-0": !researchInput,
"opacity-100": researchInput,
})}
>
{researchInput.length} / {MAX_INPUT_LENGTH}
</div>
<Button
size="sm"
className={cn("rounded-full transition-all duration-300", {
"opacity-0 pointer-events-none": !researchInput,
"opacity-100": researchInput,
})}
onClick={() => handleResearch(researchInput)}
>
研究
<CornerDownLeftIcon className="w-4 h-4 ml-2" />
</Button>
</div>
</div>
<div className="grid grid-cols-2 w-full gap-2 text-sm">
{suggestions.map((suggestion) => (
<div
key={suggestion.label}
onClick={() => handleResearch(suggestion.label)}
className="p-2 bg-slate-100/50 rounded-md border col-span-2 lg:col-span-1 flex cursor-pointer items-center space-x-2 hover:bg-slate-100 transition-all duration-300"
>
<span className="text-base">{suggestion.icon}</span>
<span className="flex-1">{suggestion.label}</span>
</div>
))}
</div>
</motion.div>
);
}
全屏模式(点击进入/退出)
如何将你的CoAgent连接到Next.js应用
在这个部分里,你将学习如何将CopilotKit CoAgent连接到你的Next.js应用,以让用户在应用内进行搜索。
安装以下 CopilotKit 包以及 OpenAI Node.js SDK。这些包让辅助代理能够与 React 状态值交互并作出应用程序内的决策,以实现这一功能。
npm install @copilotkit/react-core @copilotkit/react-ui @copilotkit/runtime @copilotkit/runtime-client-gql openai
切换到全屏 退出全屏
在 Next.js 的 app
文件夹里新建一个 api
文件夹。然后,在 api
文件夹里创建一个叫 copilotkit
的目录,并在其中添加一个 route.ts
文件。这会生成一个 API 端点(/api/copilotkit
),用来连接前端应用到 CopilotKit CoAgent。
首先,我们进入app目录
cd app
创建api目录并进入
mkdir api && cd api
创建copilotkit目录并进入
mkdir copilotkit && cd copilotkit
创建一个名为route.ts的新文件
touch route.ts
全屏 全屏退出
将下面的代码段复制到 api/copilotkit/route.ts
文件:
import { NextRequest } from "next/server";
import {
CopilotRuntime,
OpenAIAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import OpenAI from "openai";
//👇🏻 将 OpenAI 初始化为适配器
const openai = new OpenAI();
const serviceAdapter = new OpenAIAdapter({ openai } as any);
//👇🏻 将 CopilotKit 运行时与 CoAgent 连接
const runtime = new CopilotRuntime({
remoteEndpoints: [
{
url: process.env.REMOTE_ACTION_URL || "http://localhost:8000/copilotkit",
},
],
});
export const POST = async (req: NextRequest) => {
// 返回处理请求的结果
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter,
endpoint: "/api/copilotkit",
});
return handleRequest(req);
};
进入全屏 退出全屏
下面的代码片段在/api/copilotkit
API接口配置了CopilotKit运行环境,使CopilotKit通过AI助手处理用户请求。
最后,更新app/page.tsx
文件,通过在应用程序中使用CopilotKit组件包裹整个代码,为所有应用程序组件提供助手环境。
"use client";
// 引入模型选择器组件
import { ModelSelector } from "@/components/ModelSelector";
// 引入研究包装器组件
import { ResearchWrapper } from "@/components/ResearchWrapper";
// 引入模型选择器提供者
import { ModelSelectorProvider, useModelSelectorContext } from "@/lib/model-selector-provider";
// 引入研究提供者
import { ResearchProvider } from "@/lib/research-provider";
// 引入CopilotKit组件
import { CopilotKit } from "@copilotkit/react-core";
// 引入CopilotKit样式
import "@copilotkit/react-ui/styles.css";
// 导出默认函数ModelSelectorWrapper
export default function ModelSelectorWrapper() {
return (
<main className="flex flex-col items-center justify-between">
// 创建主区域
<ModelSelectorProvider>
// 展示Home组件
<Home/>
// 展示模型选择器组件
<ModelSelector />
</ModelSelectorProvider>
</main>
);
}
// 创建Home函数
function Home() {
const { useLgc } = useModelSelectorContext();
return (
<CopilotKit runtimeUrl={useLgc ? "/api/copilotkit-lgc" : "/api/copilotkit"} agent="ai_researcher">
<ResearchProvider>
// 使用研究提供者
<ResearchWrapper />
</ResearchProvider>
</CopilotKit>
);
}
全屏模式,退出全屏
CopilotKit 组件覆盖整个应用程序,并接受两个参数 - runtimeUrl
和 agent
。其中,runtimeUrl
是托管 AI 代理的后端 API 路由,而 agent
是执行操作的代理名称。
接受请求并实时传输响应给前端
让 CopilotKit 可以访问和处理用户输入,它提供了一个 [useCoAgent
钩子],允许从应用程序中的任何位置访问代理的状态信息。
例如,下面的代码片段展示了如何使用 useCoAgent
钩子。state
变量允许访问代理的当前状态,setState
用于修改代理的状态,而 run
函数用于执行代理的指令。start
和 stop
函数分别用来启动和停止代理的运行。
const { state, setState, run, start, stop } = useCoAgent({
name: "搜索代理",
});
切换到全屏模式 切换回正常模式
更新 HomeView
组件,当提供搜索查询时执行代理操作。
//👇🏻 从 CopilotKit 导入 useCoAgent 模块
import { useCoAgent } from "@copilotkit/react-core";
const { run: runResearchAgent } = useCoAgent({
name: "search_agent", // 设置名为 'search_agent' 的代理
});
const handleResearch = (query: string) => {
setResearchQuery(query); // 设置搜索查询
runResearchAgent(query); // 运行搜索代理
};
进入全屏 退出全屏
接下来,你可以通过访问 useCoAgent
钩子中的状态变量来将搜索结果流式传输显示到 ResultsView
。然后,把下面这段代码粘贴到 ResultsView
组件里。
"use client";
import { useResearchContext } from "@/lib/research-provider";
import { motion } from "framer-motion";
import { BookOpenIcon, LoaderCircleIcon, SparkleIcon } from "lucide-react";
import { SkeletonLoader } from "./SkeletonLoader";
import { useCoAgent } from "@copilotkit/react-core";
import { Progress } from "./Progress";
import { AnswerMarkdown } from "./AnswerMarkdown";
export function ResultsView() {
const { researchQuery } = useResearchContext();
//👇🏻 代理状态
const { state: agentState } = useCoAgent({
name: "search_agent",
});
console.log("AGENT_STATE", agentState);
//👇🏻 跟踪当前代理处理的步骤
const steps =
agentState?.steps?.map((step: any) => {
return {
description: step.description || "",
status: step.status || "pending",
updates: step.updates || [],
};
}) || [];
const isLoading = !agentState?.answer?.markdown;
return (
<motion.div
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -50 }}
transition={{ duration: 0.5, ease: "easeOut" }}
>
<div className='max-w-[1000px] p-8 lg:p-4 flex flex-col gap-y-8 mt-4 lg:mt-6 text-sm lg:text-base'>
<div className='space-y-4'>
<h1 className='text-3xl lg:text-4xl font-extralight'>
{researchQuery}
</h1>
</div>
<Progress steps={steps} />
<div className='grid grid-cols-12 gap-8'>
<div className='col-span-12 lg:col-span-8 flex flex-col'>
<h2 className='flex items-center gap-x-2'>
{isLoading ? (
<LoaderCircleIcon className='animate-spin w-4 h-4 text-slate-500' />
) : (
<SparkleIcon className='w-4 h-4 text-slate-500' />
)}
答案
</h2>
<div className='text-slate-500 font-light'>
{isLoading ? (
<SkeletonLoader />
) : (
<AnswerMarkdown markdown={agentState?.answer?.markdown} /> //👈🏼 注释:显示搜索的答案
)}
</div>
</div>
{agentState?.answer?.references?.length && (
<div className='flex col-span-12 lg:col-span-4 flex-col gap-y-4 w-[200px]'>
<h2 className='flex items-center gap-x-2'>
<BookOpenIcon className='w-4 h-4 text-slate-500' />
参考资料
</h2>
<ul className='text-slate-900 font-light text-sm flex flex-col gap-y-2'>
{agentState?.answer?.references?.map(
(ref: any, idx: number) => (
<li key={idx}>
<a
href={ref.url}
target='_blank'
rel='noopener noreferrer'
>
{idx + 1}. {ref.title}
</a>
</li>
)
)}
</ul>
</div>
)}
</div>
</div>
</motion.div>
);
}
全屏,退出全屏
上述代码片段从代理的状态中获取搜索结果,并使用 useCoAgent
钩子将其流式传输到前端。搜索结果以 Markdown 格式返回,并传递给该 AnswerMarkdown
组件,该组件会在页面上渲染内容。
最后,将代码片段复制到 AnswerMarkdown
组件中。这会利用 React Markdown 库 把 markdown 内容转换成格式化的文本。
import Markdown from "react-markdown";
// 导入Markdown模块
export function AnswerMarkdown({ markdown }: { markdown: string }) {
// 返回一个包含markdown内容的div
return (
<div className='markdown-wrapper'>
<Markdown>{markdown}</Markdown>
</div>
);
}
全屏模式 退出全屏
恭喜你完成了本教程里的项目。你也可以在这里观看视频回放。
总结
LLM智能最有效的是与人类智能一起工作,而CopilotKit CoAgents只需几分钟就能让您将AI代理、副驾和其他类型的助手集成到您的软件应用中。
如果你需要开发一个AI产品或将AI代理嵌入到你的应用中,你可以考虑使用CopilotKit。
你可以在 GitHub 上找到本教程的源代码文件。
在 GitHub 上的 CopilotKit 示例页面:https://github.com/CopilotKit/CopilotKit/tree/main/examples/coagents-ai-researcher
感谢您的阅读!
共同学习,写下你的评论
评论加载中...
作者其他优质文章