CopilotKit 自定义渲染 Agent 思考链:`reasoningMessage` 槽与 AG-UI `REASONING_MESSAGE` 事件实战解析
2026/9/12 5:24:25 网站建设 项目流程

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)提升为与userassistanttool平级的一等消息类型:只要后端以 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-customreasoning-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 原始事件处理动作
RawContentBlockStartEventcontent_block.type == "thinking"为当前 thinking 块分配独立的消息 ID(f"msg-{run_id}-think-{id(block)}"),并重置started标志
RawContentBlockStopEvent/ParsedContentBlockStopEvent若块已开始,发送REASONING_MESSAGE_END,然后清空当前块状态
RawContentBlockDeltaEventdelta.type == "thinking_delta"首次增量时先发REASONING_MESSAGE_STARTrole="reasoning"),随后每个非空增量都发REASONING_MESSAGE_CONTENT

这段实现有两个值得注意的细节:

  1. 一轮对话可能包含多个thinking内容块,每个块用id(block)生成独立消息 ID,保证每个块都有各自平衡的 START/CONTENT/END 生命周期,而不是复用已 END 的消息 ID;
  2. 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;
  • CopilotChatmessageView对象承载各消息类型的槽位,其中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} />, ); }

ReasoningComponentresolveSlotComponent(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。它内部还暴露了HeaderContentToggle三个子槽与 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_*生命周期闭合(否则前端会渲染一个永不结束的"思考中"气泡):

  1. 正常流结束:若 thinking 块已开始但content_block_stop未到(流提前结束),仍补发REASONING_MESSAGE_END(reasoning_agent.py);
  2. 缓冲区尾部冲刷:流结束后,残余缓冲区按当前状态分别发 reasoning 或 text 事件(reasoning_agent.py);
  3. 异常路径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-cacheX-Accel-Buffering: no,以禁用中间层缓冲。

运行前需要确认:

  • 依赖:requirements.txt中包含anthropicag_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-customreasoning-defaulttool-rendering-reasoning-chain归为一类——它们都依赖"把 Claude Extended Thinking 块流式转译为独立 AG-UI 消息部件"的能力,这也是reasoning_agent.py这类专用后端存在的意义。

小结

从主题文档出发,本文还原了一条完整链路:Claude 原生thinking_delta→ Python Agent 状态机 → AG-UIREASONING_MESSAGE_START/CONTENT/END→ runtime 代理 →CopilotChatMessageViewrole === "reasoning"分流 →reasoningMessage槽 → 自定义琥珀色卡片。无论你是想复刻"可见思考链"的产品形态,还是想深入理解 CopilotKit v2 槽系统与 AG-UI 事件协议,reasoning-customreasoning-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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询