外观
推理 token(Reasoning tokens)会暴露支持扩展思考(extended thinking)的高级模型(如 OpenAI 的 GPT-5 和 Anthropic 的 Claude)的内部思考过程。这些模型会生成结构化内容块,将推理与最终答案分开,让你可以构建展示模型如何得出其回答的界面(UI)。
import { PatternEmbed } from "/snippets/pattern-embed.jsx"
什么是推理 token?
当具有推理能力的模型处理提示词时,会生成两种不同类型的内容:
- 推理块:模型的内部思维链、问题拆解以及逐步分析
- 文本块:呈现给用户的最终、精炼的回复
这些内容以类型化的内容块形式在 AIMessage 中传递,可通过 contentBlocks 属性访问:
ts
// 推理块
{ type: "reasoning", reasoning: "Let me think about this step by step..." }
// 文本块
{ type: "text", text: "The answer is 42." }INFO
并非所有模型都会产生推理 token。此模式专门适用于支持扩展思考或思维链输出的模型。标准对话模型只返回文本块。
使用场景
- 透明性:向用户展示模型的推理过程,以建立对其答案的信任
- 调试:检查模型的思考过程,找出它在何处出错
- 教学工具:通过展示 AI 如何处理问题来教学生解决问题的思路
- 决策支持:让领域专家验证建议背后的推理
- 质量保证:在受监管行业中审计推理链以符合合规要求
提取推理块与文本块
AIMessage 上的 contentBlocks 数组按生成顺序包含所有内容块。通过 type 过滤,可将推理与文本分开:
ts
import { AIMessage } from "langchain";
function extractBlocks(msg: AIMessage) {
const reasoningBlocks = msg.contentBlocks
.filter((b) => b.type === "reasoning")
.map((b) => b.reasoning);
const textBlocks = msg.contentBlocks
.filter((b) => b.type === "text")
.map((b) => b.text);
return {
reasoning: reasoningBlocks.join(""),
text: textBlocks.join(""),
};
}单条消息可能包含多个推理块(例如,如果模型暂停推理、产生部分文本,然后又继续推理)。将它们拼接起来即可获得完整的思考过程。
从 useStream 访问消息
将 useStream 连接到你的支持推理的智能体,并在聊天界面中遍历 stream.messages。通过 HumanMessage.isInstance 和 AIMessage.isInstance 进行分支判断,然后将每条助手消息传递给读取 contentBlocks 并将推理与文本分开的组件。在 stream.isLoading 为 true 时,对最后一条消息设置 isStreaming,这样随着 token 陆续到达,思考块会实时更新。
INFO
The code examples use useStream<typeof myAgent> for type-safe stream state. See Type inference for Python or JavaScript backends.
tsx
import { useStream } from "@langchain/react";
import { AIMessage, HumanMessage } from "langchain";
function Chat() {
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "reasoning",
});
return (
{stream.messages.map((msg, i) => {
if (HumanMessage.isInstance(msg)) {
return <HumanBubble key={i} text={msg.text} />;
}
if (AIMessage.isInstance(msg)) {
return (
<AIResponse
key={i}
message={msg}
isStreaming={stream.isLoading && i === stream.messages.length - 1}
/>
);
}
return null;
})}
);
}vue
<script setup lang="ts">
import { useStream } from "@langchain/vue";
import { AIMessage, HumanMessage } from "langchain";
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "reasoning",
});
</script>
<template>
<template v-for="(msg, i) in stream.messages.value" :key="i">
<HumanBubble v-if="HumanMessage.isInstance(msg)" :text="msg.text" />
<AIResponse
v-else-if="AIMessage.isInstance(msg)"
:message="msg"
:isStreaming="stream.isLoading.value && i === stream.messages.value.length - 1"
/>
</template>
</template>svelte
<script lang="ts">
import { useStream } from "@langchain/svelte";
import { AIMessage, HumanMessage } from "langchain";
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "reasoning",
});
</script>
{#each stream.messages as msg, i}
{#if HumanMessage.isInstance(msg)}
<HumanBubble text={msg.text} />
{:else if AIMessage.isInstance(msg)}
<AIResponse
message={msg}
isStreaming={stream.isLoading && i === stream.messages.length - 1}
/>
{/if}
{/each}ts
import { Component } from "@angular/core";
import { injectStream } from "@langchain/angular";
import { AIMessage, HumanMessage } from "langchain";
@Component({
selector: "app-chat",
template: `
@for (msg of stream.messages(); track $index) {
@if (isHuman(msg)) {
<human-bubble [text]="msg.text" />
} @else if (isAI(msg)) {
<ai-response
[message]="msg"
[isStreaming]="stream.isLoading() && $index === stream.messages().length - 1"
/>
}
}
`,
})
export class ChatComponent {
stream = injectStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "reasoning",
});
isHuman = HumanMessage.isInstance;
isAI = AIMessage.isInstance;
}构建 ThinkingBubble 组件
ThinkingBubble 在视觉上独特、可折叠的容器中呈现推理 token。用户可以展开它以查看完整的思考过程,或折叠它以便专注于最终答案。
tsx
import { useState } from "react";
function ThinkingBubble({
reasoning,
isStreaming,
}: {
reasoning: string;
isStreaming: boolean;
}) {
const [isExpanded, setIsExpanded] = useState(false);
const charCount = reasoning.length;
const previewLength = 120;
const preview =
reasoning.length > previewLength
? reasoning.slice(0, previewLength) + "..."
: reasoning;
return (
<button
className="thinking-header"
onClick={() => setIsExpanded(!isExpanded)}
>
{isStreaming ? (
) : (
"💭"
)}
{isStreaming ? "Thinking..." : `Thought process (${charCount} chars)`}
▶
</button>
{isExpanded && (
<pre>{reasoning}</pre>
)}
{!isExpanded && !isStreaming && (
{preview}
)}
);
}渲染完整的 AI 回复
将 ThinkingBubble 和标准文本气泡组合到同一个 AIResponse 组件中:
tsx
function AIResponse({
message,
isStreaming,
}: {
message: AIMessage;
isStreaming: boolean;
}) {
const reasoningBlocks = message.contentBlocks
.filter((b) => b.type === "reasoning")
.map((b) => b.reasoning)
.join("");
const textBlocks = message.contentBlocks
.filter((b) => b.type === "text")
.map((b) => b.text)
.join("");
const hasReasoning = reasoningBlocks.length > 0;
const hasText = textBlocks.length > 0;
const isReasoningPhase = isStreaming && !hasText;
const isTextPhase = isStreaming && hasText;
return (
{hasReasoning && (
<ThinkingBubble
reasoning={reasoningBlocks}
isStreaming={isReasoningPhase}
/>
)}
{hasText && (
{textBlocks}
{isTextPhase && ▊}
)}
);
}处理边界情况
没有推理的消息
并非每条 AI 消息都会包含推理块。当 contentBlocks 只有文本块时,渲染一个不包含 ThinkingBubble 的标准消息气泡。
空推理块
某些模型会生成空的推理块作为占位符。过滤掉这些:
ts
const meaningfulReasoning = message.contentBlocks
.filter((b) => b.type === "reasoning" && b.reasoning.trim().length > 0);多个推理-文本循环
单条消息可以在推理块和文本块之间交替出现。如果需要保留这种交替顺序,请按顺序遍历 contentBlocks,而不是按类型分组:
ts
message.contentBlocks.forEach((block) => {
if (block.type === "reasoning") {
// 渲染 ThinkingBubble
} else if (block.type === "text") {
// 渲染文本段落
}
});最佳实践
- 默认折叠:按需展示推理,而不是默认展开
- 显示字符数:让用户快速了解回复背后有多少思考投入
- 在视觉上进行区分:使用不同的颜色、边框或背景,确保推理永远不会与实际答案混淆
- 动画过渡:平滑的展开/折叠动画能提升观感质量
- 考虑无障碍性:在切换按钮上使用正确的 ARIA 属性(
aria-expanded、aria-controls) - 在预览中截断:折叠时展示推理的简短预览,让用户决定是否展开