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:
+17
-20
@@ -133,37 +133,34 @@ 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
|
||||||
|
|
||||||
# 发送检索结果 — 包含文档标题和摘要
|
# 发送检索结果(只在有文档时)
|
||||||
doc_details = []
|
if docs:
|
||||||
for i, doc in enumerate(docs[:5]):
|
doc_details = []
|
||||||
metadata = doc.metadata if hasattr(doc, 'metadata') else {}
|
for i, doc in enumerate(docs[:5]):
|
||||||
title = metadata.get("title", metadata.get("filename", f"文档 {i+1}"))
|
metadata = doc.metadata if hasattr(doc, 'metadata') else {}
|
||||||
preview = doc.page_content[:100].replace('\n', ' ')
|
title = metadata.get("title", metadata.get("filename", f"文档 {i+1}"))
|
||||||
doc_details.append(f"**{title}**: {preview}...")
|
preview = doc.page_content[:100].replace('\n', ' ')
|
||||||
|
doc_details.append(f"**{title}**: {preview}...")
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
"type": "thinking",
|
"type": "thinking",
|
||||||
"stage": "retrieved",
|
"stage": "retrieved",
|
||||||
"message": f"检索到 {len(docs)} 篇相关文档",
|
"message": f"检索到 {len(docs)} 篇相关文档",
|
||||||
"doc_count": len(docs),
|
"doc_count": len(docs),
|
||||||
"time": round(retrieval_time, 2),
|
"time": round(retrieval_time, 2),
|
||||||
"details": doc_details
|
"details": doc_details
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user