Skip to content

CopilotKit 提供完整的 React 聊天运行时,当你希望智能体返回结构化 UI 负载而不仅仅是纯文本时,它与 LangGraph 尤其相配。在这种模式中,你的 LangGraph 部署同时提供图 API 和一个自定义 CopilotKit 端点,而前端将助手消息解析为动态 React 组件。

在服务器端,copilotkit 包提供 CopilotKitMiddleware,使 LangGraph 图、LangChain 智能体或 Deep Agent 能够使用 Agent UI (AG-UI) 线路协议进行通信、将工具和消息事件流式输出到聊天界面,以及读写共享的 CopilotKit 状态切片,并提供辅助函数在你的图前面挂载一个 CopilotKit 兼容的 HTTP 端点。

当你想要以下功能时,这种方法很有用:

  • 现成的聊天运行时,而不是自己连接 stream.messages
  • 一个自定义服务器端点,可以在已部署的图旁边添加特定于提供商的行为
  • 由受限组件注册表渲染的结构化生成式 UI

CopilotKit for LangGraph 文档还介绍了基于同一套中间件和客户端之上的生成式 UI人在回路(HITL)和共享状态

INFO

关于 CopilotKit 特有的 API、UI 模式和运行时配置,请参阅 CopilotKit 文档。如需 Deep Agent 演练,请参阅 CopilotKit 文档中的 Deep Agents 与 CopilotKit

import { ExampleEmbed } from "/snippets/example-embed.jsx"

工作原理

从高层来看,CopilotKit 位于你的 React 应用与 LangGraph 部署之间。前端将对话状态发送到一个与图 API 一起挂载的自定义 /api/copilotkit 路由,该路由将请求转发给 LangGraph,而响应则同时带回助手消息和你的组件注册表能够渲染的任何结构化 UI 负载。

  1. 照常部署图,使用 LangSmith 或 LangGraph 开发服务器。
  2. 用 HTTP 应用扩展部署,在与图 API 相邻的位置挂载一个 CopilotKit 路由。
  3. 将前端包裹在 CopilotKit,并指向该自定义运行时 URL。
  4. 注册动态 UI 组件,并在渲染时将助手响应解析为这些组件。

Python 服务器端能得到什么

copilotkit 及相关包在 LangGraph 部署与 CopilotKit 客户端之间架起桥梁。

