CopilotKit 默认推理渲染实战:内置 CopilotChatReasoningMessage 的 "Thought for X" 可折叠卡片
【免费下载链接】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
本指南以 Claude SDK Python 集成示例中的reasoning-default演示为切入点,讲解在 CopilotKit v2 中零配置渲染 Agent 思考链(reasoning chain)的完整方案:如何让后端通过 AG-UI 协议以REASONING_MESSAGE_*事件流式推送推理内容,如何让前端内置的CopilotChatReasoningMessage组件把它呈现为带 "Thinking… / Thought for X" 头部、可展开折叠的卡片,以及它与自定义reasoningMessage插槽渲染方案的对比与取舍。读完本文,你将掌握在 reasoning-default 演示目录 对应的场景下,零插槽覆盖启用思考链展示的能力,并能依据 内置组件源码 理解其底层实现。
一、演示背景:同一后端,两种前端渲染
reasoning-default演示的全部代码都在 showcase/integrations/claude-sdk-python/src/app/demos/reasoning-default/ 下,其 README 点明了它的设计定位:
Same backend as
reasoning-custom, but the page passes NO customreasoningMessageslot — CopilotKit's built-inCopilotChatReasoningMessagerenders the reasoning as a collapsible "Thought for X" card.
翻译过来即:它与reasoning-custom(自定义渲染演示)共享同一个后端reasoning_agent图,唯一的区别在于前端是否覆盖reasoningMessage插槽。这个"同后端、双前端"的结构是理解 CopilotKit 推理渲染体系的最佳入口:
reasoning-default:不传任何自定义插槽,由内置组件CopilotChatReasoningMessage负责渲染,产出可折叠的 "Thought for X" 卡片;reasoning-custom:通过messageView.reasoningMessage插槽传入自定义的ReasoningBlock组件,把思考链重绘为琥珀色标签的 "Agent reasoning" 卡片,具体见 reasoning-custom 目录。
两个演示共用同一个后端端点/reasoning,这在 route.ts 的路由映射 中写得很明确:
const dedicatedAgentPaths: Record<string, string> = { // ... // Reasoning demos share a single backend that emits AG-UI // REASONING_MESSAGE_* events (parsed out of <reasoning>...</reasoning> // blocks the model emits). The two demo cells differ only on the // frontend slot configuration. "reasoning-default": "/reasoning", "reasoning-custom": "/reasoning", };也就是说,前端渲染方式的选择完全不影响后端协议:无论你最终用内置卡片还是自定义卡片,后端都以同一套 AG-UI 推理事件向客户端推送数据。这正是 CopilotKit v2 将"消息类型"与"渲染实现"解耦的体现。
二、零配置渲染:默认插槽的完整页面代码
reasoning-default的页面代码极其精简,全部核心逻辑只有两个文件:
2.1 页面入口 page.tsx
完整代码如下(来自 page.tsx):
"use client"; // Reasoning — Default // // Pairs with `reasoning-custom` (the Custom variant) so users can // compare default vs custom reasoning rendering side by side. This cell // renders <CopilotChat> with NO slot override — reasoning messages are // rendered by the built-in `CopilotChatReasoningMessage` component // (Thinking… / Thought for X header with an expandable content region). // // Both demos share the same backend (`reasoning_agent` graph) and the // same runtime URL (/api/copilotkit). The only difference is whether the // `messageView.reasoningMessage` slot is overridden. import { CopilotKit, CopilotChat } from "@copilotkit/react-core/v2"; import { useReasoningDefaultSuggestions } from "./suggestions"; // @region[default-reasoning-zero-config] const AGENT_ID = "reasoning-default"; export default function ReasoningDefaultDemo() { return ( <CopilotKit runtimeUrl="/api/copilotkit" agent={AGENT_ID}> <div className="flex justify-center items-center h-screen w-full"> <div className="h-full w-full max-w-4xl"> <Chat /> </div> </div> </CopilotKit> ); } function Chat() { useReasoningDefaultSuggestions(); return <CopilotChat agentId={AGENT_ID} className="h-full rounded-2xl" />; } // @endregion[default-reasoning-zero-config]关键点逐条拆解:
CopilotKit runtimeUrl="/api/copilotkit" agent={AGENT_ID}:运行时挂载在 Next.js 的/api/copilotkit单路由上,并通过agent属性绑定名为reasoning-default的 Agent。该 Agent 名在 route.ts 的 agentNames 列表 中被注册,同时被dedicatedAgentPaths重定向到后端/reasoning路径。<CopilotChat agentId={AGENT_ID} ...>:CopilotChat预置聊天组件只传了agentId和样式类名,完全没有messageView属性,更没有reasoningMessage插槽。这意味着消息列表将使用全部默认插槽组件,其中reasoningMessage默认指向内置的CopilotChatReasoningMessage。className="h-full rounded-2xl":仅做布局与圆角样式定制,与推理渲染逻辑无关。
2.2 建议提示 suggestions.ts
建议提示文件 用useConfigureSuggestions注册了一个推理诱发问题:
"use client"; import { useConfigureSuggestions } from "@copilotkit/react-core/v2"; // Suggestions registered via the v2 chat composer hook. The prompt is a // concrete reasoning-eliciting question — gpt-5-mini (and other OpenAI // reasoning models) only emit `response.reasoning_summary_text.delta` // events when there's a real problem to think about. Meta-prompts like // "show your reasoning" produce no reasoning summary, so the reasoning // slot would never light up. export function useReasoningDefaultSuggestions() { useConfigureSuggestions({ suggestions: [ { title: "Show reasoning", message: "Explain step by step why the sky appears blue during the day but red at sunset.", }, ], available: "always", }); }这里藏着一个实战要点:并非随便什么提问都会触发思考链。只有当用户提出一个"真正需要思考"的具体问题(例如"为什么天空白天是蓝色、日落时是红色"这种需要分步推导的问题)时,带推理能力的模型才会产生真实的 reasoning 输出;而类似"show your reasoning"这种元提示(meta-prompt)往往不会触发推理流,导致推理插槽永远不亮。因此,演示特意选用了一个具体的、可诱发分步推理的问题作为默认建议。
三、后端数据来源:AG-UI 的 REASONING_MESSAGE_* 事件
前端能渲染思考链,前提是后端把推理内容以 AG-UI 协议事件推送过来。与reasoning-custom共享的reasoning_agent图在 reasoning_agent.py 中实现,其 docstring 说明了核心思路:
The Anthropic Python SDK supports Claude's extended-thinking ("thinking budget") parameter on
messages.stream, which streamsthinking_deltacontent blocks separately from text. We map those onto AG-UI'sREASONING_MESSAGE_*events. Models without extended-thinking fall back to an inline<reasoning>...</reasoning>system-prompt convention that this agent parses out of the text stream.
即两条推理通道:
原生 extended-thinking 通道(默认启用):通过
messages.stream(..., thinking={"type": "adaptive"})开启 Claude 的原生思考块,流式事件中的RawContentBlockStartEvent(block.type == "thinking")与RawContentBlockDeltaEvent(delta.type == "thinking_delta")被逐一转发为 AG-UI 的ReasoningMessageStartEvent/ReasoningMessageContentEvent/ReasoningMessageEndEvent。<reasoning>...</reasoning>内联标签回退通道:当无法启用原生思考时,系统提示词指示模型先在正文中输出<reasoning>...</reasoning>标签包裹的思考过程,再由 Agent 中的状态机把标签内的内容切分并映射为同样的REASONING_MESSAGE_*事件。代码中对应REASONING_SYSTEM_PROMPT与一套以REASONING_OPEN/REASONING_CLOSE为界、带缓冲区的流式解析状态机。
值得注意的工程细节是:原生通道启用时,系统提示词刻意不要求模型输出<reasoning>标签(NATIVE_REASONING_SYSTEM_PROMPT明确写了 "Do not wrap your answer in any XML or markup tags")。原因在源码注释中说明得很清楚:如果同时启用原生思考与标签指令,真实 Claude 会产出两份推理内容(原生 thinking 块 + 标签文本),造成 "double-bubble" 重复显示。
此外,reasoning_agent.py 还对消息生命周期做了健壮性兜底:无论流正常结束、中途截断还是抛出异常,只要存在已开始但未结束的推理块,都会补发ReasoningMessageEndEvent,避免前端渲染出"永远 Thinking"的悬空气泡。
该后端通过 agent_server.py 中的/reasoning端点 暴露为 FastAPI 流式接口,并经由createClaudeHttpAgent包装为@ag-ui/client的HttpAgent,具体见 claude-http-agent.ts。前端 Next.js 运行时通过CopilotRuntime+createCopilotRuntimeHandler在单路由模式下把这个 Agent 代理给浏览器。
四、内置组件源码解析:CopilotChatReasoningMessage 的 "Thought for X" 卡片
reasoning-default的核心看点,就是内置组件 CopilotChatReasoningMessage.tsx 如何把ReasoningMessage渲染成可折叠卡片。该组件实现了头部(Header)、内容区(Content)与展开切换(Toggle)三个可独立覆写的子插槽,默认组合起来就是 README 所述的 collapsible "Thought for X" card。
4.1 主组件:标签与计时
主组件接收message(ReasoningMessage类型)、messages与isRunning,其标签逻辑为:
const isLatest = messages?.[messages.length - 1]?.id === message.id; const isStreaming = !!(isRunning && isLatest); const hasContent = !!(message.content && message.content.length > 0); const label = isStreaming ? "Thinking…" : `Thought for ${formatDuration(elapsed)}`;- 流式进行中(
isStreaming为 true):头部显示"Thinking…",并伴随一个脉冲小圆点(loading 指示); - 流式结束:头部切换为"Thought for X",其中 X 是
formatDuration(elapsed)生成的耗时描述。formatDuration对秒数做人性化处理:小于 1 秒显示 "a few seconds",小于 60 秒显示 "N seconds",超过 60 秒显示 "Xm Ys"。
耗时通过startTimeRef+setInterval每秒 tick 一次计算,仅在流式期间计时,流结束时会取一个最终快照。
4.2 展开 / 折叠行为
展开状态的核心逻辑是"流式中默认展开,流结束后自动折叠,但尊重用户手动操作":
const [isOpen, setIsOpen] = useState(isStreaming); const userToggledRef = useRef(false); useEffect(() => { if (isStreaming) { userToggledRef.current = false; setIsOpen(true); } else if (!userToggledRef.current) { setIsOpen(false); } }, [isStreaming]);- 新的流式会话开始时重置
userToggledRef并强制展开,让用户实时看到思考过程; - 流结束、且用户未曾手动点击过时自动折叠,保持对话界面整洁;
- 若用户手动展开/折叠过(
userToggledRef.current = true),则自动折叠逻辑不再覆盖用户的显式意图——源码注释提到这个设计还避免了 CI 上异步forceUpdate时序与点击处理器竞争导致的测试抖动。
折叠动画由Toggle子插槽完成:使用grid-template-rows在1fr与0fr之间过渡,配合overflow-hidden实现平滑的高度动画。
4.3 内容渲染:Streamdown 流式 Markdown
内容区(Content子插槽)在无内容且非流式时不渲染任何 DOM;有内容时通过Streamdown组件渲染推理文本(推理内容通常是 Markdown 格式的思考链),流式期间还会在末尾附加一个脉冲光标动画:
<div className="cpk:text-sm cpk:text-muted-foreground"> <Streamdown> {typeof contentChildren === "string" ? contentChildren : ""} </Streamdown> {isStreaming && hasContent && ( <span className="cpk:inline-flex cpk:items-center cpk:ml-1 cpk:align-middle"> <span className="cpk:w-2 cpk:h-2 cpk:rounded-full cpk:bg-muted-foreground cpk:animate-pulse-cursor" /> </span> )} </div>头部的ChevronRight图标会在可展开(有内容)时渲染,并随展开状态旋转 90 度,配合aria-expanded保证无障碍可访问性。
4.4 为什么是"默认":消息分发的实现依据
CopilotChatReasoningMessage之所以能成为默认渲染,根因在消息视图组件 CopilotChatMessageView.tsx 中:v2 将reasoning视为一等公民消息类型,在renderMessageBlock中按message.role分发:
} else if (message.role === "reasoning") { elements.push( <MemoizedReasoningMessage key={message.id} message={message as ReasoningMessage} messages={messages} isRunning={isRunning} ReasoningMessageComponent={ReasoningComponent} slotProps={reasoningSlotProps} />, ); }ReasoningComponent由resolveSlotComponent(reasoningMessage, CopilotChatReasoningMessage)解析:当reasoningMessage插槽未提供任何值时,默认回落到CopilotChatReasoningMessage。这正是reasoning-default页面"什么都不传"也能渲染思考链的原因。若传了组件则替换之(reasoning-custom的做法),传字符串则视为 className 应用于默认组件,传对象则视为默认组件的部分 props——三种插槽形态都在resolveSlotComponent中统一处理。
此外,MemoizedReasoningMessage做了精细化 memo:仅在消息 id、内容、最新状态(isStreaming 切换)、组件引用或 slot props 变化时重渲染,避免无关消息更新引起整个推理卡片重绘;同时CopilotChatMessageView还会在最后一条消息是reasoning时隐藏聊天气泡级 loading 光标(showCursor逻辑),因为推理卡片本身已带自己的 loading 指示,避免双重闪烁。
五、默认 vs 自定义:如何选择渲染方案
对照 reasoning-custom 的自定义渲染,两种方案的本质差异是:
| 维度 | reasoning-default(内置默认) | reasoning-custom(自定义插槽) |
|---|---|---|
是否覆盖messageView.reasoningMessage | 否 | 是,传入ReasoningBlock |
| 头部样式 | "Thinking… / Thought for X"(含流式计时) | "Thinking… / Agent reasoning"(琥珀色标签) |
| 交互 | 默认展开、流毕自动折叠,可手动切换 | 常驻内联展示思考链,不折叠 |
| 渲染组件 | CopilotChatReasoningMessage(含 Header/Content/Toggle 三个可再细分插槽) | 自绘ReasoningBlock(源码见此处) |
| 适用场景 | 开箱即用、想省事地获得规范可折叠 UI | 需要品牌化、强视觉强调(如琥珀色标签)或定制交互 |
自定义方案在reasoning-custom/page.tsx中通过以下方式接入:
<CopilotChat agentId={AGENT_ID} className="h-full rounded-2xl" messageView={{ reasoningMessage: ReasoningBlock as unknown as typeof CopilotChatReasoningMessage, }} />自定义组件接收message(ReasoningMessage)、messages与isRunning三个插槽入参,ReasoningBlock用它来判断当前是否正在流式、是否有内容,并据此显示 "Thinking…" / "Agent reasoning" / "…" 三种状态。这类插槽入参协议与内置组件完全一致,因此自定义组件可以无缝替换默认组件。
六、完整调用链路与本地运行方式
综合上述源码,reasoning-default的完整链路为:
- 用户在
CopilotChat中输入或点击建议问题; - 前端通过
runtimeUrl="/api/copilotkit"把请求发到 Next.js 运行时,运行时按agent={AGENT_ID}查找名为reasoning-default的 Agent; - route.ts 将该 Agent 映射到后端
http://localhost:8000/reasoning(AGENT_URL环境变量可覆盖),经HttpAgent以 AG-UI 协议代理; - 后端 reasoning_agent.py 通过
ANTHROPIC_API_KEY调用 Claude(模型名取ANTHROPIC_REASONING_MODEL,缺省回落到ANTHROPIC_MODEL,最终经normalize_claude_model归一),开启 adaptive extended thinking,把thinking_delta映射为REASONING_MESSAGE_START / CONTENT / END事件流返回; - 前端运行时把
role === "reasoning"的消息交给默认的CopilotChatReasoningMessage,渲染出可折叠的 "Thought for X" 卡片。
运行该演示需要满足的前提(与仓库其他示例一致):
- 后端服务运行在 8000 端口,通过 agent_server.py 提供
/reasoning端点,/health探针供运行时健康检查; - 设置
ANTHROPIC_API_KEY(模型为 Claude 系列,需支持 extended-thinking 能力才能走原生思考通道); - 前端 Next.js 应用在
/api/copilotkit挂载 CopilotKit 运行时,确保AGENT_URL指向后端地址。
七、小结
reasoning-default演示用最短的代码量(一个<CopilotChat>、零插槽覆盖)验证了 CopilotKit v2 的推理渲染体系:AG-UI 协议让推理成为与文本、工具调用并列的一等消息类型,内置的CopilotChatReasoningMessage提供开箱即用的 "Thinking… / Thought for X" 可折叠卡片,而插槽机制则为需要深度定制的场景保留了reasoningMessage出口。参考实现与源码均可在本仓库中查阅:前端演示位于 reasoning-default 目录,内置组件见 CopilotChatReasoningMessage.tsx,消息分发逻辑见 CopilotChatMessageView.tsx,后端事件生成见 reasoning_agent.py。
【免费下载链接】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),仅供参考