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:
@@ -133,17 +133,15 @@ class RAGChain:
|
||||
"""流式调用(返回答案流和文档,包含思考过程)"""
|
||||
import time
|
||||
|
||||
# 0. 思考阶段开始
|
||||
start_time = time.time()
|
||||
yield {"type": "thinking", "stage": "understanding", "message": "正在理解问题..."}
|
||||
|
||||
# 1. 检索文档
|
||||
yield {"type": "thinking", "stage": "retrieving", "message": "正在检索相关知识..."}
|
||||
retrieval_start = time.time()
|
||||
docs = await self.retriever.ainvoke(question)
|
||||
retrieval_time = time.time() - retrieval_start
|
||||
|
||||
# 发送检索结果 — 包含文档标题和摘要
|
||||
# 发送检索结果(只在有文档时)
|
||||
if docs:
|
||||
doc_details = []
|
||||
for i, doc in enumerate(docs[:5]):
|
||||
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)
|
||||
|
||||
# 2. 构建prompt
|
||||
yield {"type": "thinking", "stage": "generating", "message": f"基于 {len(docs)} 篇文档生成回答..."}
|
||||
# 2. 构建prompt并流式生成
|
||||
prompt_value = await self.prompt.ainvoke({"context": context, "question": question})
|
||||
messages = prompt_value.to_messages()
|
||||
|
||||
|
||||
@@ -85,16 +85,8 @@ class ConversationChain:
|
||||
"""流式调用(包含思考过程,捕获推理模型的真实推理内容)"""
|
||||
import time
|
||||
|
||||
# 思考阶段
|
||||
start_time = time.time()
|
||||
yield {"type": "thinking", "stage": "understanding", "message": "正在理解问题..."}
|
||||
|
||||
# 准备历史
|
||||
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 捕获推理内容
|
||||
prompt_value = await self.prompt.ainvoke({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
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 ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
@@ -23,30 +23,29 @@ const ThinkingProcess = ({ thinking, isStreaming }: { thinking: ThinkingStep[];
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
if (!thinking || thinking.length === 0) return null;
|
||||
|
||||
// 分离状态步骤和推理内容
|
||||
const statusSteps = 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 hasDocs = retrievedStep?.details && retrievedStep.details.length > 0;
|
||||
|
||||
// 没有推理内容也没有文档详情时不显示
|
||||
if (!hasReasoning && !hasDocs && !isStreaming) return null;
|
||||
|
||||
// 合并推理文本
|
||||
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 collapsedTitle = isStreaming
|
||||
? isReasoningNow
|
||||
? '深度思考中...'
|
||||
: lastStatusStep?.message || '思考中...'
|
||||
? isReasoningNow ? '思考中...' : '检索中...'
|
||||
: hasReasoning
|
||||
? `思考过程 (${reasoningText.length} 字)`
|
||||
: `思考过程${totalTime > 0 ? ` (${totalTime.toFixed(1)}s)` : ''}`;
|
||||
: retrievedStep
|
||||
? `检索到 ${retrievedStep.doc_count} 篇相关文档`
|
||||
: '思考过程';
|
||||
|
||||
return (
|
||||
<div className="mb-2.5">
|
||||
@@ -62,37 +61,14 @@ const ThinkingProcess = ({ thinking, isStreaming }: { thinking: ThinkingStep[];
|
||||
</svg>
|
||||
<Brain className={cn("h-3 w-3", isReasoningNow && "animate-pulse text-primary")} />
|
||||
<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>
|
||||
{expanded && (
|
||||
<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 && (
|
||||
<div className="mt-1 space-y-1">
|
||||
{hasDocs && (
|
||||
<div className="space-y-1">
|
||||
<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">
|
||||
{detail}
|
||||
</div>
|
||||
@@ -103,9 +79,6 @@ const ThinkingProcess = ({ thinking, isStreaming }: { thinking: ThinkingStep[];
|
||||
{/* 推理内容 */}
|
||||
{hasReasoning && (
|
||||
<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">
|
||||
{reasoningText}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user