详细教程演示如何用 Next.js、LangChain 和 CopilotKit 构建 AI 博客平台,包含实战代码和开发流程。
在本文中,你将学习如何构建一个由 AI 驱动的博客平台,该平台可以搜索网络,并针对任意主题展开研究以撰写博客文章。
Next.js 用作应用框架 🖥️
OpenAI 用作 LLM 🧠
LangChain 和 Tavily 用于构建能够搜索网络的 AI 智能体 🤖
使用 CopilotKit 将 AI 集成到应用中 🪁
Supabase 用于存储和检索博客平台的文章数据。
CopilotKit 是一个开源的 AI Copilot 框架和平台。它能够帮助你轻松地将强大的 AI 功能集成到 React 应用中。
ChatBots💬:具备上下文感知能力的应用内聊天机器人,可以在应用内执行操作
ChatBots💬:具备上下文感知能力的应用内聊天机器人,可以在应用内执行操作
CopilotTextArea📝:由 AI 驱动的文本字段,支持上下文感知的自动补全和内容插入
CopilotTextArea📝:由 AI 驱动的文本字段,支持上下文感知的自动补全和内容插入
Co-Agents🤖:可以与你的应用及用户交互的应用内 AI 智能体,由 LangChain 提供支持。
Co-Agents🤖:可以与你的应用及用户交互的应用内 AI 智能体,由 LangChain 提供支持。
现在回到本文的内容。
在开始构建应用之前,我们先来了解一下构建该应用所需的依赖或软件包。
copilotkit/react-core:CopilotKit 前端软件包,提供 React Hooks,用于向 Copilot 提供应用状态和操作(AI 功能)
copilotkit/react-ui:CopilotKit 前端软件包,用于实现聊天机器人侧边栏 UI
copilotkit/react-textarea:CopilotKit 前端软件包,用于在演示文稿的演讲者备注中进行 AI 辅助文本编辑。
LangChainJS:用于开发由语言模型驱动的应用程序的框架。
Tavily Search API:用于帮助 LLM 和 AI 应用连接可信实时知识的 API。
在安装项目的所有软件包和依赖之前,首先在终端中运行以下命令创建一个 Nextjs 项目。
npx create-next-app@latest
随后,系统会提示你选择一些选项。你可以按照下图所示进行选择。
之后,使用你喜欢的文本编辑器打开新创建的 Nextjs 项目。然后在命令行中运行以下命令,安装项目的所有软件包和依赖。
npm i @copilotkit/backend @copilotkit/shared @langchain/langgraph @copilotkit/react-core @copilotkit/react-ui @copilotkit/react-textarea @supabase/ssr @supabase/auth-helpers-nextjs
在本节中,我将引导你使用静态内容创建博客平台的前端,以定义平台的用户界面。
首先,进入 /[root]/src/app 并创建一个名为 components 的文件夹。在 components 文件夹中创建一个名为 Article.tsx 的文件。
然后,将以下代码添加到该文件中。这段代码定义了一个名为 Article 的函数组件,用于渲染文章创建表单。
"use client";
import { useRef, useState } from "react";
export function Article() {
// Define state variables for article outline, copilot text, and article title
const [articleOutline, setArticleOutline] = useState("");
const [copilotText, setCopilotText] = useState("");
const [articleTitle, setArticleTitle] = useState("");
return (
// Form element for article input
<form
action={""}
className="w-full h-full gap-10 flex flex-col items-center p-10">
{/* Input field for article title */}
<div className="flex w-full items-start gap-3">
<textarea
className="p-2 w-full h-12 rounded-lg flex-grow overflow-x-auto overflow-y-hidden whitespace-nowrap"
id="title"
name="title"
value={articleTitle}
placeholder="Article Title"
onChange={(event) => setArticleTitle(event.target.value)}
/>
</div>
{/* Textarea for article content */}
<textarea
className="p-4 w-full aspect-square font-bold text-xl bg-slate-800 text-white rounded-lg resize-none"
id="content"
name="content"
value={copilotText}
placeholder="Write your article content here"
onChange={(event) => setCopilotText(event.target.value)}
/>
{/* Publish button */}
<button
type="submit"
className="p-4 w-full !bg-slate-800 text-white rounded-lg">Publish</button>
</form>
);
}
接下来,在 components 文件夹中再创建一个文件,并将其命名为 Header.tsx。然后将以下代码添加到该文件中。这段代码定义了一个名为 Header 的函数组件,用于渲染博客平台的导航栏。
import Link from "next/link";
export default function Header() {
return (
<>
<header className="flex flex-wrap sm:justify-start sm:flex-nowrap z-50 w-full bg-white border-b border-gray-200 text-sm py-3 sm:py-0 ">
<nav
className="relative max-w-7xl w-full mx-auto px-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8"
aria-label="Global">
<div className="flex items-center justify-between">
<Link
className="flex-none text-xl font-semibold "
href="/"
aria-label="Brand">
AIBlogging
</Link>
</div>
<div id="navbar-collapse-with-animation" className="">
<div className="flex flex-col gap-y-4 gap-x-0 mt-5 sm:flex-row sm:items-center sm:justify-end sm:gap-y-0 sm:gap-x-7 sm:mt-0 sm:ps-7">
<Link
className="flex items-center font-medium text-gray-500 border-2 border-indigo-600 text-center p-2 rounded-md hover:text-blue-600 sm:border-s sm:my-6 "
href="/writearticle">
Create Post
</Link>
</div>
</div>
</nav>
</header>
</>
);
}
之后,进入 /[root]/src/app 并创建一个名为 writearticle 的文件夹。在 writearticle 文件夹中创建一个名为 page.tsx 的文件。然后将以下代码添加到该文件中,用于导入 Article 和 Header 组件。接着,代码定义了一个名为 WriteArticle 的函数组件,用于渲染导航栏和文章创建表单。
import { Article } from "../components/Article";
import Header from "../components/Header";
export default function WriteArticle() {
return (
<>
<Header />
<Article />
</>
);
}
接下来,进入 /[root]/src/page.tsx 文件,并添加以下代码。这段代码定义了一个名为 Home 的函数组件,用于渲染博客平台首页,首页将显示已发布文章的列表。
import Image from "next/image";
import Link from "next/link";
import Header from "./components/Header";
const Home = async () => {
return (
<>
<Header />
<div className="max-w-[85rem] h-full px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto">
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
<Link
key={""}
className="group flex flex-col h-full bg-white border border-gray-200 hover:border-transparent hover:shadow-lg transition-all duration-300 rounded-xl p-5 "
href={""}>
<div className="aspect-w-16 aspect-h-11">
<Image
className="object-cover h-48 w-96 rounded-xl"
src={`https://source.unsplash.com/featured/?${encodeURIComponent(
"hello world"
)}`}
width={500}
height={500}
alt="Image Description"
/>
</div>
<div className="my-6">
<h3 className="text-xl font-semibold text-gray-800 ">
Hello World
</h3>
</div>
</Link>
</div>
</div>
</>
);
};
export default Home;
之后,进入 next.config.js 文件,并添加以下代码。该配置允许你使用 Unsplash 上的图片作为已发布文章的封面图片。
module.exports = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "source.unsplash.com",
},
],
},
};
最后,在命令行中运行 npm run dev 命令,然后访问 http://localhost:3000/。现在,你应该能在浏览器中看到博客平台的前端,如下图所示。
在本节中,我将引导你把博客平台与 CopilotKit 后端集成。该后端负责处理来自前端的请求,提供函数调用能力,并支持 GPT 等各种 LLM 后端。此外,我们还将集成一个名为 Tavily 的 AI 智能体,它可以在网络上研究任意主题。
首先,在根目录中创建一个名为 .env.local 的文件。然后在该文件中添加以下环境变量,用于保存你的 ChatGPT 和 Tavily Search API 密钥。
OPENAI_API_KEY="Your ChatGPT API key"
TAVILY_API_KEY="Your Tavily Search API key"
要获取 ChatGPT API 密钥,请访问 https://platform.openai.com/api-keys。
获取 Tavily Search API key,请导航至 https://app.tavily.com/home
之后,进入 /[root]/src/app 并创建一个名为 api 的文件夹。在 api 文件夹中,创建一个名为 copilotkit 的文件夹。在 copilotkit 文件夹中,创建一个名为 research.ts 的文件。然后导航到这个 research.ts gist 文件,复制代码,并将其添加到 research.ts 文件中
接下来,在 /[root]/src/app/api/copilotkit 文件夹中创建一个名为 route.ts 的文件。该文件将包含设置后端功能的代码以处理 POST 请求。它会有条件地包含一个执行给定主题研究的"research"操作。
现在在文件顶部导入以下模块。
import { CopilotBackend, OpenAIAdapter } from "@copilotkit/backend"; // For backend functionality with CopilotKit.
import { researchWithLangGraph } from "./research"; // Import a custom function for conducting research.
import { AnnotatedFunction } from "@copilotkit/shared"; // For annotating functions with metadata.
在上述代码下方,定义一个运行时环境变量和一个名为 researchAction 的函数,该函数使用下面的代码进行某个主题的研究。
// Define a runtime environment variable, indicating the environment where the code is expected to run.
export const runtime = "edge";
// Define an annotated function for research. This object includes metadata and an implementation for the function.
const researchAction: AnnotatedFunction<any> = {
name: "research", // Function name.
description: "Call this function to conduct research on a certain topic. Respect other notes about when to call this function", // Function description.
argumentAnnotations: [ // Annotations for arguments that the function accepts.
{
name: "topic", // Argument name.
type: "string", // Argument type.
description: "The topic to research. 5 characters or longer.", // Argument description.
required: true, // Indicates that the argument is required.
},
],
implementation: async (topic) => { // The actual function implementation.
console.log("Researching topic: ", topic); // Log the research topic.
return await researchWithLangGraph(topic); // Call the research function and return its result.
},
};
然后在上述代码下方添加下面的代码以定义一个处理 POST 请求的异步函数。
// Define an asynchronous function that handles POST requests.
export async function POST(req: Request): Promise<Response> {
const actions: AnnotatedFunction<any>[] = []; // Initialize an array to hold actions.
// Check if a specific environment variable is set, indicating access to certain functionality.
if (process.env["TAVILY_API_KEY"]) {
actions.push(researchAction); // Add the research action to the actions array if the condition is true.
}
// Instantiate CopilotBackend with the actions defined above.
const copilotKit = new CopilotBackend({
actions: actions,
});
// Use the CopilotBackend instance to generate a response for the incoming request using an OpenAIAdapter.
return copilotKit.response(req, new OpenAIAdapter());
}
在本节中,我将引导你完成使用 CopilotKit 前端集成博客平台的过程,以促进博客文章研究和文章大纲生成。我们将使用聊天机器人侧边栏组件、copilot textarea 组件、useMakeCopilotReadable hook 来向 Copilot 提供应用状态和其他信息,以及 useCopilotAction hook 来为 Copilot 提供可调用的操作。
首先,在 /[root]/src/app/components/Article.tsx 文件顶部导入 useMakeCopilotReadable、useCopilotAction、CopilotTextarea 和 HTMLCopilotTextAreaElement hooks。
import {
useMakeCopilotReadable,
useCopilotAction,
} from "@copilotkit/react-core";
import {
CopilotTextarea,
HTMLCopilotTextAreaElement,
} from "@copilotkit/react-textarea";
在 Article 函数内,在状态变量下方添加以下代码,该代码使用 useMakeCopilotReadable hook 将生成的文章大纲作为应用内聊天机器人的上下文添加。该 hook 使文章大纲对 copilot 可读。
useMakeCopilotReadable("Blog article outline: " + JSON.stringify(articleOutline));
在 useMakeCopilotReadable hook 下方,使用下面的代码创建一个名为 copilotTextareaRef 的引用,指向一个名为 HTMLCopilotTextAreaElement 的 textarea 元素。
const copilotTextareaRef = useRef<HTMLCopilotTextAreaElement>(null);
在上述代码下方,添加以下代码,该代码使用 useCopilotAction hook 设置一个名为 researchBlogArticleTopic 的操作,该操作将启用对给定主题的博客文章研究。该操作接受两个名为 articleTitle 和 articleOutline 的参数,用于启用文章标题和大纲的生成。
该操作包含一个处理程序函数,该函数基于给定的主题生成文章标题和大纲。在处理程序函数内,articleOutline 状态使用新生成的大纲更新,同时 articleTitle 状态使用新生成的标题更新,如下所示。
useCopilotAction(
{
name: "researchBlogArticleTopic",
description: "Research a given topic for a blog article.",
parameters: [
{
name: "articleTitle",
type: "string",
description: "Title for a blog article.",
required: true,
},
{
name: "articleOutline",
type: "string",
description:"Outline for a blog article that shows what the article covers.",
required: true,
},
],
handler: async ({ articleOutline, articleTitle }) => {
setArticleOutline(articleOutline);
setArticleTitle(articleTitle);
},
},
[]
);
在上述代码下方,进入表单组件并添加以下 CopilotTextarea 元素,该元素将使你能够对文章内容进行补全、插入和编辑。
<CopilotTextarea
value={copilotText}
ref={copilotTextareaRef}
placeholder="Write your article content here"
onChange={(event) => setCopilotText(event.target.value)}
className="p-4 w-full aspect-square font-bold text-xl bg-slate-800 text-white rounded-lg resize-none"
placeholderStyle={{
color: "white",
opacity: 0.5,
}}
autosuggestionsConfig={{
textareaPurpose: articleTitle,
chatApiConfigs: {
suggestionsApiConfig: {
forwardedParams: {
max_tokens: 5,
stop: ["\n", ".", ","],
},
},
insertionApiConfig: {},
},
debounceTime: 250,
}}
/>
然后为文章内容的 Textarea 添加 Tailwindcss hidden 类,如下所示。该 textarea 将保存文章的内容,并在发布文章后使其能够被插入到数据库中。
{/* Textarea for article content */}
<textarea
className="p-4 w-full aspect-square font-bold text-xl bg-slate-800 text-white rounded-lg resize-none hidden"
id="content"
name="content"
value={copilotText}
placeholder="Write your article content here"
onChange={(event) => setCopilotText(event.target.value)}
/>
之后,进入 /[root]/src/app/writearticle/page.tsx 文件,并使用下面的代码在顶部导入 CopilotKit 前端包和样式。
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotSidebar } from "@copilotkit/react-ui";
import "@copilotkit/react-ui/styles.css";
import "@copilotkit/react-textarea/styles.css";
然后使用 CopilotKit 和 CopilotSidebar 包装 Article 组件,如下所示。CopilotKit 组件指定 CopilotKit 后端端点的 URL(/api/copilotkit/openai/),而 CopilotSidebar 渲染应用内聊天机器人,你可以向其提供提示以研究文章的任何主题。
export default function WriteArticle() {
return (
<>
<Header />
<CopilotKit url="/api/copilotkit">
<CopilotSidebar
instructions="Help the user research a blog article topic."
defaultOpen={true}
labels={{
title: "Blog Article Copilot",
initial:
"Hi you! 👋 I can help you research any topic for a blog article.",
}}
clickOutsideToClose={false}>
<Article />
</CopilotSidebar>
</CopilotKit>
</>
);
}
之后,运行开发服务器并导航至 http://localhost:3000/writearticle。你应该会看到应用内聊天机器人已集成到博客平台中。
在右侧给聊天机器人输入提示词,比如"研究一篇关于生成式AI的博客文章主题,并给我文章大纲"。聊天机器人将开始研究该主题,然后生成博客标题。
当你在编辑器中开始写作时,你应该会看到内容自动建议,如下所示。
在本部分,我将引导你完成将博客平台与 Supabase 数据库集成的过程,以插入和获取博客文章数据。
要开始,请导航至 supabase.com,然后在主页上点击"开始你的项目"按钮。
然后创建一个名为 AiBloggingPlatform 的新项目,如下所示。
项目创建完成后,将你的 Supabase URL 和 API 密钥添加到 env.local 文件中的环境变量,如下所示。
NEXT_PUBLIC_SUPABASE_URL="Your Supabase URL"
NEXT_PUBLIC_SUPABASE_ANON_KEY="Your Supabase API Key"
之后,进入你的 Supabase 项目仪表板,打开 SQL 编辑器部分。然后将以下 SQL 代码添加到编辑器中,并按 CTRL + Enter 键创建一个名为 articles 的表。articles 表包含 id、title 和 content 列。
create table if not exists
articles (
id bigint primary key generated always as identity,
title text,
content text
);
表创建完成后,你应该会获得一条成功消息,如下所示。
之后,进入 /[root]/src/ 文件夹并创建一个名为 utils 的文件夹。在 utils 文件夹内,创建一个名为 supabase.ts 的文件,并添加以下代码来创建和返回一个 Supabase 客户端。
// 从 Supabase SSR 包导入必要的函数和类型
import { createServerClient, type CookieOptions } from '@supabase/ssr'
// 定义一个名为 'supabase' 的函数,它接收一个 'CookieOptions' 对象作为输入
export const supabase = (cookies: CookieOptions) => {
// 从提供的 'CookieOptions' 对象中检索 cookies
const cookieStore = cookies()
// 创建并返回一个配置了环境变量和 cookie 处理的 Supabase 客户端
return createServerClient(
// 从环境变量中检索 Supabase URL
process.env.NEXT_PUBLIC_SUPABASE_URL!,
// 从环境变量中检索 Supabase 匿名密钥
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
// 定义一个自定义的 'get' 函数以从 cookie 存储中按名称检索 cookies
get(name: string) {
return cookieStore.get(name)?.value
},
},
}
)
}
然后进入 /[root]/src/app 文件夹并创建一个名为 serveractions 的文件夹。在 serveractions 文件夹内,创建一个名为 AddArticle.ts 的文件,并添加以下代码以将博客文章数据插入到 Supabase 数据库中。
// 导入服务端操作所需的必要函数和模块
"use server";
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
// 定义一个名为 'addArticle' 的异步函数,它接收表单数据作为输入
export async function addArticle(formData: any) {
// 从提供的表单数据中提取标题和内容
const title = formData.get("title");
const content = formData.get("content");
// 从 HTTP 头中检索 cookies
const cookieStore = cookies();
// 创建一个配置了提供的 cookies 的 Supabase 客户端
const supabase = createServerComponentClient({ cookies: () => cookieStore });
// 将文章数据插入到 Supabase 上的 'articles' 表中
const { data, error } = await supabase.from("articles").insert([
{
title,
content,
},
]);
// 检查插入过程中是否有错误
if (error) {
console.error("Error inserting data", error);
return;
}
// 成功添加文章后,将用户重定向到主页
redirect("/");
// 返回成功消息
return { message: "Success" };
}
之后,进入 /[root]/src/app/components/Article.tsx 文件并导入 addArticle 函数。
import { addArticle } from "../serveractions/AddArticle";
然后将 addArticle 函数添加为表单 action 参数,如下所示。
// 用于文章输入的表单元素
<form
action={addArticle}
className="w-full h-full gap-10 flex flex-col items-center p-10">
</form>
之后,导航至 http://localhost:3000/writearticle,研究你选择的主题,添加文章内容,然后点击底部的发布按钮来发布文章。
然后进入你的 Supabase 项目仪表板并导航至表编辑器部分。你应该会看到你的文章数据已被插入到 Supabase 数据库中,如下所示。
接下来,进入 /[root]/src/app/page.tsx 文件并在顶部导入 cookies 和 supabase 包。
import { cookies } from "next/headers";
import { supabase } from "@/utils/supabase";
然后在 Home 函数内部,添加以下代码以从 Supabase 数据库中获取文章数据。
const { data: articles, error } = await supabase(cookies).from('articles').select('*')
之后,如下所示更新元素代码以在博客平台主页上渲染已发布的文章。
return (
<>
<Header />
<div className="max-w-[85rem] h-full px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto">
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
{articles?.map((post: any) => (
<Link
key={post.id}
className="group flex flex-col h-full bg-white border border-gray-200 hover:border-transparent hover:shadow-lg transition-all duration-300 rounded-xl p-5 "
href={`/posts/${post.id}`}>
<div className="aspect-w-16 aspect-h-11">
<Image
className="object-cover h-48 w-96 rounded-xl"
src={`https://source.unsplash.com/featured/?${encodeURIComponent(
post.title
)