组件作用
CopilotKitMiddleware将 CopilotKit 与 AG-UI 的状态和请求并入你的智能体,包括前端工具调用和上下文。将其添加到 create_agent 或 create_deep_agent 的 middleware 列表中。
CopilotKitState(子类)自定义状态:扩展 CopilotKitState,使 CopilotKit 键成为图状态的一部分。
LangGraphAGUIAgent将编译后的图与运行时所需的名称和描述打包在一起。
add_langgraph_fastapi_endpoint(来自 ag-ui-langgraph连接一个 FastAPI 应用,使 CopilotKit 能够在同一个 LangGraph 进程中运行你的图。当你在 langgraph.json 中添加自定义 http 应用而不是单独的 HTTP 服务器时使用它。

CopilotKitMiddleware 对 create_deep_agent 以及当你将其添加到 middleware 列表时来自 create_agent 的图是同一个中间件。对于带 CopilotKitState 和 FastAPI 桥接的 create_agent 图,请按照下面的 Python main.py 示例进行操作。结构化生成式 UI(例如来自客户端的 useAgentContextoutput_schema)需要额外的中间件,将 Copilot 状态映射到结构化输出策略,如同一节中可展开的 src/middleware.py 示例所示。

langgraph.jsonhttp 键上挂载 app 遵循通常的 LangGraph 或 LangSmith 部署方式,因此单个进程同时向 CopilotKit 客户端提供图和同一个 FastAPI 应用。

安装

对于后端端点:

bash
bun add @copilotkit/runtime hono
bash
uv add copilotkit ag-ui-langgraph fastapi uvicorn

中间件包与 Deep Agents 技术栈并列。将它与你的对话模型包一起安装(此示例使用 OpenAI):

python
pip install -U deepagents copilotkit langchain-openai
python
uv add deepagents copilotkit langchain-openai

对于前端应用:

bash
bun add @copilotkit/react-core @copilotkit/react-ui @hashbrownai/core @hashbrownai/react

在 Deep Agent 中使用 CopilotKit

CopilotKitMiddleware 添加到传给 create_deep_agent 的 middleware 列表中。该中间件让 CopilotKit 能够路由前端工具调用并使聊天状态与你的图保持一致。将你配置的其他中间件保留在同一列表中。

编译后的图随后即可插入到识别 CopilotKit 或 AG-UI 的进程中(例如下面的 FastAPI 模式),或接入 CopilotKit 文档中的Deep Agents 与 CopilotKit等指南。

python
from deepagents import create_deep_agent
from copilotkit import CopilotKitMiddleware
from langgraph.checkpoint.memory import MemorySaver

def get_weather(location: str) -> str:
    """Return a simple weather string for a location."""
    return f"The weather in {location} is sunny."

agent = create_deep_agent(
    model="openai:gpt-5.5",
    tools=[get_weather],
    middleware=[CopilotKitMiddleware()],  # AG-UI、前端工具与上下文
    system_prompt="You are a helpful research assistant.",
    checkpointer=MemorySaver(),
)

用自定义端点扩展 LangGraph 部署

关键思想是 LangGraph 部署不只是提供图服务。它还可以加载一个 HTTP 应用,让你在部署本身旁边挂载额外的路由。

langgraph.json 中,将 http.app 指向你的自定义应用入口点:

json
{
  "graphs": {
    "copilotkit_shadify": "./src/agents/copilotkit-shadify.ts:agent"
  },
  "http": {
    "app": "./src/api/app.ts:app"
  }
}
json
{
  "dependencies": ["."],
  "graphs": {
    "copilotkit_shadify": "./main.py:agent"
  },
  "http": {
    "app": "./main.py:app"
  }
}

然后创建 Hono 应用并注册 CopilotKit 路由:

ts
import { Hono } from "hono";
import { registerCopilotKit } from "./copilotkit.js";

export const app = new Hono();

registerCopilotKit(app);

在 Python 中,创建一个 FastAPI 应用,并通过 CopilotKit 的 AG-UI 桥接暴露 LangGraph 智能体:

python
from typing import Any, TypedDict

from ag_ui_langgraph import add_langgraph_fastapi_endpoint
from copilotkit import CopilotKitMiddleware, CopilotKitState, LangGraphAGUIAgent
from fastapi import FastAPI
from langchain.agents import create_agent

from src.middleware import apply_structured_output_schema, normalize_context

class AgentState(CopilotKitState):
    pass

class AgentContext(TypedDict, total=False):
    output_schema: dict[str, Any]

agent = create_agent(
    model="openai:gpt-5.5",
    middleware=[
        normalize_context,
        CopilotKitMiddleware(),
        apply_structured_output_schema,
    ],
    context_schema=AgentContext,
    state_schema=AgentState,
    system_prompt=(
        "You are a helpful UI assistant. Build visual responses using the "
        "available components."
    ),
)

app = FastAPI()

add_langgraph_fastapi_endpoint(
    app=app,
    agent=LangGraphAGUIAgent(
        name="copilotkit_shadify",
        description="A UI assistant that returns structured component payloads.",
        graph=agent,
    ),
    path="/",
)

这个自定义应用是重要的扩展点:它挂载一个识别 CopilotKit 的运行时,而无需替换底层的 LangGraph 部署。

在该路由内部,创建一个 CopilotRuntime 并使用 LangGraphAgent 将其指回已部署的图:

ts
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 智能体还需要中间件,来读取转发的 output_schema 并将其转换为模型的结构化 responseFormat

ts
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_genai: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 运行时转发模式,而智能体将其转换为模型必须遵循的结构化输出契约。

在 Python 中,等效工作发生在中间件中:规范化 CopilotKit 上下文,并将来自 useAgentContext(...)output_schema 转发到模型的结构化输出配置中。

python
import json
from collections.abc import Mapping

from langchain.agents.middleware import before_agent, wrap_model_call
from langchain.agents.structured_output import ProviderStrategy

@wrap_model_call
async def apply_structured_output_schema(request, handler):
    schema = None
    runtime = getattr(request, "runtime", None)
    runtime_context = getattr(runtime, "context", None)

    if isinstance(runtime_context, Mapping):
        schema = runtime_context.get("output_schema")

    if schema is None and isinstance(getattr(request, "state", None), dict):
        copilot_context = request.state.get("copilotkit", {}).get("context")
        if isinstance(copilot_context, list):
            for item in copilot_context:
                if isinstance(item, dict) and item.get("description") == "output_schema":
                    schema = item.get("value")
                    break

    if isinstance(schema, str):
        try:
            schema = json.loads(schema)
        except json.JSONDecodeError:
            schema = None

    if isinstance(schema, dict):
        request = request.override(
            response_format=ProviderStrategy(schema=schema, strict=True),
        )

    return await handler(request)

@before_agent
def normalize_context(state, runtime):
    copilotkit_state = state.get("copilotkit", {})
    context = copilotkit_state.get("context")

    if isinstance(context, list):
        normalized = [
            item.model_dump() if hasattr(item, "model_dump") else item
            for item in context
        ]
        return {"copilotkit": {**copilotkit_state, "context": normalized}}

    return None

结果是关注点清晰分离:

  • LangGraph 仍然负责图的执行和持久化
  • CopilotKit 负责面向聊天的运行时契约
  • 你的自定义端点在同一个部署中将它们粘合在一起

当你使用 CopilotKit 运行时适配器时,将你的 CopilotKit runtimeUrl 指向 FastAPI(或其他)应用暴露的路由,而不是仅指向原始的图 REST 表面。 当使用 Node CopilotRuntime 时,请遵循 CopilotKit 文档中的 LangGraphHttpAgentLangGraphAgentPython 图和中间件仍然定义工具行为和智能体逻辑。

结构化前端应用

在前端,将你的应用包裹在 CopilotKit 中并指向自定义运行时 URL:

tsx
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 API
  • useAgentContext(...) 将 UI 模式发送给智能体,使模型知道它应该生成什么样的结构化输出格式

注册动态组件

组件注册表位于 useChatKit() 中。这是你定义允许智能体发出哪些组件集合的地方,例如卡片、行、列、图表、代码块和按钮。

tsx
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",
      }),
    ],
  });
}

