feat: capture reasoning content from thinking models, improve thinking UI

- Backend: intercept reasoning_content from DeepSeek-R1/QwQ streaming
  chunks and emit as thinking events (stage=reasoning)
- RAG chain: show retrieved document titles/previews in thinking steps
- Conversation chain: directly stream from LLM to capture reasoning
- Frontend: collapsible thinking panel with reasoning section,
  document details, and time summary
- Replace relative time with HH:mm format for message timestamps

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 11:47:14 +08:00
parent 62da8b3c58
commit 51d6fd22c0
5 changed files with 159 additions and 47 deletions
+4
View File
@@ -295,6 +295,7 @@ async def stream_message(
# 使用带思考过程的流式输出
full_answer = ""
thinking_steps = []
first_chunk = True
async for result in conversation_chain.astream_with_thinking(request.message, chat_history=chat_history):
if result["type"] == "thinking":
# 收集思考过程
@@ -305,6 +306,9 @@ async def stream_message(
})
yield f"data: {json.dumps(result, ensure_ascii=False)}\n\n"
elif result["type"] == "chunk":
if first_chunk:
# 第一个实际内容chunk前发送reasoning完成信号
first_chunk = False
full_answer += result["content"]
yield f"data: {json.dumps({'type': 'chunk', 'content': result['content']}, ensure_ascii=False)}\n\n"
elif result["type"] == "complete":
+33 -13
View File
@@ -129,38 +129,58 @@ class RAGChain:
async def astream_with_sources(self, question: str):
"""流式调用(返回答案流和文档,包含思考过程)"""
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
# 发送检索结果
# 发送检索结果 — 包含文档标题和摘要
doc_details = []
for i, doc in enumerate(docs[:5]):
metadata = doc.metadata if hasattr(doc, 'metadata') else {}
title = metadata.get("title", metadata.get("filename", f"文档 {i+1}"))
preview = doc.page_content[:100].replace('\n', ' ')
doc_details.append(f"**{title}**: {preview}...")
yield {
"type": "thinking",
"stage": "retrieved",
"message": f"{len(docs)} 相关文档",
"message": f"检索{len(docs)} 相关文档",
"doc_count": len(docs),
"time": round(retrieval_time, 2)
"time": round(retrieval_time, 2),
"details": doc_details
}
context = "\n\n".join(doc.page_content for doc in docs)
# 2. 构建prompt
yield {"type": "thinking", "stage": "generating", "message": "正在生成回答..."}
yield {"type": "thinking", "stage": "generating", "message": f"基于 {len(docs)} 篇文档生成回答..."}
messages = await self.prompt.ainvoke({"context": context, "question": question})
# 3. 流式生成答案
# 3. 流式生成答案(捕获推理内容)
answer_chunks = []
reasoning_parts = []
async for chunk in self.llm.astream(messages):
# 捕获推理内容(DeepSeek-R1/QwQ 等推理模型)
if hasattr(chunk, 'additional_kwargs') and 'reasoning_content' in chunk.additional_kwargs:
reasoning_text = chunk.additional_kwargs['reasoning_content']
if reasoning_text:
reasoning_parts.append(reasoning_text)
yield {"type": "thinking", "stage": "reasoning", "message": reasoning_text}
elif hasattr(chunk, 'reasoning_content') and chunk.reasoning_content:
reasoning_parts.append(chunk.reasoning_content)
yield {"type": "thinking", "stage": "reasoning", "message": chunk.reasoning_content}
content = chunk.content if hasattr(chunk, 'content') else str(chunk)
answer_chunks.append(content)
yield {"type": "chunk", "content": content}
if content:
answer_chunks.append(content)
yield {"type": "chunk", "content": content}
# 4. 完成,返回sources
total_time = time.time() - start_time
+26 -9
View File
@@ -80,24 +80,41 @@ class ConversationChain:
}
async def astream_with_thinking(self, question: str, chat_history: List[Dict] = None):
"""流式调用(包含思考过程)"""
"""流式调用(包含思考过程,捕获推理模型的真实推理内容"""
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": "正在生成回答..."}
# 流式生成
async for chunk in self.chain.astream({
# 直接流式调用 LLM 以捕获推理内容
prompt_messages = await self.prompt.ainvoke({
"question": question,
"chat_history": history_messages
}):
yield {"type": "chunk", "content": chunk}
})
content_started = False
async for chunk in self.llm.astream(prompt_messages):
# 捕获推理内容(DeepSeek-R1/QwQ 等推理模型)
if hasattr(chunk, 'additional_kwargs') and 'reasoning_content' in chunk.additional_kwargs:
reasoning_text = chunk.additional_kwargs['reasoning_content']
if reasoning_text:
yield {"type": "thinking", "stage": "reasoning", "message": reasoning_text}
elif hasattr(chunk, 'reasoning_content') and chunk.reasoning_content:
yield {"type": "thinking", "stage": "reasoning", "message": chunk.reasoning_content}
content = chunk.content if hasattr(chunk, 'content') else str(chunk)
if content:
if not content_started:
content_started = True
yield {"type": "chunk", "content": content}
# 完成
total_time = time.time() - start_time