news 2026/9/12 16:24:55

CopilotKit 默认推理渲染实战:内置 CopilotChatReasoningMessage 的 “Thought for X“ 可折叠卡片

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CopilotKit 默认推理渲染实战:内置 CopilotChatReasoningMessage 的 “Thought for X“ 可折叠卡片

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 asreasoning-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]

关键点逐条拆解:

  1. CopilotKit runtimeUrl="/api/copilotkit" agent={AGENT_ID}:运行时挂载在 Next.js 的/api/copilotkit单路由上,并通过agent属性绑定名为reasoning-default的 Agent。该 Agent 名在 route.ts 的 agentNames 列表 中被注册,同时被dedicatedAgentPaths重定向到后端/reasoning路径。

  2. <CopilotChat agentId={AGENT_ID} ...>CopilotChat预置聊天组件只传了agentId和样式类名,完全没有messageView属性,更没有reasoningMessage插槽。这意味着消息列表将使用全部默认插槽组件,其中reasoningMessage默认指向内置的CopilotChatReasoningMessage

  3. 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 onmessages.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.

即两条推理通道:

  1. 原生 extended-thinking 通道(默认启用):通过messages.stream(..., thinking={"type": "adaptive"})开启 Claude 的原生思考块,流式事件中的RawContentBlockStartEventblock.type == "thinking")与RawContentBlockDeltaEventdelta.type == "thinking_delta")被逐一转发为 AG-UI 的ReasoningMessageStartEvent/ReasoningMessageContentEvent/ReasoningMessageEndEvent

  2. <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/clientHttpAgent,具体见 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 主组件:标签与计时

主组件接收messageReasoningMessage类型)、messagesisRunning,其标签逻辑为:

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-rows1fr0fr之间过渡,配合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} />, ); }

ReasoningComponentresolveSlotComponent(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, }} />

自定义组件接收messageReasoningMessage)、messagesisRunning三个插槽入参,ReasoningBlock用它来判断当前是否正在流式、是否有内容,并据此显示 "Thinking…" / "Agent reasoning" / "…" 三种状态。这类插槽入参协议与内置组件完全一致,因此自定义组件可以无缝替换默认组件。

六、完整调用链路与本地运行方式

综合上述源码,reasoning-default的完整链路为:

  1. 用户在CopilotChat中输入或点击建议问题;
  2. 前端通过runtimeUrl="/api/copilotkit"把请求发到 Next.js 运行时,运行时按agent={AGENT_ID}查找名为reasoning-default的 Agent;
  3. route.ts 将该 Agent 映射到后端http://localhost:8000/reasoningAGENT_URL环境变量可覆盖),经HttpAgent以 AG-UI 协议代理;
  4. 后端 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事件流返回;
  5. 前端运行时把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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 16:24:52

2023年AI论文写作工具测评与使用指南

1. 论文写作工具市场现状分析2023年AI写作工具市场规模已达47亿美元&#xff0c;年增长率超过300%。作为从业多年的学术编辑&#xff0c;我见证了这个领域从简单的语法检查工具发展到如今能辅助完成80%论文写作流程的智能系统。专科生毕业论文写作存在几个典型痛点&#xff1a;…

作者头像 李华
网站建设 2026/9/12 16:24:44

Java Lambda封装Service调用的实践与优化

1. 为什么我们需要Lambda封装Service调用在传统的Spring开发中&#xff0c;Service层的依赖注入一直是个绕不开的话题。我们通常会看到这样的代码&#xff1a;Controller public class UserController {Autowiredprivate UserService userService;Autowiredprivate OrderServic…

作者头像 李华
网站建设 2026/9/12 16:24:28

ESP32蓝牙Beacon测距实战:从RSSI建模到工业落地

1. 项目概述&#xff1a;为什么在ESP32上做蓝牙Beacon测距不是“炫技”&#xff0c;而是真实场景的刚需你手头有一块ESP32开发板&#xff0c;刚用VSCodeESP-IDF配好环境&#xff0c;烧录了第一个LED闪烁例程&#xff0c;正打算往物联网方向深挖——这时候&#xff0c;“蓝牙Bea…

作者头像 李华
网站建设 2026/9/12 16:24:23

RISC-V AIA中断控制器迁移实践:从PLIC到APLIC与IMSIC

1. PLIC的“够用”与“不够用”&#xff1a;迁移不是赶时髦1.1 PLIC到底做了什么&#xff1a;一张表看懂传统中断链路先把PLIC的家底理清楚。RISC-V规范里的PLIC&#xff08;Platform-Level Interrupt Controller&#xff09;承担的是“平台外部中断汇聚”的职责&#xff1a;UA…

作者头像 李华