CopilotKit 自定义渲染 Agent 思考链:reasoningMessage槽与 AG-UIREASONING_MESSAGE事件实战解析
【免费下载链接】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 开源仓库中的
claude-sdk-python集成示例(showcase/integrations/claude-sdk-python/src/app/demos/reasoning-custom/)为核心素材,完整讲解:Python Agent 如何把 Claude 原生 Extended Thinking(thinking_delta通道)转译为 AG-UI 的REASONING_MESSAGE_*事件流,前端又如何通过 v2 组件树中的messageView.reasoningMessage槽,把思考链从默认的可折叠卡片"换皮"成自定义的琥珀色"Agent reasoning"气泡。读完本文,你将掌握"思考链从模型到 UI 的全链路打通"与"槽化自定义渲染"两套可复用方案。
1. 这个 Demo 在解决什么问题:把"思考过程"变成一等 UI 元素
在多数聊天式 AI 应用中,模型"一步一步推理"的过程要么被隐藏,要么混在正文里以<thinking>...</thinking>之类的伪标签出现。CopilotKit 的 v2 架构把推理(reasoning)提升为与user、assistant、tool平级的一等消息类型:只要后端以 AG-UI 协议发出REASONING_MESSAGE_*事件,前端就能用专属的渲染槽把它呈现出来。
reasoning-custom演示(其说明文档即本文主题文档 README.md)的目标非常聚焦:
- 后端是一个 Python Agent(
reasoning_agent),它优先把 Claude 的原生 Extended Thinking(thinking_delta)直接映射为REASONING_MESSAGE_*事件; - 前端通过覆盖
messageView.reasoningMessage槽,将思考链渲染为一张带琥珀色标签、斜体正文的 "Agent reasoning" 卡片; - 它与 reasoning-default 共享同一个后端,二者并排展示"默认渲染 vs 自定义渲染"的差异。
从展示层入口文件 page.tsx 的注释可以看出,Reasoning 在 v2 中是一等消息类型的判断依据位于 CopilotChatMessageView.tsx:该组件以message.role === "reasoning"分流,默认用CopilotChatReasoningMessage渲染,而reasoningMessage槽覆盖正是官方推荐的、稳定的自定义出口。
2. 全链路架构:一次思考链请求的完整旅程
整个演示涉及三层进程,全部可以在仓库中找到对应实现:
React 前端 (reasoning-custom/page.tsx) │ runtimeUrl="/api/copilotkit" agent="reasoning-custom" ▼ Next.js API 路由 [src/app/api/copilotkit/route.ts](https://link.gitcode.com/i/fb9fb5a6e3c505bc89a8dcd406dd44f5) │ CopilotRuntime + createCopilotRuntimeHandler,按 AG-UI 协议代理 ▼ Python Agent 服务(独立进程,默认端口 8000,AGENT_URL 可配) │ POST /reasoning → [agent_server.py](https://link.gitcode.com/i/ff70cfc9df1034f81d21058ca03b6ff9) ▼ Claude API(messages.stream,thinking={"type": "adaptive"})关键的路由映射在 route.ts 中:reasoning-custom与reasoning-default两个 agent 名都被映射到同一个后端路径/reasoning。也就是说,两个 Demo 的差异只在前端槽配置,后端完全一致——这是验证"渲染与数据解耦"的最直观实验环境。后端服务的实际端口来自注释说明(默认http://localhost:8000,可通过AGENT_URL环境变量覆盖)。
3. 后端实现:Claude 原生 Extended Thinking 到 AG-UI 事件
3.1 为什么优先走原生通道
核心实现位于 reasoning_agent.py。模块 docstring 明确了策略:Anthropic Python SDK 的messages.stream支持 Claude 的 Extended Thinking("thinking budget"),thinking_delta内容块与文本块分开流式传输,Agent 把这些内容块逐一映射到 AG-UI 的REASONING_MESSAGE_*事件。
关键在于流的配置(reasoning_agent.py):
async with client.messages.stream( model=reasoning_model, max_tokens=3072, system=system, messages=messages, thinking={"type": "adaptive"}, ) as stream:thinking={"type": "adaptive"}表示启用自适应 Extended Thinking,Claude 的逐步计划会以原生thinking内容块的形式到达;max_tokens=3072为总 token 预算(含思考与正文);- 模型名取自
ANTHROPIC_REASONING_MODEL,未设置时回退到ANTHROPIC_MODEL,最终默认值为claude-opus-4-8,并经normalize_claude_model()(见 claude_agent_sdk_adapter.py)做兼容归一化。
3.2 事件映射逻辑:thinking 块 → REASONING_MESSAGE_*
事件循环按 Anthropic SDK 的原始事件类型分派(reasoning_agent.py):
| Anthropic 原始事件 | 处理动作 |
|---|---|
RawContentBlockStartEvent且content_block.type == "thinking" | 为当前 thinking 块分配独立的消息 ID(f"msg-{run_id}-think-{id(block)}"),并重置started标志 |
RawContentBlockStopEvent/ParsedContentBlockStopEvent | 若块已开始,发送REASONING_MESSAGE_END,然后清空当前块状态 |
RawContentBlockDeltaEvent且delta.type == "thinking_delta" | 首次增量时先发REASONING_MESSAGE_START(role="reasoning"),随后每个非空增量都发REASONING_MESSAGE_CONTENT |
这段实现有两个值得注意的细节:
- 一轮对话可能包含多个
thinking内容块,每个块用id(block)生成独立消息 ID,保证每个块都有各自平衡的 START/CONTENT/END 生命周期,而不是复用已 END 的消息 ID; native_reasoning_id is not None充当"当前有 thinking 块正在流式传输"的哨兵,同时支撑了正常结束、提前结束与异常路径上的生命周期兜底(详见第 8 节)。
3.3 兜底路径:<reasoning>...</reasoning>标签解析状态机
对于不支持原生 Extended Thinking 的部署场景,Agent 保留了从文本流中解析<reasoning>...</reasoning>标签的状态机(reasoning_agent.py)。其核心是一个带缓冲的双态切换器:
- 非 reasoning 态:在缓冲区中查找
<reasoning>;未找到则把内容按文本增量发出(但会"暂留"尾部几个字符,防止开标签被流式切割);找到则把开标签之前的文本作为正文发出,切换进 reasoning 态; - reasoning 态:查找
</reasoning>;未找到则暂留尾部并持续发REASONING_MESSAGE_CONTENT;找到则发完剩余内容、发送REASONING_MESSAGE_END,再切回文本态。
与之配套的是两套系统提示词(reasoning_agent.py):
NATIVE_REASONING_SYSTEM_PROMPT(默认):由于原生通道始终开启,提示词刻意不要求模型输出<reasoning>标签——否则真实 Claude 会产生"原生 thinking 块 + 标签文本"双重推理块(源码注释称之为 double-bubble);REASONING_SYSTEM_PROMPT(兜底):仅在原生通道不可用时启用,重新要求模型先输出<reasoning>...</reasoning>再给最终答案,并为 Agent 提供了一段可读性约束与示例。
_native_thinking_enabled = True这一开关与两套提示词共同构成了"原生优先、标签兜底"的降级路径。需要说明的是,当前仓库代码中该开关是硬编码为开启的,标签解析器处于"休眠但可用"的状态。
4. 前端实现:用messageView.reasoningMessage槽接管思考链渲染
4.1 挂载代码
页面入口 page.tsx 的完整结构如下(这也是原文档对应的实战代码):
"use client"; import type { CopilotChatReasoningMessage } from "@copilotkit/react-core/v2"; import { CopilotKit, CopilotChat } from "@copilotkit/react-core/v2"; import { ReasoningBlock } from "./reasoning-block"; import { useReasoningCustomSuggestions } from "./suggestions"; const AGENT_ID = "reasoning-custom"; export default function ReasoningCustomDemo() { 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() { useReasoningCustomSuggestions(); return ( <CopilotChat agentId={AGENT_ID} className="h-full rounded-2xl" messageView={{ reasoningMessage: ReasoningBlock as unknown as typeof CopilotChatReasoningMessage, }} /> ); }要点拆解:
CopilotKit负责连接 runtime(/api/copilotkit)并声明默认 agent;CopilotChat的messageView对象承载各消息类型的槽位,其中reasoningMessage槽被替换为自定义组件ReasoningBlock;- 类型断言
as unknown as typeof CopilotChatReasoningMessage只用于满足组件签名对齐,运行时不产生额外开销。
4.2 自定义渲染组件:琥珀色 "Agent reasoning" 卡片
ReasoningBlock 接收槽系统注入的三个 props:
"use client"; import React from "react"; import type { ReasoningMessage, Message } from "@ag-ui/core"; export function ReasoningBlock({ message, messages, isRunning, }: { message: ReasoningMessage; messages?: Message[]; isRunning?: boolean; }) { const isLatest = messages?.[messages.length - 1]?.id === message.id; const isStreaming = !!(isRunning && isLatest); const hasContent = !!(message.content && message.content.length > 0); return ( <div >} 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)解析而来(CopilotChatMessageView.tsx):传组件类型则直接使用;传字符串则视为 className(合并到默认组件);传对象则视为默认组件的 props 覆盖。也就是说,槽自定义有三种等价写法,reasoning-custom用的是第一种。
5.2 槽类型与引用稳定性
槽的 TypeScript 形态定义在 slots.tsx:
export type SlotValue<C extends React.ComponentType<any>> = | C // 组件类型 | string // className 字符串 | Partial<React.ComponentProps<C>>; // props 对象而 CopilotChat.tsx 用useShallowStableRef稳定槽对象引用,避免父组件每次渲染产生新引用、导致消息列表不必要的重渲染——这是把messageView作为内联对象传入时框架为你做好的性能保障。
5.3 记忆化细节
MemoizedReasoningMessage的比较逻辑(CopilotChatMessageView.tsx)值得注意:它对比消息 ID、内容、是否"最新"、isRunning(仅当该消息是当前最新消息时)以及组件引用。其中"最新状态变化"(例如 reasoning 消息后面追加了正文消息导致isStreaming从 true 变 false)会强制重渲染,这保证了流式结束后卡片能立刻切换到最终态。
6. 默认渲染 vs 自定义渲染:两种槽形态的对照实验
reasoning-custom与 reasoning-default 是同一后端的两个前端形态:
- 默认形态(reasoning-default/page.tsx):
<CopilotChat>不传任何槽覆盖,思考链由内置组件CopilotChatReasoningMessage渲染; - 自定义形态:本文第 4 节描述的槽覆盖。
内置组件 CopilotChatReasoningMessage.tsx 的默认行为是:流式时显示 "Thinking…"(附带脉冲圆点),流式结束后自动折叠成可展开的 "Thought for X" 头部(X 为formatDuration计算出的耗时,如 "8 seconds" / "1m 5s"),内容区使用Streamdown渲染流式 Markdown。它内部还暴露了Header、Content、Toggle三个子槽与 children 渲染函数,意味着即使不整体替换组件,也可以只换头部或内容区。
两者的区别一句话概括:默认组件是"可折叠的计时思考气泡",自定义组件是"始终可见的琥珀色标注卡片"。选择哪种取决于产品诉求——是让思考链占据视觉焦点,还是把它压缩进可展开区域。
7. 一个容易踩的坑:为什么"给我一步步推理"不触发思考链
suggestions.ts 为聊天框注册了一条默认建议:
useConfigureSuggestions({ suggestions: [ { title: "Show reasoning", message: "Explain step by step why the sky appears blue during the day but red at sunset.", }, ], available: "always", });源码注释给出了选择这个问题的关键原因:像 "show your reasoning step by step" 这类元提示不会产生任何思考内容——模型会将其识别为"要求暴露思维链"的请求并拒绝,直接返回纯文本,导致 reasoning 槽永远无法点亮。只有真正需要多步推理的具体问题(如"为什么天空白天是蓝的、日落时是红的")才会可靠地触发思考通道。这与前端isStreaming/hasContent的判定结合后,意味着:触发不了思考链时,自定义槽永远显示 "…",问题往往不在渲染层,而在提示词层。
8. 事件生命周期保障:源码级的健壮性设计
后端在三个路径上保证了REASONING_MESSAGE_*生命周期闭合(否则前端会渲染一个永不结束的"思考中"气泡):
- 正常流结束:若 thinking 块已开始但
content_block_stop未到(流提前结束),仍补发REASONING_MESSAGE_END(reasoning_agent.py); - 缓冲区尾部冲刷:流结束后,残余缓冲区按当前状态分别发 reasoning 或 text 事件(reasoning_agent.py);
- 异常路径:
except Exception内先关闭未闭合的 thinking 块与内联 reasoning 块,再发错误文本气泡(reasoning_agent.py)。
内联标签路径还有一个幂等守卫reasoning_ended:流内关闭、缓冲冲刷与异常清理共用同一标志位,保证同一内联块不会重复发出REASONING_MESSAGE_END。前端侧的 deduplicateMessages 则进一步按消息 ID 去重,防止重复事件导致重复渲染。
9. 运行与验证:如何把这个 Demo 跑起来
9.1 后端
Python Agent 服务由 agent_server.py 提供,其中POST /reasoning端点把请求负载解析为 AG-UI 的RunAgentInput,并以text/event-stream流式返回run_reasoning_agent的事件序列(agent_server.py),响应头包含Cache-Control: no-cache与X-Accel-Buffering: no,以禁用中间层缓冲。
运行前需要确认:
- 依赖:
requirements.txt中包含anthropic与ag_ui(AG-UI 协议库); - 环境变量:
ANTHROPIC_API_KEY必填;ANTHROPIC_REASONING_MODEL/ANTHROPIC_MODEL可选(默认claude-opus-4-8);前端侧AGENT_URL指向后端地址(默认http://localhost:8000)。
9.2 前端
Next.js 路由 route.ts 用createCopilotRuntimeHandler+CopilotRuntime建立 AG-UI 代理,把reasoning-custom等 agent 名映射到后端路径。前端页面访问/demos/reasoning-custom即可看到琥珀色卡片。
9.3 测试验证
同目录下的端到端测试 tool-rendering-reasoning-chain.spec.ts(针对复用同一渲染模式的变体 Demo)用[data-testid="reasoning-block"]断言自定义槽确实挂载、且多轮对话中每轮都产生新的 reasoning 块。这从测试侧印证了"槽覆盖生效 = DOM 中出现reasoning-block测试标记"这一事实。
10. 延伸:同一机制的复用形态
- tool-rendering-reasoning-chain(demo 目录):把"自定义 reasoning 槽"与"顺序工具调用渲染"组合进同一个聊天界面,其中的 reasoning-block.tsx 与本文主题组件实现一致,证明该模式可平移到更复杂的场景;
- PARITY_NOTES.md:明确将
reasoning-custom、reasoning-default、tool-rendering-reasoning-chain归为一类——它们都依赖"把 Claude Extended Thinking 块流式转译为独立 AG-UI 消息部件"的能力,这也是reasoning_agent.py这类专用后端存在的意义。
小结
从主题文档出发,本文还原了一条完整链路:Claude 原生thinking_delta→ Python Agent 状态机 → AG-UIREASONING_MESSAGE_START/CONTENT/END→ runtime 代理 →CopilotChatMessageView按role === "reasoning"分流 →reasoningMessage槽 → 自定义琥珀色卡片。无论你是想复刻"可见思考链"的产品形态,还是想深入理解 CopilotKit v2 槽系统与 AG-UI 事件协议,reasoning-custom与reasoning-default这对"同后端、异前端"的 Demo 都是最直接的可运行参照。
关键文件索引:
- 主题文档:README.md
- 前端入口:page.tsx | 自定义渲染组件:reasoning-block.tsx
- 后端 Agent:reasoning_agent.py
- 后端端点:agent_server.py | Runtime 代理:route.ts
- 前端槽机制:CopilotChatMessageView.tsx | 默认渲染组件:CopilotChatReasoningMessage.tsx
【免费下载链接】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),仅供参考