CopilotKit
通过自定义端点、Python AG-UI 桥接器、结构化生成式 UI 以及即时通讯平台频道,在 LangGraph、Deep Agents 和 React 中使用 CopilotKit
CopilotKit 提供了完整的 React 聊天运行时,当你希望 agent 返回结构化 UI 载荷而不仅仅是纯文本时,它与 LangGraph 的配合尤为出色。前端与 CopilotKit 运行时 URL 通信,并将助手消息解析为动态 React 组件。
在这种模式下,你的 LangGraph 部署在同一进程中同时提供图 API 和自定义 CopilotKit 端点。
在服务端,copilotkit 包提供了 CopilotKitMiddleware,使 LangGraph 图、LangChain agent 或 Deep Agent 能够使用 Agent UI (AG-UI) 有线协议,将工具和消息事件流式传输到聊天 UI,并读取或写入共享的 CopilotKit 状态切片。TypeScript 设置通常将 CopilotKit 运行时挂载在图 API 旁。Python 设置则在部署上挂载一个 AG-UI FastAPI 桥接器,并在其前面通过 Node 运行 CopilotKit 运行时。
当你希望实现以下需求时,这种方法非常有用:
- 一个现成的聊天运行时,而无需自行连接
stream.messages - 一个可以在已部署的图旁添加特定提供商行为的自定义服务端点
- 从受约束的组件注册表中渲染结构化生成式 UI
CopilotKit for LangGraph 还介绍了在相同中间件和客户端之上的生成式 UI、人机协同(HITL)以及共享状态。
NOTE
有关 CopilotKit 特定的 API、UI 模式和运行时配置,请参阅 CopilotKit 文档。有关 Deep Agent 的演练,请参阅 CopilotKit 文档中的 Deep Agents 与 CopilotKit。
工作原理
从高层次来看,CopilotKit 位于你的 React 应用与 LangGraph 部署之间。
前端将对话状态发送到与图 API 共同挂载的自定义 /api/copilotkit 路由。该路由将请求转发给 LangGraph,响应返回时既包含助手消息,也包含你的组件注册表可以渲染的任何结构化 UI 载荷。
- 像往常一样部署图:使用 LangSmith 或 LangGraph 开发服务器。
- 使用 HTTP 应用扩展部署:在图 API 旁挂载 CopilotKit 路由。
- 在前端使用
CopilotKit包裹,并将其指向该自定义运行时 URL。 - 注册动态 UI 组件,并在渲染时将助手响应解析为这些组件。
安装
对于后端端点:
bun add @copilotkit/runtime hono对于前端应用:
bun add @copilotkit/react-core @copilotkit/react-ui @hashbrownai/core @hashbrownai/react使用自定义端点扩展 LangGraph 部署
核心理念是 LangGraph 部署不仅可以提供图服务。它还可以加载一个 HTTP 应用,从而允许你在部署本身旁边挂载额外的路由。
在 langgraph.json 中,将 http.app 指向你的自定义应用入口点:
{
"graphs": {
"copilotkit_shadify": "./src/agents/copilotkit-shadify.ts:agent"
},
"http": {
"app": "./src/api/app.ts:app"
}
}然后创建 Hono 应用并注册 CopilotKit 路由:
import { Hono } from "hono";
import { registerCopilotKit } from "./copilotkit.js";
export const app = new Hono();
registerCopilotKit(app);这个自定义应用是关键的扩展点:它在不替换底层 LangGraph 部署的情况下挂载了支持 CopilotKit 的运行时。
在该路由内部,创建一个 CopilotRuntime,并使用 LangGraphAgent 将其指回已部署的图:
import { type Hono } from "hono";
import { createCopilotEndpointSingleRoute, CopilotRuntime } from "@copilotkit/runtime/v2";
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
const defaultAgentHost = process.env.LANGGRAPH_DEPLOYMENT_URL || "http://127.0.0.1:2024";
const agentUrl = defaultAgentHost.startsWith("http")
? defaultAgentHost
: `http://${defaultAgentHost}`;
class BridgedLangGraphAgent extends LangGraphAgent {
override prepareRunAgentInput(
input: Parameters<LangGraphAgent["prepareRunAgentInput"]>[0],
): ReturnType<LangGraphAgent["prepareRunAgentInput"]> {
const prepared = super.prepareRunAgentInput(input);
return {
...prepared,
context: normalizeCopilotContext(prepared.context) as ReturnType<
LangGraphAgent["prepareRunAgentInput"]
>["context"],
};
}
override async getAssistant(): Promise<Awaited<ReturnType<LangGraphAgent["getAssistant"]>>> {
const assistants = await this.client.assistants.search({
graphId: this.graphId,
limit: 100,
});
const assistant = assistants.find((candidate) => candidate.graph_id === this.graphId);
if (assistant) {
return assistant;
}
return super.getAssistant();
}
}
export function registerCopilotKit(app: Hono) {
const runtime = new CopilotRuntime({
agents: {
default: new BridgedLangGraphAgent({
deploymentUrl: agentUrl,
graphId: "copilotkit_shadify",
}),
},
});
const copilotApp = createCopilotEndpointSingleRoute({
runtime,
basePath: "/api/copilotkit",
});
app.route("/", copilotApp);
}
function normalizeCopilotContext(context: unknown): unknown {
if (!Array.isArray(context)) {
return context;
}
const normalizedEntries = context.flatMap((item) => {
if (!item || typeof item !== "object") {
return [];
}
const entry = item as { description?: unknown; value?: unknown };
return typeof entry.description === "string" ? [[entry.description, entry.value] as const] : [];
});
return Object.fromEntries(normalizedEntries);
}路由适配器只是 TypeScript 设置的一半。你的 LangChain agent 还需要中间件来读取转发的 output_schema,并将其转换为模型的结构化 responseFormat:
import { createAgent, createMiddleware, toolStrategy } from "langchain";
import { z } from "zod";
import { deepSearchTool, searchWebTool } from "../tools/index.js";
const contextSchema = z.object({
output_schema: z.unknown().optional(),
});
const structuredOutputMiddleware = createMiddleware({
name: "CopilotKitStructuredOutput",
contextSchema,
wrapModelCall: async (request, handler) => {
const rawOutputSchema = getRuntimeOutputSchema(request.runtime);
const schema = normalizeOutputSchema(rawOutputSchema);
if (!schema) {
return handler(request);
}
const responseFormat = toolStrategy(
schema as unknown as Parameters<typeof toolStrategy>[0],
{
toolMessageContent: "Structured UI response generated.",
},
);
return handler({
...request,
responseFormat,
});
},
});
export const agent = createAgent({
model: process.env.COPILOTKIT_MODEL ?? "google:gemini-3.6-flash",
contextSchema,
middleware: [structuredOutputMiddleware],
tools: [searchWebTool, deepSearchTool],
systemPrompt: `You are a helpful UI assistant inspired by the CopilotKit Shadify example.
Build rich visual responses with the available UI components when they add value.
Only wrap actual UI layouts inside cards. Plain Markdown answers should stay as Markdown.
Use rows for side-by-side layouts with at most two columns.
Prefer simple, polished outputs over dense dashboards.
When using charts, make labels and values concise and easy to read.
When showing code, prefer the code_block component.
When researching topics, use the available search tools first and then present the result cleanly.`,
});
function normalizeOutputSchema(value: unknown): Record<string, unknown> | null {
let schema = value;
if (typeof schema === "string") {
try {
schema = JSON.parse(schema);
} catch {
return null;
}
}
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
return null;
}
const normalized = { ...(schema as Record<string, unknown>) };
if (!normalized.title) {
normalized.title = "CopilotKitStructuredOutput";
}
if (!normalized.description) {
normalized.description = "Structured response schema for the CopilotKit preview.";
}
return normalized;
}
function getRuntimeOutputSchema(runtime: {
context?: { output_schema?: unknown };
configurable?: Record<string, unknown>;
}): unknown {
if (runtime.context?.output_schema !== undefined) {
return runtime.context.output_schema;
}
const configurable = runtime.configurable;
if (!configurable || typeof configurable !== "object" || Array.isArray(configurable)) {
return undefined;
}
return configurable.output_schema;
}该中间件使前端的 useAgentContext({ description: "output_schema", ... }) 能够发挥作用。CopilotKit 运行时转发该模式(schema),agent 则将其转换为模型必须遵循的结构化输出契约。
其结果是清晰的关注点分离:
- LangGraph 仍然负责图的执行和持久化
- CopilotKit 负责面向聊天的运行时契约
- 你的自定义端点在单个部署中将它们粘合在一起
组织前端应用结构
在前端,使用 CopilotKit 包裹你的应用,并将其指向自定义运行时 URL:
import { CopilotKit } from "@copilotkit/react-core";
import { CopilotChat, useAgentContext } from "@copilotkit/react-core/v2";
import { s } from "@hashbrownai/core";
import { useChatKit } from "@/components/chat/chat-kit";
import { chatTheme } from "@/lib/chat-theme";
export function App() {
return (
<CopilotKit runtimeUrl={import.meta.env.VITE_RUNTIME_URL ?? "/api/copilotkit"}>
<Page />
</CopilotKit>
);
}
function Page() {
const chatKit = useChatKit();
useAgentContext({
description: "output_schema",
value: s.toJsonSchema(chatKit.schema),
});
return <CopilotChat {...chatTheme} />;
}这里有两个关键部分:
runtimeUrl="/api/copilotkit"将聊天发送到你的自定义后端路由,而不是直接发送到原始 LangGraph APIuseAgentContext(...)将 UI 模式发送给 agent,以便模型了解其应该生成的结构化输出格式
注册动态组件
组件注册表位于 useChatKit() 中。在这里你可以定义允许 agent 生成的组件集,例如卡片(card)、行(row)、列(column)、图表(chart)、代码块(code block)和按钮(button)。
import { s } from "@hashbrownai/core";
import { exposeComponent, exposeMarkdown, useUiKit } from "@hashbrownai/react";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { CodeBlock } from "@/components/ui/code-block";
import { Row, Column } from "@/components/ui/layout";
import { SimpleChart } from "@/components/ui/simple-chart";
export function useChatKit() {
return useUiKit({
components: [
exposeMarkdown(),
exposeComponent(Card, {
name: "card",
description: "Card to wrap generative UI content.",
children: "any",
}),
exposeComponent(Row, {
name: "row",
props: {
gap: s.string("Tailwind gap size") as never,
},
children: "any",
}),
exposeComponent(Column, {
name: "column",
children: "any",
}),
exposeComponent(SimpleChart, {
name: "chart",
props: {
labels: s.array("Category labels", s.string("A label")),
values: s.array("Numeric values", s.number("A value")),
},
children: false,
}),
exposeComponent(CodeBlock, {
name: "code_block",
props: {
code: s.streaming.string("The code to display"),
language: s.string("Programming language") as never,
},
children: false,
}),
exposeComponent(Button, {
name: "button",
children: "text",
}),
],
});
}该注册表成为了 agent 与 UI 之间的契约。模型并不是在生成任意 JSX,而是在生成必须针对你公开的组件和属性进行验证的结构化数据。
将助手消息渲染为动态 UI
一旦助手响应到达,自定义消息渲染器就会决定如何显示它。在此示例中:
- 助手消息会根据 UI 工具包模式解析为结构化 JSON
- 有效的结构化输出被渲染为真实的 React 组件
- 用户消息则渲染为普通的聊天气泡
import type { AssistantMessage } from "@ag-ui/core";
import type { RenderMessageProps } from "@copilotkit/react-ui";
import { useJsonParser } from "@hashbrownai/react";
import { memo } from "react";
import { useChatKit } from "@/components/chat/chat-kit";
import { Squircle } from "@/components/squircle";
const AssistantMessageRenderer = memo(function AssistantMessageRenderer({
message,
}: {
message: AssistantMessage;
}) {
const kit = useChatKit();
const { value } = useJsonParser(message.content ?? "", kit.schema);
if (!value) return null;
return (
<div className="group/msg mt-2 flex w-full justify-start">
<div className="magic-text-output w-full px-1 py-1">{kit.render(value)}</div>
</div>
);
});
export function CustomMessageRenderer({ message }: RenderMessageProps) {
if (message.role === "assistant") {
return <AssistantMessageRenderer message={message} />;
}
return (
<div className="flex w-full justify-end">
<Squircle className="w-full max-w-[64ch] px-4 py-3">
<pre>{typeof message.content === "string" ? message.content : JSON.stringify(message.content, null, 2)}</pre>
</Squircle>
</div>
);
}这种渲染器模式使集成体验感觉原生:
- CopilotKit 处理聊天状态和传输
- 自定义渲染器决定助手载荷如何转变为 UI
- Hashbrown 将经过验证的结构化数据转换为具体的 React 元素
频道(Channels)
驱动应用内 copilot 的同一个 agent 也可以作为机器人运行在 Slack 及其他即时通讯平台上。CopilotKit Channels 通过托管的 CopilotKit Intelligence 连接将你的 LangChain agent 连接到即时通讯平台:Intelligence 保存平台凭据并负责消息传递,而你的进程则运行该 agent。
NOTE
Channels 需要 @copilotkit/channels 0.6.1 和 @copilotkit/runtime 1.65.0(作为经过测试的配对版本一同安装),以及在长期运行的主机上运行 Node.js 22 或更高版本。频道连接的 LangChain 部署可以是 Python 或 TypeScript。
NOTE
本页展示了 Channels 如何与 LangChain agent 结合使用。有关具有 LangGraph 后端的完整 Slack 演练,请参阅在 Slack 中连接并运行你的 agent。有关其他 agent 后端和平台支持范围,请参阅 Channels 概述。
运作机制
CopilotKit Intelligence 拥有平台连接(Slack、Microsoft Teams 等)及其凭据。长期运行的 Channels 监听器拥有 agent、工具和应用程序逻辑。每一轮交互都遵循相同的路径:
- 用户在 Slack 中向你的应用发送消息。
- Intelligence 使用为该 Channel 配置的凭据接收平台事件。
- 持久的 Intelligence 网关连接将该轮交互交付给你的 Channels 监听器。
- 监听器通过 AG-UI 运行你的 LangChain agent 并渲染回复。
- Intelligence 将结果作为原生 Slack Block Kit 发送回去。
平台凭据绝不会进入 agent 进程。在 CopilotKit Intelligence 中创建一个托管 Channel 并连接 Slack:Intelligence 会存储 Slack 凭据,并为监听器提供一个频道代码(CHANNEL_CODE)以及项目范围的 Intelligence API 密钥(INTELLIGENCE_API_KEY)。
安装
安装经过测试的 SDK 配对版本,然后安装 TypeScript 工具:
npm install --save-exact @copilotkit/channels@0.6.1 @copilotkit/runtime@1.65.0
npm install -D tsx typescript @types/nodepnpm add --save-exact @copilotkit/channels@0.6.1 @copilotkit/runtime@1.65.0
pnpm add -D tsx typescript @types/nodeyarn add --exact @copilotkit/channels@0.6.1 @copilotkit/runtime@1.65.0
yarn add -D tsx typescript @types/node使用 NodeNext TypeScript 配置:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"types": ["node"]
},
"include": ["*.ts", "*.tsx"]
}连接你的 LangChain Agent
为每个对话返回一个全新的 agent(以线程 thread 为键),这样状态就不会在不同的 Slack 线程之间泄漏。LangGraph Server 使用 LangGraph API 通信,因此请使用 @copilotkit/runtime/langgraph 中的 LangGraphAgent,而不是 HttpAgent。将其指向你已经运行的部署:
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
// 为每个对话创建全新的 agent,以 thread 为键。
export function makeAgent(threadId: string) {
const agent = new LangGraphAgent({
deploymentUrl: process.env.LANGGRAPH_DEPLOYMENT_URL!,
graphId: "copilotkit_shadify", // 上文部署的图
});
agent.threadId = threadId;
return agent;
}运行 Channel 监听器
使用来自 Intelligence 的 Code 声明 Channel,并将每条消息转发给 agent。创建 Node 监听器正是连接 Channel 的关键动作。在创建监听器之前注册进程清理处理,以便在连接窗口期间按下 Ctrl-C 仍能干净地停止 Channel。
import { createServer } from "node:http";
import { createChannel } from "@copilotkit/channels";
import { CopilotKitIntelligence, CopilotRuntime } from "@copilotkit/runtime/v2";
import { createCopilotNodeListener } from "@copilotkit/runtime/v2/node";
import { makeAgent } from "./agent.js";
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing required environment variable: ${name}`);
return value;
}
// `name` 必须与 CopilotKit Intelligence 中显示的 Channel Code 一致。
const channel = createChannel({
name: required("CHANNEL_CODE"),
identifyUser: "platform",
agent: makeAgent,
});
// 每条传入消息都会开启一轮对话;runAgent 会流式传回回复。
channel.onMessage(async ({ thread, message }) => {
await thread.runAgent({
prompt: message.contentParts?.length
? [
...(message.text ? [{ type: "text" as const, text: message.text }] : []),
...message.contentParts,
]
: message.text,
context: [{ description: "Originating platform", value: message.platform }],
});
});
const intelligence = new CopilotKitIntelligence({
apiKey: required("INTELLIGENCE_API_KEY"),
});
const runtime = new CopilotRuntime({
agents: {},
intelligence,
channels: [channel],
});
// 在监听器存在之前绑定清理逻辑,因为创建它就会启动 Channel。
// 这样在连接窗口期间按下 Ctrl-C 仍能将其清理关闭。
let teardown: (() => Promise<void>) | undefined;
const shutdown = async () => {
await teardown?.();
};
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
const listener = createCopilotNodeListener({
runtime,
basePath: "/api/copilotkit",
});
const channels = listener.channels;
const server = createServer(listener);
teardown = async () => {
await channels.stop();
if (server.listening) server.close();
};
// 创建监听器即可启动 Channel;不存在 `channel.start()`。
// `ready()` 是可选的;在报告 Channel 在线之前先检查 `status()`。
await channels.ready({ timeoutMs: 30_000 });
if (channels.status().overall !== "online") {
throw new Error("Slack Channel is not online");
}
const port = Number(process.env.PORT ?? 3000);
server.listen(port, () => {
console.log(`Slack Channel online; listening on :${port}`);
});设置运行时机密,然后启动进程:
INTELLIGENCE_API_KEY=<project-api-key>
CHANNEL_CODE=support-slack
LANGGRAPH_DEPLOYMENT_URL=http://127.0.0.1:2024
PORT=3000node --env-file=.env --import tsx channel.ts一旦进程连接成功,该 Channel 在 Intelligence 中就会从 Waiting for runtime(等待运行时)变为 Online(在线)。
其他平台
托管的 Slack 已正式发布(GA)。托管的 Teams 属于受控集成目标。Channels SDK 还提供了针对 Teams、Discord、Telegram 和 WhatsApp 的直接适配器。有关当前平台列表和各平台设置,请参阅 Channels 概述。
资源
- CopilotKit 文档中的 Deep Agents 与 CopilotKit —— 端到端 Next.js、开发服务器以及 Deep Agent 路径
- CopilotKit: LangGraph 特性 —— 生成式 UI、人机协同(HITL)、共享状态
- LangGraph 部署 —— 生产环境与开发服务器
最佳实践
- 保持自定义端点轻量:使用它来使 CopilotKit 适配你的图部署,而不是重复已经在图中存在的业务逻辑
- 显式发送模式(schema):每次页面挂载时,
useAgentContext都应该描述 UI 契约 - 注册受约束的组件集:仅公开你确实希望模型使用的组件和 props
- 将渲染视为解析步骤:在渲染助手内容之前,根据你的模式对其进行解析
- 保持用户消息简单:只有助手消息需要结构化渲染器;用户消息可以保留为普通聊天气泡
