CopilotKit 提供完整的 React 聊天运行时。当你希望代理返回结构化 UI payload,而不仅是纯文本时,它与 LangGraph 尤其搭配良好。在此模式中,LangGraph 部署会同时提供图 API 和自定义 CopilotKit 端点,而前端会将助手消息解析为动态 React 组件。 在服务器端,copilotkit 包提供 CopilotKitMiddleware,使 LangGraph 图、LangChain 代理或 Deep Agent 可以使用 Agent UI (AG-UI) 传输协议,将工具和消息事件流式传输到聊天 UI,并读取或写入共享的 CopilotKit 状态切片。它还提供 helper,用于在图前面挂载与 CopilotKit 兼容的 HTTP 端点。 当你需要以下能力时,此方法很有用:
  • 使用现成聊天运行时,而不是自己连接 stream.messages
  • 在已部署图旁边添加自定义服务器端点,以支持提供商特定行为
  • 从受约束组件注册表渲染结构化生成式 UI
CopilotKit for LangGraph 还基于相同中间件和客户端,记录了生成式 UI人在环路(HITL)和共享状态
如需 CopilotKit 专用 API、UI 模式和运行时配置,请参阅 CopilotKit 文档。如需 Deep Agent 演练,请参阅 CopilotKit 文档中的 Deep Agents and CopilotKit

工作方式

从高层看,CopilotKit 位于 React 应用和 LangGraph 部署之间。前端会将对话状态发送到与图 API 并列挂载的自定义 /api/copilotkit 路由,该路由将请求转发给 LangGraph,响应会带回助手消息以及组件注册表可渲染的任何结构化 UI payload。
  1. 照常部署图,可以使用 LangSmith 或 LangGraph 开发服务器。
  2. 用 HTTP app 扩展部署,在图 API 旁边挂载 CopilotKit 路由。
  3. CopilotKit 包裹前端,并将其指向该自定义运行时 URL。
  4. 注册动态 UI 组件,并在渲染时将助手响应解析为这些组件。

Python 服务器提供什么

copilotkit 及相关包会桥接 LangGraph 部署和 CopilotKit 客户端。
组件作用
CopilotKitMiddleware将 CopilotKit 和 AG-UI 状态与请求合并到代理中,包括前端工具调用和上下文。将它添加到 create_agentcreate_deep_agentmiddleware 列表中。
CopilotKitState(子类)自定义状态:扩展 CopilotKitState,让 CopilotKit key 成为图状态的一部分。
LangGraphAGUIAgent为运行时打包一个已编译图,以及名称和描述。
add_langgraph_fastapi_endpoint(来自 ag-ui-langgraph连接 FastAPI app,使 CopilotKit 可以在同一个 LangGraph 进程中运行你的图。当你在 langgraph.json 中添加自定义 http app,而不是使用单独 HTTP 服务器时使用它。
CopilotKitMiddleware 添加到 middleware 列表后,它既可用于 create_deep_agent,也可用于来自 create_agent 的图。对于带 CopilotKitState 和 FastAPI 桥接的 create_agent 图,请参考下面的 Python main.py 示例。结构化生成式 UI(例如来自客户端的 useAgentContextoutput_schema)需要额外中间件,将 Copilot 状态映射到结构化输出策略,如同一小节中可展开的 src/middleware.py 示例所示。 langgraph.jsonhttp 键上挂载 app 遵循常规 LangGraph 或 LangSmith 部署,因此同一进程可以同时向 CopilotKit 客户端提供图和同一个 FastAPI app。

安装

对于后端端点:
uv add copilotkit ag-ui-langgraph fastapi uvicorn
该中间件包与 Deep Agents 栈配合使用。将它与你的聊天模型包一起安装(此示例使用 OpenAI):
pip install -U deepagents copilotkit langchain-openai
对于前端应用:
bun add @copilotkit/react-core @copilotkit/react-ui @hashbrownai/core @hashbrownai/react

将 CopilotKit 与 Deep Agent 搭配使用

CopilotKitMiddleware 添加到传给 create_deep_agentmiddleware 列表中。该中间件让 CopilotKit 可以路由前端工具调用,并使聊天状态与你的图保持一致。将你配置的任何其他中间件保留在同一个列表中。 随后,已编译图就可以接入感知 CopilotKit 或 AG-UI 的进程(例如下面的 FastAPI 模式),或接入 CopilotKit 文档中的 Deep Agents and CopilotKit 指南。
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.4",
    tools=[get_weather],
    middleware=[CopilotKitMiddleware()],  # AG-UI, frontend tools, and context
    system_prompt="You are a helpful research assistant.",
    checkpointer=MemorySaver(),
)

使用自定义端点扩展 LangGraph 部署

关键思路是,LangGraph 部署不只提供图服务。它还可以加载 HTTP app,使你可以在部署本身旁边挂载额外路由。 langgraph.json 中,将 http.app 指向自定义 app 入口点:
{
  "dependencies": ["."],
  "graphs": {
    "copilotkit_shadify": "./main.py:agent"
  },
  "http": {
    "app": "./main.py:app"
  }
}
在 Python 中,创建 FastAPI app,并通过 CopilotKit 的 AG-UI 桥接公开 LangGraph 代理:
main.py
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.4",
    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="/",
)
这个自定义 app 是重要的扩展点:它会挂载感知 CopilotKit 的运行时,而不会替换底层 LangGraph 部署。 在 Python 中,等价工作发生在中间件中:规范化 CopilotKit 上下文,并将来自 useAgentContext(...)output_schema 转发到模型的结构化输出配置中。
src/middleware.py
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(或其他)app 暴露的路由,而不只是原始图 REST 接口。 请参考 CopilotKit 文档中 Node CopilotRuntimeLangGraphHttpAgentLangGraphAgentPython 图和中间件仍定义工具行为和代理逻辑。 :::

组织前端应用

在前端,用 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 API
  • useAgentContext(...) 会将 UI schema 发送给代理,让模型知道应生成什么结构化输出格式

注册动态组件

组件注册表位于 useChatKit() 中。你可以在这里定义代理允许发出的组件集合,例如卡片、行、列、图表、代码块和按钮。
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,而是在生成结构化数据,并且这些数据必须通过你暴露的组件和 props 校验。

将助手消息渲染为动态 UI

助手响应到达后,自定义消息渲染器会决定如何显示它。在此示例中:
  • 助手消息会根据 UI kit schema 解析为结构化 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 处理聊天状态和传输
  • 自定义渲染器决定助手 payload 如何变成 UI
  • Hashbrown 将已验证的结构化数据转换为具体 React 元素

资源

最佳实践

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