这个注册表成为智能体与 UI 之间的契约。模型并不是在生成任意的 JSX。它生成的是结构化数据,必须针对你暴露的组件和属性进行校验。

将助手消息渲染为动态 UI

一旦助手响应到达,自定义消息渲染器就决定如何展示它。在此示例中:

  • 助手消息将针对 UI kit 模式解析为结构化 JSON
  • 有效的结构化输出被渲染为真正的 React 组件
  • 用户消息被渲染为普通的聊天气泡
tsx
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 (
      {kit.render(value)}
  );
});

export function CustomMessageRenderer({ message }: RenderMessageProps) {
  if (message.role === "assistant") {
    return <AssistantMessageRenderer message={message} />;
  }

  return (
      <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>
  );
}

这种渲染器模式正是让集成看起来浑然天成的关键:

  • CopilotKit 处理聊天状态和传输
  • 自定义渲染器决定助手负载如何变成 UI
  • Hashbrown 将校验通过的结构化数据转换为具体的 React 元素

资源

最佳实践

  • 保持自定义端点精简: 用它来让 CopilotKit 适配你的图部署,而不是复制图内部已有的业务逻辑
  • 显式发送模式: 每次页面挂载时,useAgentContext 都应描述 UI 契约
  • 注册受限的组件集合: 只暴露你确实希望模型使用的组件和属性
  • 将渲染视为解析步骤: 在渲染之前,先根据你的模式解析助手内容
  • 保持用户消息为纯文本: 只有助手消息需要结构化渲染器;用户消息可以保持为普通的聊天气泡