CopilotKit Headless Chat 最小实战指南:基于 LlamaIndex 的 useAgent + useCopilotKit 双 Hook 手写聊天界面
【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit
本指南聚焦 CopilotKit 开源仓库中 LlamaIndex 集成示例的 Headless Chat(Simple)场景,讲解如何在不使用<CopilotChat />预构建组件的前提下,仅凭useAgent与useCopilotKit两个核心 Hook 加一套 shadcn/ui 外壳,搭出一个可运行、可测试的最小聊天界面。读完本文你将掌握 Headless 模式的核心数据流、agent.addMessage+runAgent的调用链、文本气泡与输入框的自实现方案,以及这套界面如何与 LlamaIndex 后端 Agent 通过 AG-UI 协议联通。
一、Headless 模式是什么:Bring Your Own UI
在 CopilotKit 的体系里,聊天界面有两种构建方式:
- Pre-Built(预构建):直接使用
<CopilotChat />、<CopilotSidebar />、<CopilotPopup />等现成组件,由框架接管消息列表、输入框、气泡样式等全部 UI 细节。仓库中 prebuilt-sidebar 和 prebuilt-popup 是这类方式的代表。 - Headless(无头):CopilotKit 只提供状态与行为(Hook),界面的每一像素都由你手写。Headless 的价值在于可以把 Agent 能力无缝嵌入你已有的设计系统、品牌风格或特殊布局,而不必受限于框架自带的 UI。
本文要讲的 headless-simple 就是这个方向的最小实现:源码注释明确写道「Headless = bring-your-own-UI. Simple = the smallest possible chat using the two core hooks (useAgent+useCopilotKit), styled with shadcn/ui」。它刻意不做工具渲染(tool rendering)、不做生成式 UI(generative UI),只有纯文本进、纯文本出,作为开发者复制粘贴起步的规范样本。
与之对应,仓库中还提供了功能更完整的 headless-complete,包含附件上传、工具卡片渲染、流式打字指示等完整实现。两者对照阅读,可以清晰看到从「最小可用」到「完整可用」的演进路径。
二、QA 清单解析:这个 Demo 要验证什么
本指南对应的 QA 文档 headless-simple.md 定义了三条验收标准,它们是理解该 Demo 定位的钥匙:
- Navigate to
/demos/headless-simple——导航到该演示路由,确认页面可访问; - Verify the hand-rolled chat UI is visible (no CopilotChat)——验证页面上渲染的是手写聊天 UI,而不是默认的
<CopilotChat />组件。这是 Headless 模式的「结构性信号」; - Send "Show a card about cats" and verify the
show_cardtool renders a titled card——发送示例指令,验证 Agent 通过show_card前端工具在界面上渲染出一张带标题的卡片。
第三条中的show_card是 LlamaIndex 侧 Agent 暴露的前端工具(frontend tool),其定义位于 agent.py:
def show_card( title: Annotated[str, "Short heading for the card."], body: Annotated[str, "Body text for the card."], ) -> str: """Display a titled card with a short body of text. Rendered on the frontend via useComponent.""" return f"Displayed card: {title}"从源码结构看,这个工具本身不执行任何后端逻辑,只返回一行确认字符串;真正的「渲染」发生在前端——Agent 发出调用意图后,由前端注册的组件(useComponent)把title和body渲染成卡片。这是理解 CopilotKit 前端工具模型的关键:工具声明在 Agent 侧,执行渲染在浏览器侧。
三、页面装配:一个 Provider 加一个自定义组件
整个页面的装配非常精简,page.tsx 只有十几行:
"use client"; import { CopilotKit } from "@copilotkit/react-core/v2"; import { Chat } from "./chat"; export default function HeadlessSimpleDemo() { return ( <CopilotKit runtimeUrl="/api/copilotkit" agent="headless-simple"> <Chat /> </CopilotKit> ); }三个关键点:
<CopilotKit>Provider:来自@copilotkit/react-core/v2,是所有 Hook 的上下文来源。它接收两个属性——runtimeUrl指向 Next.js 的 API 路由/api/copilotkit,agent指定要绑定的 Agent 名称(headless-simple)。"use client"指令:所有使用 Hook 的组件都必须在客户端渲染,这与 Next.js App Router 的约定一致。<Chat />自定义组件:聊天界面的一切都由这个手写组件承载。
Agent 名称headless-simple与后端的路由注册对应。在 route.ts 中,headless-simple(以及下划线别名headless_simple)被注册进共享 Agent 列表,所有共享 Agent 都指向同一个 LlamaIndex 后端:
const sharedAgentNames = [ // ... "headless_simple", "headless_complete", // Hyphenated aliases matching what the demo pages actually request "headless-simple", "headless-complete", // ... ];而该 API 路由内部通过CopilotRuntime+HttpAgent把请求代理到独立的 LlamaIndex Agent 服务(默认http://localhost:8000),双方通过AG-UI 协议通信:
const AGENT_URL = process.env.AGENT_URL || "http://localhost:8000"; function createAgent(subpath: string = "") { return new HttpAgent({ url: `${AGENT_URL}${subpath}/run` }); }在 LlamaIndex 侧,get_ag_ui_workflow_router()自动把工作流包装成 AG-UI 兼容的 FastAPI 路由(见 agent.py 的模块注释),并由 agent_server.py 的app.include_router(agent_router)挂载。也就是说:前端 Hook → CopilotKit Runtime → AG-UI 协议 → LlamaIndex 工作流,这就是整条数据链路。
四、核心逻辑:两个 Hook 撑起整个聊天
Headless-simple 的灵魂在 chat.tsx。整个聊天逻辑的核心区域(源码中以@region[use-agent-simple]标注)只有约 20 行:
const { agent } = useAgent({ agentId: "headless-simple" }); const { copilotkit } = useCopilotKit(); const [input, setInput] = useState(""); const send = (text: string) => { const trimmed = text.trim(); if (!trimmed || agent.isRunning) return; agent.addMessage({ id: crypto.randomUUID(), role: "user", content: trimmed, }); setInput(""); void copilotkit.runAgent({ agent }).catch((err) => { console.error("[langgraph-python:headless-simple] runAgent failed", err); }); };拆解这段代码,就是 Headless 模式的标准三步曲:
useAgent({ agentId }):拿到目标 Agent 的句柄。它暴露了messages(消息日志)、addMessage()(追加消息)、isRunning(运行状态)等核心能力。注意这里的agentId要与 Provider 上的agent属性对应。agent.addMessage():先把用户输入以role: "user"追加进本地消息列表。crypto.randomUUID()生成消息 ID,无需服务端参与。copilotkit.runAgent({ agent }):真正触发一次 Agent 运行。它返回 Promise,Demo 里用void丢弃返回值,只保留.catch做错误日志——源码注释特别强调,静默吞掉错误会示范不良实践,因此这里把网络失败、运行时错误、传输断开等问题打印到控制台,方便开发者排查。
if (!trimmed || agent.isRunning) return;这行防御逻辑同时处理了两件事:过滤空白输入、防止在 Agent 运行期间重复发送。
消息渲染:只显示用户与助手的纯文本
运行结束后,Agent 的消息会流式写入agent.messages。Simple 版刻意做了最小化过滤,只渲染纯文本的用户/助手消息:
const visible = agent.messages.flatMap((m) => { if (m.role !== "user" && m.role !== "assistant") return []; if (typeof m.content !== "string" || m.content.length === 0) return []; return [{ id: m.id, role: m.role, content: m.content }]; });这个过滤有两个意图:跳过工具调用(tool call)、系统消息等非文本消息类型;同时跳过内容为空的条目。之后遍历visible数组,分别渲染UserBubble和AssistantBubble(定义见 message-bubble.tsx)。每个气泡都带data-testid(headless-message-user/headless-message-assistant)和data-message-role属性,这些是下方测试章节会用到的重要锚点。
打字指示器:由运行状态驱动
const last = visible[visible.length - 1]; const showTyping = agent.isRunning && (!last || last.role === "user");当agent.isRunning为真、且最后一条可见消息来自用户(或还没有消息)时,显示 TypingIndicator——三个带动画延迟的跳动圆点,模拟「Agent 正在思考」。
空状态:首屏引导的三个示例提示
首次加载、还没有消息时,页面渲染 EmptyState,其中定义了三句示例提示词:
const SAMPLES = [ "Say hello in one short sentence.", "Tell me a one-line joke.", "Give me a fun fact.", ];点击任意一个 Badge 按钮,会直接调用send()填入该提示词。源码中对空状态的布局处理还有一个值得注意的细节:它被渲染在ScrollArea(Radix)之外,因为 Radix ScrollArea 内部会包一层display: table的容器,破坏h-full的高度传播,导致子元素无法垂直居中——这个注释(见 chat.tsx)对任何使用 Radix ScrollArea 做居中的开发者都有借鉴价值。
输入框:Enter 发送,Shift+Enter 换行
composer.tsx 实现底部输入区:单行 Textarea,回车发送、Shift+回车插入换行,发送按钮在输入为空或 Agent 运行时禁用。其data-testid="headless-composer"同样是测试锚点。
五、后端支撑:LlamaIndex 工作流如何提供 Agent
虽然 Simple 版前端极简,但后端依然由完整的 LlamaIndex Agent 支撑。在 agent.py 中,工作流通过FixedAGUIChatWorkflow装配:
async def _agent_workflow_factory(): wf = FixedAGUIChatWorkflow( llm=OpenAI(model="gpt-4.1", **_openai_kwargs), frontend_tools=[ change_background, generate_haiku, generate_task_steps, book_call, show_card, get_weather, ], backend_tools=[ query_data, manage_sales_todos, get_sales_todos_tool, schedule_meeting, search_flights, generate_a2ui, ], system_prompt=_AGENT_SYSTEM_PROMPT, initial_state={"todos": []}, ) wf.render_only_tool_names = {"get_weather"} return wf要点解读:
frontend_tools:声明前端工具,show_card就在其中——QA 文档要求验证的正是它。这类工具的执行发生在浏览器端(由前端useComponent渲染结果),Agent 只负责在合适时机发起调用。backend_tools:服务端执行的工具,如query_data(查询金融数据)、search_flights(搜索航班)等。system_prompt:系统提示词中明确包含「- Show titled cards with a body of text (via show_card frontend tool)」,告诉模型何时调用该工具。render_only_tool_names:标记仅渲染不阻塞的工具(get_weather),使其渲染状态能正确过渡到「完成」。
这里的FixedAGUIChatWorkflow是从 hitl_in_chat_agent.py 导入的,模块注释说明它修复了上游库的三个 bug(重复的工具调用渲染、缺失的parent_message_id、错误的工具结果消息角色),属于仓库内部的工程化处理,可作为理解项目深度的一个注脚。
启动这套前后端的最直接方式是仓库package.json中定义的dev脚本:
next dev --turbopack # 以及并行运行: PYTHONPATH=. python -m uvicorn agent_server:app --host 0.0.0.0 --port 8000 --reload即 Next.js 前端 + 8000 端口的 LlamaIndex Agent 服务,详见 package.json 中的dev命令。OPENAI_API_KEY需在环境中配置(路由的健康检查会显示其是否已设置)。
六、测试验证:E2E 如何守护「Headless」语义
QA 清单的验收可以由 Playwright 测试自动覆盖,测试文件是 headless-simple.spec.ts。该测试描述了明确的守护语义:
- 自定义输入框是「Headless」的结构性信号:如果页面退化回默认的
<CopilotChat />,headless-composer这个 testid 就会消失,测试随即失败; - 自定义气泡是另一道防线:
headless-message-assistanttestid 的缺失同样意味着回归到了预构建 UI; - 确定性夹具:测试使用
aimock确定性响应夹具,为三个示例提示词分别固定了回答内容(如问候语"Hi! In one short sentence: I'm a CopilotKit demo agent"、笑话、冷知识),如果夹具匹配路由出错、导致某个提示词拿到了别的回复,断言会以清晰的 diff 失败。
测试结构为 4 个用例:
- 页面加载后自定义输入框与三个示例提示按钮可见;
- 点击 "Say hello in one short sentence." 后,确定性的问候语出现在自定义助手气泡中;
- 点击 "Tell me a one-line joke." 后,确定性笑话出现在助手气泡中;
- 点击 "Give me a fun fact." 后,确定性冷知识出现在助手气泡中。
每个断言都设置了 30 秒超时(ASSERT_TIMEOUT),以容纳 Agent 运行时长。这套测试同时守护了「Headless 界面存在」与「消息链路工作正常」两层语义,是理解该 Demo 验收标准的权威参考。
七、延伸:从 Simple 到 Complete 的演进路径
如果你需要工具渲染、附件上传、建议栏、流式打字效果等更完整的能力,仓库中的 headless-complete 是下一站。它把聊天拆分为chat.tsx、composer.tsx、message-list.tsx、message-assistant.tsx等模块,并新增hooks/use-tool-renderers.tsx、hooks/use-frontend-components.ts、hooks/use-headless-suggestions.ts等 Hook,展示如何在纯手写界面上逐步叠加高级特性。对比两个 Demo 的目录结构(headless-simple 5 个文件 vs headless-complete 十余个模块),可以直观感受「最小可用」到「生产级」的复杂度跃迁。
八、小结
Headless Simple 用最少的前端代码演示了 CopilotKit 的完整心智模型:<CopilotKit>Provider 提供运行时,useAgent管理单个 Agent 的消息与状态,useCopilotKit负责触发执行;UI 全部自绘,测试锚点由自定义data-testid提供,后端通过 CopilotKit Runtime 以 AG-UI 协议连接 LlamaIndex 工作流。对于需要在现有设计系统中嵌入 Agent 能力的开发者,这个 Demo 就是最合适的起点模板。
【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考