推理令牌能够揭示如 OpenAI 的 o1/o3 和 Anthropic 的 Claude 等具备扩展思考能力的先进模型的内部思维过程。这些模型会生成结构化的内容区块,将推理过程与最终答案分离,从而让你能够构建展示模型如何得出其响应的用户界面。
什么是推理令牌?
当具备推理能力的模型处理提示时,它们会生成两种不同类型的内容:
- 推理区块:模型的内部思维链、问题分解以及逐步分析过程
- 文本区块:呈现给用户的最终、精炼的响应
这些内容以类型化内容区块的形式在 AIMessage 中传递,可通过 contentBlocks 属性访问:
// 推理区块
{ type: "reasoning", reasoning: "让我一步步思考这个问题..." }
// 文本区块
{ type: "text", text: "答案是 42。" }
并非所有模型都会产生推理令牌。此模式专门适用于支持扩展思考或思维链输出的模型。标准聊天模型仅返回文本区块。
使用场景
- 透明度:向用户展示模型的推理过程,以建立对其答案的信任
- 调试:检查模型的思维过程,以识别其出错之处
- 教育工具:通过揭示 AI 如何解决问题来教授学生问题解决技巧
- 决策支持:让领域专家验证建议背后的推理过程
- 质量保证:在受监管行业中审核推理链以确保合规性
提取推理和文本区块
AIMessage 上的 contentBlocks 数组包含按生成顺序排列的所有区块。通过 type 过滤它们,可以将推理与文本分离:
import { AIMessage } from "@langchain/core/messages";
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 访问消息
定义一个与你的智能体状态模式匹配的 TypeScript 接口,并将其作为类型参数传递给 useStream,以便以类型安全的方式访问状态值。在下面的示例中,将 typeof myAgent 替换为你的接口名称:
import type { BaseMessage } from "@langchain/core/messages";
interface AgentState {
messages: BaseMessage[];
}
import { useStream } from "@langchain/react";
import { AIMessage, HumanMessage } from "@langchain/core/messages";
function Chat() {
const stream = useStream<typeof myAgent>({
apiUrl: "http://localhost:2024",
assistantId: "reasoning",
});
return (
<div className="messages">
{stream.messages.map((msg, i) => {
if (HumanMessage.isInstance(msg)) {
return <HumanBubble key={i} text={msg.content} />;
}
if (AIMessage.isInstance(msg)) {
return (
<AIResponse
key={i}
message={msg}
isStreaming={stream.isLoading && i === stream.messages.length - 1}
/>
);
}
return null;
})}
</div>
);
}
构建 ThinkingBubble 组件
ThinkingBubble 在一个视觉上独特、可折叠的容器中呈现推理令牌。用户可以展开它以查看完整的思维过程,或折叠它以专注于最终答案。
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 (
<div className="thinking-bubble">
<button
className="thinking-header"
onClick={() => setIsExpanded(!isExpanded)}
>
<span className="thinking-icon">
{isStreaming ? (
<span className="thinking-spinner" />
) : (
"💭"
)}
</span>
<span className="thinking-label">
{isStreaming ? "思考中..." : `思维过程 (${charCount} 字符)`}
</span>
<span className={`chevron ${isExpanded ? "expanded" : ""}`}>▶</span>
</button>
{isExpanded && (
<div className="thinking-content">
<pre>{reasoning}</pre>
</div>
)}
{!isExpanded && !isStreaming && (
<div className="thinking-preview">{preview}</div>
)}
</div>
);
}
样式化 ThinkingBubble
通过独特的视觉处理方式,将推理区块与常规消息区分开来:
.thinking-bubble {
background-color: #f8f5ff;
border: 1px solid #e2d9f3;
border-radius: 8px;
padding: 12px;
margin: 8px 0;
font-size: 0.9em;
}
.thinking-header {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
background: none;
border: none;
width: 100%;
text-align: left;
color: #6b21a8;
font-weight: 500;
}
.thinking-content {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #e2d9f3;
white-space: pre-wrap;
color: #4a4a4a;
line-height: 1.5;
}
.thinking-preview {
margin-top: 4px;
color: #9ca3af;
font-style: italic;
font-size: 0.85em;
}
.chevron {
margin-left: auto;
transition: transform 0.2s;
}
.chevron.expanded {
transform: rotate(90deg);
}
推理过程的流式指示器
当模型仍在生成推理令牌时,显示一个动画指示器以传达思考正在进行中:
.thinking-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #e2d9f3;
border-top-color: #6b21a8;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
在流式传输期间,默认保持 ThinkingBubble 折叠,并仅显示旋转器。在流式传输过程中展开可能会导致新令牌到达时布局抖动。让用户在推理阶段完成后展开。
渲染完整的 AI 响应
将 ThinkingBubble 和标准文本气泡组合成一个 AIResponse 组件:
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 (
<div className="ai-response">
{hasReasoning && (
<ThinkingBubble
reasoning={reasoningBlocks}
isStreaming={isReasoningPhase}
/>
)}
{hasText && (
<div className="ai-text-bubble">
<p>{textBlocks}</p>
{isTextPhase && <span className="cursor-blink">▊</span>}
</div>
)}
</div>
);
}
处理边界情况
不含推理的消息
并非每个 AI 消息都包含推理区块。当 contentBlocks 仅包含文本区块时,渲染一个标准消息气泡,而不显示 ThinkingBubble。
空的推理区块
某些模型会生成空的推理区块作为占位符。过滤掉这些:
const meaningfulReasoning = message.contentBlocks
.filter((b) => b.type === "reasoning" && b.reasoning.trim().length > 0);
多个推理-文本循环
单个消息可能在推理和文本区块之间交替。如果需要保留这种交错顺序,请按顺序迭代 contentBlocks,而不是按类型分组:
message.contentBlocks.forEach((block) => {
if (block.type === "reasoning") {
// 渲染 ThinkingBubble
} else if (block.type === "text") {
// 渲染文本段落
}
});
最佳实践
- 默认折叠:按需显示推理过程,而非默认展开
- 显示字符数:让用户快速了解响应背后有多少思考量
- 视觉区分:使用不同的颜色、边框或背景,确保推理过程永远不会与实际答案混淆
- 动画过渡:平滑的展开/折叠动画可以提升感知质量
- 考虑可访问性:在切换按钮上使用适当的 ARIA 属性(
aria-expanded、aria-controls)
- 在预览中截断:在折叠状态下显示推理过程的简短预览,以便用户决定是否展开