关注「索引目录」公众号,获取更多干货。
大多数人工智能集成都存在过度设计的问题。
就连添加AI助手的入门教程都会把LangGraph工作流和FastAPI服务器混为一谈。你根本不需要这些。
今天,我们将构建一个无需任何外部代理框架或编排层的AI。它只需要一个了解你的应用内容并能实际执行操作的AI。
你还将学习如何实现生成式用户界面模式,该模式允许你渲染实际组件,而不仅仅是文本响应。
让我们开始吧。
为什么人工智能集成会变得复杂
在应用程序中添加人工智能听起来很简单,但当你真正尝试时就会发现并非如此。
一旦你超越了基本的聊天界面,就会遇到一个根本性的问题:LLM本身什么都做不了,它们只能生成文本。
要创建一个能够真正执行操作的人工智能:读取应用程序的状态、更新数据、调用 API——你需要自己构建底层架构。
人工智能代理的本质就是这种底层架构。从本质上讲,每个代理都运行着相同的循环:
-
观察——考虑上下文(用户输入、工具结果、内存) -
理由——决定下一步该怎么做 -
执行操作——调用工具、写入文件、调用 API,或者做任何需要做的事情。
理论上很简单。但实际上,你最终需要编写编排层来管理这个循环,编写工具注册表将应用程序的功能暴露给 LLM,编写状态管理来跟踪代理知道的内容,以及编写错误处理程序来处理模型出现异常时的错误。
context = [initial_event]
while True:
next_step = await llm.determine_next_step(context)
context.append(next_step)
if next_step.intent == "done":
return next_step.final_answer
result = await execute_step(next_step)
context.append(result)
在完整的代理设置中,LLM 位于中心——协调数据源、工具、模型和外部服务。
这就是为什么教程会讲解 LangGraph 和 FastAPI 服务器的原因。对于复杂的自动化流程来说,这种复杂性是合理的。
但如果你的目标只是在你的产品中添加一个 AI 助手——一个能够理解你的用户界面、读取你的数据并代表用户执行操作的助手,那么你就是在构建大量的基础设施来解决一个小得多的问题。
CopilotKit处理循环、上下文、流式传输和前端集成,因此您无需自己进行任何连接。
在本教程中,我们将使用 CopilotKit,无需任何外部代理框架,只需钩子和您选择的 LLM。
CopilotKit 的实际功能
CopilotKit是一个开源框架,可将 AI 代理添加到您的应用程序中,它抽象化了您刚才读到的所有内容。
您无需构建自己的代理循环、工具注册表和流式传输层,而是可以获得一组钩子和预构建组件,这些组件可以直接插入到您现有的前端中。
这个思维模型很简单,你只需要向人工智能提供两样东西:
- 您的应用程序状态
——以便它了解屏幕上显示的内容以及用户正在操作的内容。 - 一套工具
——这样它就能真正做一些有用的事情,而不仅仅是回复文本。
实际上,这对应着两个钩子:
- useAgentContext
- 与 AI 共享应用状态。您传递的任何内容都会成为 LLM 上下文的一部分:当前用户、选定项目、已加载数据 - AI 了解您的应用所知道的一切。 - useFrontendTool
- 定义 AI 可以触发的操作。您需要为其命名、描述并指定处理程序。LLM 会根据用户的请求决定何时调用它。
结果是,在应用程序中添加 AI 助手变成了一个前端问题,而不是一个基础设施问题。
您的用户界面、代理和工具都在同一个交互循环中。
如何连接所有线路(1分钟)
你可以参考官方文档,也可以直接看我的教程。我会详细解释每个部分的作用和原因。
我使用的是 Next.js 和 TypeScript,但这也适用于任何 React/Angular 框架!
// creates a nextjs app
npx create-next-app@latest .
安装所需的三个 CopilotKit 软件包:
npm install @copilotkit/react-core @copilotkit/react-ui @copilotkit/runtime
@copilotkit/react-core/v2:钩子和内置聊天组件 @copilotkit/react-ui/v2样式 @copilotkit/runtime后端运行时和LLM适配器
创建 API 路由app/api/copilotkit/route.ts。
它是接收来自用户界面的消息、通过代理循环处理消息并将响应流式返回的唯一端点。
BuiltInAgent就是运行观察→推理→行动循环的程序。 CopilotRuntime管理会话、流式传输和工具执行
这就是你的整个后端。
import { CopilotRuntime, copilotRuntimeNextJSAppRouterEndpoint } from "@copilotkit/runtime";
import { BuiltInAgent } from "@copilotkit/runtime/v2";
import { NextRequest } from "next/server";
const builtInAgent = new BuiltInAgent({
model: "openai:gpt-5",
// apiKey: process.env.OPENAI_API_KEY,
});
const runtime = new CopilotRuntime({
agents: { default: builtInAgent },
});
export const POST = async (req: NextRequest) => {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
endpoint: "/api/copilotkit",
});
return handleRequest(req);
};
.env.local在根目录下创建并添加您的OpenAI API 密钥。
OPENAI_API_KEY=sk-proj-...
如果您想切换到其他 LLM 提供商,只需BuiltInAgent在 API 路由中传递修改后的字符串即可。其他所有设置保持不变。
// Anthropic
const builtInAgent = new BuiltInAgent({ model: "anthropic:claude-sonnet-4-5" });
// Google
const builtInAgent = new BuiltInAgent({ model: "google:gemini-2.0-flash" });
添加匹配的键,.env.local就完成了。对于 Azure OpenAI、AWS Bedrock 或 Ollama 等自定义模型,请查看模型选择文档。
现在,通过将你的应用程序包装在 . 中,将前端连接到后端app/layout.tsx。
import { CopilotKit } from "@copilotkit/react-core";
import "@copilotkit/react-ui/v2/styles.css";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<CopilotKit runtimeUrl="/api/copilotkit">
{children}
</CopilotKit>
</body>
</html>
);
}
CopilotKit是其下所有钩子所依赖的上下文提供程序。runtimeUrl指向您刚刚创建的路由。
CopilotSidebar、CopilotChat和CopilotPopup等都是内置组件。本文将介绍如何添加聊天侧边栏。
import { CopilotSidebar } from "@copilotkit/react-core/v2";
export default function Page() {
return (
<main>
<h1>Your App</h1>
<CopilotSidebar />
</main>
);
}
运行一下npm run dev,你现在就拥有一个可用的AI侧边栏助手啦!🎉
它可以对任何操作做出响应,但它完全不知道你的应用里实际有什么。它不知道你的数据、你的用户界面状态,也不知道用户正在查看什么。接下来的两个步骤将解决这个问题。
为人工智能提供背景信息
默认情况下,人工智能对你的应用内容一无所知。你可以问它“我花了多少钱买食物?”,它完全不知道——它只能看到对话内容,看不到你应用的运行状态。
useAgentContext可以解决这个问题。它会在每一回合都将你的 React 状态推送到代理的上下文窗口中,这样 AI 就能始终掌握 UI 中内容的最新快照。
让我们构建一个简单的费用跟踪器来看看它是如何运作的。
"use client";
import { useState } from "react";
import { useAgentContext, CopilotSidebar } from "@copilotkit/react-core/v2";
type Expense = {
id: number;
description: string;
amount: number;
category: string;
};
const initialExpenses: Expense[] = [
{ id: 1, description: "Groceries", amount: 85, category: "Food" },
{ id: 2, description: "Netflix", amount: 15, category: "Entertainment" },
{ id: 3, description: "Uber", amount: 22, category: "Transport" },
];
export default function Page() {
const [expenses, setExpenses] = useState<Expense[]>(initialExpenses);
useAgentContext({
description: "The user's current expense list. Each item has an id, description, amount in dollars, and category.",
value: expenses,
});
return (
<main className="p-8">
<h1 className="text-2xl font-bold mb-4">My Expenses</h1>
<ul className="space-y-2">
{expenses.map((e) => (
<li key={e.id} className="flex justify-between border-b py-2">
<span>
{e.description}{" "}
<span className="text-gray-400 text-sm">({e.category})</span>
</span>
<span>${e.amount}</span>
</li>
))}
</ul>
<CopilotSidebar />
</main>
);
}
expenses这是你的普通 React 状态。useAgentContext它会在每一轮调用时将其注入到 LLM 的上下文中。
现在,如果用户问“我最大的开支是什么?”,人工智能会查看与你的用户界面渲染的同一个列表。
让人工智能做事
意识是一回事,但能够采取行动才是使智能体真正发挥作用的关键。
useFrontendTool允许您向 AI 提供一组它可以触发的操作。您可以定义一个工具,包括名称、描述和处理程序——LLM 会根据用户的请求决定何时调用它。
LLM 会读取description字段以了解每个工具的功能和每个参数的预期值,因此请清晰地编写它们,即使是随意的对话输入,模型也能准确地填充它们。
添加useFrontendTool到同一页面内。
parameters它接受一个Zod schema(一个以 TypeScript 为主的验证库)。如果您之前没有使用过它,它的原理很简单:您定义数据的结构z.object({...}),每个字段都会收到一个.describe()调用,该调用告诉 LLM 该字段的含义。
使用以下命令安装npm install zod:
"use client";
import { useState } from "react";
import { useAgentContext, useFrontendTool, CopilotSidebar } from "@copilotkit/react-core/v2";
import { z } from "zod";
// ... type and initialExpenses stay the same
export default function Page() {
const [expenses, setExpenses] = useState<Expense[]>(initialExpenses);
// ... useAgentContext
useFrontendTool({
name: "addExpense",
description:
"Add a new expense when the user mentions spending money on something.",
parameters: z.object({
description: z
.string()
.describe("What the expense was for, e.g. Lunch, Taxi, Coffee"),
amount: z.number().describe("How much was spent in dollars"),
category: z
.string()
.describe("Category: Food, Transport, Entertainment, Health, or Other"),
}),
handler: async ({ description, amount, category }) => {
setExpenses((prev) => [
...prev,
{ id: Date.now(), description, amount, category },
]);
},
});
return (
// ... remains the same
);
}
您可以发送类似“我昨晚花了 40 美元吃晚饭”这样的示例查询。代理人打电话addExpense过来{ description: "Dinner", amount: 40, category: "Food" },您的处理程序更新信息expenses,新项目会立即出现在列表中。
这是观察→原因→行动的端到端循环。
代理程序通过观察您的消息和当前支出情况useAgentContext,判断这addExpense是正确的选择,并使用正确的参数调用了您的处理程序。CopilotKit 处理了所有中间步骤。
额外内容:生成式用户界面
人工智能可以读取并更新你的数据。但目前为止,它只能以文本形式回复。
生成式用户界面是一种新概念:它不是由代理描述结果,而是直接渲染实际的用户界面。例如,如果有人询问他们的消费情况,应用程序会显示一个明细卡片。
renderCopilotKit 通过属性支持此功能useFrontendTool。该工具不会返回文本回复,而是返回一个 React 组件——使用您自己的设计系统在聊天中内联渲染。
让我们在同一个组件中添加一个摘要工具。
import { ToolCallStatus, useFrontendTool } from "@copilotkit/react-core/v2";
useFrontendTool({
name: "showSpendingSummary",
description:
"Call this when the user asks for a summary or overview of their expenses.",
parameters: z.object({}),
handler: async () => {
const summary = expenses.reduce(
(acc, e) => {
acc[e.category] = (acc[e.category] ?? 0) + e.amount;
return acc;
},
{} as Record<string, number>,
);
const total = expenses.reduce((sum, e) => sum + e.amount, 0);
return JSON.stringify({ summary, total });
},
render: ({ result, status }) => {
return (
<div className="rounded-lg border p-4 mt-2 space-y-3">
<p className="font-semibold text-sm">
{status === ToolCallStatus.InProgress ? "Calculating..." : "Spending Breakdown"}
</p>
{status === ToolCallStatus.Complete && result && (
<>
{Object.entries(
(JSON.parse(result) as { summary: Record<string, number>; total: number }).summary
).map(([category, amount]) => (
<div key={category} className="flex justify-between text-sm">
<span className="text-gray-600">{category}</span>
<span className="font-medium">${amount}</span>
</div>
))}
<div className="flex justify-between text-sm font-semibold border-t pt-2">
<span>Total</span>
<span>${(JSON.parse(result) as { summary: Record<string, number>; total: number }).total}</span>
</div>
</>
)}
</div>
);
},
});
这里发生了什么:
parameters: z.object({})为空。LLM 不需要传递任何参数,它只需要决定何时调用该工具。 handler实际执行计算,根据状态计算类别总数 expenses并返回结果。ToolCallStatus提供工具调用的确切生命周期状态 result这是你的处理程序返回的 JSON 字符串,它被解析回一个对象。 render
当你问“汇总我的支出”时,LLM 会读取查询,查看上下文,识别出这showSpendingSummary是要调用的正确工具并触发它。
这就是模式:LLM 决定何时行动,你的代码执行工作,你的组件显示结果。
这个极简费用跟踪器的工作实现可以在GitHub 仓库的该example/basic分支中找到。
该main分支机构进一步采用了相同的模式,使用了完整的看板:
-
通过询问人工智能,将任务在不同列之间移动。 -
通过自然语言添加、删除和重新分配任务 -
利用生成式用户界面获取可视化看板摘要。
关注「索引目录」公众号,获取更多干货。

