refactor: simplify thinking process UI and reduce noise

- Remove redundant status steps (understanding, preparing, generating)
  from SSE stream — only emit retrieved docs and reasoning content
- ThinkingProcess: only show when there's actual reasoning or docs
- Collapse header shows concise state: thinking count or doc count
- Clean up unused icon imports

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 14:26:30 +08:00
parent ecd5c7ff31
commit 19b6cdcbd8
3 changed files with 32 additions and 70 deletions
+3 -6
View File
@@ -133,17 +133,15 @@ class RAGChain:
"""流式调用(返回答案流和文档,包含思考过程)""" """流式调用(返回答案流和文档,包含思考过程)"""
import time import time
# 0. 思考阶段开始
start_time = time.time() start_time = time.time()
yield {"type": "thinking", "stage": "understanding", "message": "正在理解问题..."}
# 1. 检索文档 # 1. 检索文档
yield {"type": "thinking", "stage": "retrieving", "message": "正在检索相关知识..."}
retrieval_start = time.time() retrieval_start = time.time()
docs = await self.retriever.ainvoke(question) docs = await self.retriever.ainvoke(question)
retrieval_time = time.time() - retrieval_start retrieval_time = time.time() - retrieval_start
# 发送检索结果 — 包含文档标题和摘要 # 发送检索结果(只在有文档时)
if docs:
doc_details = [] doc_details = []
for i, doc in enumerate(docs[:5]): for i, doc in enumerate(docs[:5]):
metadata = doc.metadata if hasattr(doc, 'metadata') else {} metadata = doc.metadata if hasattr(doc, 'metadata') else {}
@@ -162,8 +160,7 @@ class RAGChain:
context = "\n\n".join(doc.page_content for doc in docs) context = "\n\n".join(doc.page_content for doc in docs)
# 2. 构建prompt # 2. 构建prompt并流式生成
yield {"type": "thinking", "stage": "generating", "message": f"基于 {len(docs)} 篇文档生成回答..."}
prompt_value = await self.prompt.ainvoke({"context": context, "question": question}) prompt_value = await self.prompt.ainvoke({"context": context, "question": question})
messages = prompt_value.to_messages() messages = prompt_value.to_messages()
-8
View File
@@ -85,16 +85,8 @@ class ConversationChain:
"""流式调用(包含思考过程,捕获推理模型的真实推理内容)""" """流式调用(包含思考过程,捕获推理模型的真实推理内容)"""
import time import time
# 思考阶段
start_time = time.time() start_time = time.time()
yield {"type": "thinking", "stage": "understanding", "message": "正在理解问题..."}
# 准备历史
history_messages = self._format_history(chat_history or []) history_messages = self._format_history(chat_history or [])
history_count = len([m for m in (chat_history or []) if m["role"] == "user"])
yield {"type": "thinking", "stage": "preparing", "message": f"加载对话上下文({history_count} 轮历史)..." if history_count > 0 else "准备生成回答..."}
yield {"type": "thinking", "stage": "generating", "message": "正在生成回答..."}
# 直接使用原始 OpenAI SDK 捕获推理内容 # 直接使用原始 OpenAI SDK 捕获推理内容
prompt_value = await self.prompt.ainvoke({ prompt_value = await self.prompt.ainvoke({
+15 -42
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { ChatMessage, ThinkingStep } from "@/types"; import { ChatMessage, ThinkingStep } from "@/types";
import { User, Bot, Copy, Edit, RotateCcw, ThumbsUp, ThumbsDown, Loader2, CheckCircle2, FileSearch, Brain, Sparkles } from "lucide-react"; import { User, Bot, Copy, Edit, RotateCcw, ThumbsUp, ThumbsDown, Loader2, Brain } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
@@ -23,30 +23,29 @@ const ThinkingProcess = ({ thinking, isStreaming }: { thinking: ThinkingStep[];
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
if (!thinking || thinking.length === 0) return null; if (!thinking || thinking.length === 0) return null;
// 分离状态步骤推理内容 // 只提取有实际内容的步骤推理内容和检索文档
const statusSteps = thinking.filter(s => s.stage !== 'reasoning');
const reasoningSteps = thinking.filter(s => s.stage === 'reasoning'); const reasoningSteps = thinking.filter(s => s.stage === 'reasoning');
const retrievedStep = thinking.find(s => s.stage === 'retrieved');
const hasReasoning = reasoningSteps.length > 0; const hasReasoning = reasoningSteps.length > 0;
const hasDocs = retrievedStep?.details && retrievedStep.details.length > 0;
// 没有推理内容也没有文档详情时不显示
if (!hasReasoning && !hasDocs && !isStreaming) return null;
// 合并推理文本 // 合并推理文本
const reasoningText = reasoningSteps.map(s => s.message).join(''); const reasoningText = reasoningSteps.map(s => s.message).join('');
// 汇总信息 // 流式时是否正在推理
const retrievedStep = statusSteps.find(s => s.stage === 'retrieved');
const totalTime = statusSteps.reduce((sum, s) => sum + (s.time || 0), 0);
// 流式时显示最后状态
const lastStatusStep = statusSteps[statusSteps.length - 1];
const isReasoningNow = isStreaming && thinking[thinking.length - 1]?.stage === 'reasoning'; const isReasoningNow = isStreaming && thinking[thinking.length - 1]?.stage === 'reasoning';
// 折叠标题 // 折叠标题
const collapsedTitle = isStreaming const collapsedTitle = isStreaming
? isReasoningNow ? isReasoningNow ? '思考中...' : '检索中...'
? '深度思考中...'
: lastStatusStep?.message || '思考中...'
: hasReasoning : hasReasoning
? `思考过程 (${reasoningText.length} 字)` ? `思考过程 (${reasoningText.length} 字)`
: `思考过程${totalTime > 0 ? ` (${totalTime.toFixed(1)}s)` : ''}`; : retrievedStep
? `检索到 ${retrievedStep.doc_count} 篇相关文档`
: '思考过程';
return ( return (
<div className="mb-2.5"> <div className="mb-2.5">
@@ -62,37 +61,14 @@ const ThinkingProcess = ({ thinking, isStreaming }: { thinking: ThinkingStep[];
</svg> </svg>
<Brain className={cn("h-3 w-3", isReasoningNow && "animate-pulse text-primary")} /> <Brain className={cn("h-3 w-3", isReasoningNow && "animate-pulse text-primary")} />
<span>{collapsedTitle}</span> <span>{collapsedTitle}</span>
{retrievedStep?.doc_count != null && (
<span className="opacity-60 ml-1">· {retrievedStep.doc_count} </span>
)}
{totalTime > 0 && !isStreaming && (
<span className="opacity-50 ml-1">· {totalTime.toFixed(1)}s</span>
)}
</button> </button>
{expanded && ( {expanded && (
<div className="mt-1.5 ml-4 space-y-2 text-xs border-l-2 border-border pl-3"> <div className="mt-1.5 ml-4 space-y-2 text-xs border-l-2 border-border pl-3">
{/* 状态步骤 */}
{statusSteps.map((step, index) => (
<div key={`s-${index}`} className="flex items-center gap-1.5 text-muted-foreground">
{step.stage === 'retrieving' ? (
<FileSearch className="h-3 w-3" />
) : step.stage === 'retrieved' ? (
<CheckCircle2 className="h-3 w-3 text-green-500" />
) : step.stage === 'generating' ? (
<Sparkles className="h-3 w-3" />
) : (
<div className="h-1.5 w-1.5 rounded-full bg-current" />
)}
<span>{step.message}</span>
{step.time != null && <span className="opacity-50">{step.time.toFixed(1)}s</span>}
</div>
))}
{/* 检索到的文档详情 */} {/* 检索到的文档详情 */}
{retrievedStep?.details && retrievedStep.details.length > 0 && ( {hasDocs && (
<div className="mt-1 space-y-1"> <div className="space-y-1">
<div className="text-muted-foreground font-medium"></div> <div className="text-muted-foreground font-medium"></div>
{retrievedStep.details.map((detail: string, i: number) => ( {retrievedStep?.details?.map((detail: string, i: number) => (
<div key={i} className="text-muted-foreground/80 pl-2 border-l border-border/50"> <div key={i} className="text-muted-foreground/80 pl-2 border-l border-border/50">
{detail} {detail}
</div> </div>
@@ -103,9 +79,6 @@ const ThinkingProcess = ({ thinking, isStreaming }: { thinking: ThinkingStep[];
{/* 推理内容 */} {/* 推理内容 */}
{hasReasoning && ( {hasReasoning && (
<div className="mt-1"> <div className="mt-1">
<div className="text-muted-foreground font-medium mb-1">
{isReasoningNow ? '推理进行中...' : '推理过程:'}
</div>
<div className="text-foreground/80 whitespace-pre-wrap leading-relaxed bg-muted/30 rounded-lg p-2.5 max-h-80 overflow-y-auto"> <div className="text-foreground/80 whitespace-pre-wrap leading-relaxed bg-muted/30 rounded-lg p-2.5 max-h-80 overflow-y-auto">
{reasoningText} {reasoningText}
</div> </div>