Streamlit状态管理进阶:DeepSeek-R1-Distill-Qwen-1.5B支持对话历史导出为Markdown
1. 项目背景与痛点分析
如果你用过一些本地部署的AI对话助手,可能会遇到一个很实际的问题:聊了半天,想保存对话记录,结果发现只能复制粘贴,或者干脆没有保存功能。特别是当模型输出了详细的思考过程、代码示例或者重要结论时,手动整理这些内容既费时又容易出错。
我最近在魔塔平台上部署了下载量很高的DeepSeek-R1-Distill-Qwen-1.5B模型,这是一个超轻量的蒸馏模型,只有1.5B参数,但融合了DeepSeek的逻辑推理能力和Qwen的成熟架构。用Streamlit做了个简单的聊天界面后,发现虽然对话体验不错,但缺少一个关键功能:对话历史的持久化保存。
想想看这些场景:
- 你让AI帮你分析一个复杂的逻辑问题,它给出了详细的推理步骤,你想保存下来以后复习
- 模型帮你写了一段代码,你想把代码和对应的解释一起保存到项目文档里
- 你和AI进行多轮技术讨论,生成了有价值的技术方案,需要整理成会议纪要
如果每次都要手动复制粘贴,不仅效率低下,还容易丢失格式。特别是这个模型会输出结构化的思考过程(用``标签标记),手动处理这些标签转换更是麻烦。
2. 为什么需要对话历史导出功能
2.1 从用户体验角度
一个好的AI对话工具,不应该只是"用完即走"。用户投入时间与AI进行对话,产生的对话记录本身就是有价值的数字资产。这些记录可能是:
- 学习笔记:AI讲解某个概念的过程
- 工作文档:AI协助生成的方案、代码、报告
- 创意草稿:与AI头脑风暴产生的想法
- 问题解决方案:AI帮助解决的具体技术问题
如果这些内容无法方便地保存和复用,那么对话的价值就大打折扣。用户需要能够随时回顾、编辑、分享这些对话记录。
2.2 从技术实现角度
DeepSeek-R1-Distill-Qwen-1.5B模型有一个特点:它会输出结构化的思考过程。在原始输出中,思考过程被包裹在标签中,最终答案在标签后。比如:
<think> 这是一个关于Python列表排序的问题。用户想要降序排列,我可以使用sorted函数配合reverse参数,或者使用sort方法。 </think> 要降序排列列表,可以使用sorted(list, reverse=True)或者list.sort(reverse=True)。在界面上,我们通过处理把这些标签转换成了更友好的"思考过程"和"回答"格式。但如果用户想保存这个对话,他们得到的应该是处理后的干净内容,而不是原始的标签文本。
2.3 从数据管理角度
Streamlit默认的会话状态(session state)是临时的,页面刷新或者服务重启后,对话历史就会丢失。虽然我们提供了"清空"按钮来主动清理,但用户更需要的是"保存"功能,把有价值的对话持久化存储。
3. 实现方案设计
3.1 核心需求分析
基于以上痛点,我决定为这个DeepSeek对话助手添加对话历史导出功能,主要满足以下几个需求:
- 格式友好:导出的内容应该是人类可读的格式,保留对话的结构和层次
- 内容完整:包含完整的对话历史,包括用户的提问和AI的回答
- 处理智能:自动处理模型输出的思考过程标签,转换成清晰的格式
- 操作简单:用户不需要复杂的操作就能导出和保存
- 多格式支持:至少支持Markdown格式,因为Markdown既适合阅读也适合后续编辑
3.2 技术选型:为什么选择Markdown
在考虑导出格式时,我对比了几种常见选项:
| 格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 纯文本 | 通用性强,任何设备都能打开 | 格式单一,无法体现层次结构 | 简单记录 |
| HTML | 格式丰富,支持复杂样式 | 文件体积大,需要浏览器查看 | 网页展示 |
| JSON | 结构化好,便于程序处理 | 人类可读性差,不适合直接阅读 | 数据交换 |
| Markdown | 轻量级,既适合阅读也适合编辑,支持代码块、列表等格式 | 需要支持Markdown的编辑器 | 技术文档、笔记保存 |
对于技术对话场景,Markdown是最合适的选择:
- 代码块可以很好地展示AI生成的代码
- 列表和标题可以组织思考过程
- 引用块可以突出重要结论
- 文件体积小,便于分享和存储
3.3 系统架构设计
整个导出功能的架构相对简单,主要涉及三个部分:
用户界面层(Streamlit) ↓ 业务逻辑层(对话处理、格式转换) ↓ 数据持久化层(Markdown生成、文件下载)关键的技术决策:
- 实时转换:不是在导出时才处理格式,而是在每轮对话时就把处理好的内容保存起来
- 增量更新:每次新对话都更新可导出的内容,而不是等到导出时才生成
- 客户端下载:生成Markdown文件后,通过浏览器直接下载,不涉及服务器端存储
4. 代码实现详解
4.1 扩展会话状态管理
首先,我们需要扩展Streamlit的会话状态(session state),不仅要存储原始的对话消息,还要存储格式化后的内容用于导出。
import streamlit as st # 初始化会话状态 if "messages" not in st.session_state: st.session_state.messages = [] # 新增:用于导出Markdown的格式化对话历史 if "export_history" not in st.session_state: st.session_state.export_history = "" # 新增:对话计数器,用于生成有序的对话记录 if "conversation_count" not in st.session_state: st.session_state.conversation_count = 04.2 改进对话处理逻辑
原来的对话处理只关注界面显示,现在需要同时生成可用于导出的格式化内容。
def process_ai_response(response_text): """ 处理AI的响应,分离思考过程和最终答案 同时生成Markdown格式的内容 """ # 原有的标签处理逻辑 think_content = "" answer_content = response_text if "<think>" in response_text and "</think>" in response_text: think_start = response_text.find("<think>") + len("<think>") think_end = response_text.find("</think>") think_content = response_text[think_start:think_end].strip() answer_content = response_text[think_end + len("</think>"):].strip() # 新增:生成Markdown格式的对话记录 conversation_md = generate_conversation_markdown(think_content, answer_content) # 保存到导出历史中 st.session_state.export_history += conversation_md return think_content, answer_content def generate_conversation_markdown(think_content, answer_content): """ 生成单轮对话的Markdown格式 """ st.session_state.conversation_count += 1 md_content = f"## 对话 {st.session_state.conversation_count}\n\n" # 获取最新的用户消息 if st.session_state.messages: last_user_msg = st.session_state.messages[-2]["content"] if len(st.session_state.messages) >= 2 else "" md_content += f"**用户**: {last_user_msg}\n\n" # 添加思考过程(如果有) if think_content: md_content += f"### 思考过程\n\n{think_content}\n\n" # 添加最终回答 md_content += f"### AI回答\n\n{answer_content}\n\n" # 添加分隔线 md_content += "---\n\n" return md_content4.3 实现Markdown导出功能
核心的导出功能通过Streamlit的下载按钮实现:
def export_conversation_to_markdown(): """ 导出完整对话历史为Markdown文件 """ if not st.session_state.export_history: st.warning("还没有对话记录可以导出") return None # 生成完整的Markdown文档 full_md = f"""# DeepSeek-R1对话记录 生成时间:{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} 模型版本:DeepSeek-R1-Distill-Qwen-1.5B 对话轮数:{st.session_state.conversation_count} --- {st.session_state.export_history} ## 对话统计 - 总对话轮数:{st.session_state.conversation_count} - 导出时间:{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - 模型配置:temperature=0.6, top_p=0.95, max_new_tokens=2048 """ return full_md # 在侧边栏添加导出按钮 with st.sidebar: st.markdown("---") st.subheader("对话管理") if st.button("🧹 清空对话"): st.session_state.messages = [] st.session_state.export_history = "" st.session_state.conversation_count = 0 st.rerun() # 新增导出按钮 if st.session_state.export_history: md_content = export_conversation_to_markdown() st.download_button( label=" 导出为Markdown", data=md_content, file_name=f"deepseek_conversation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md", mime="text/markdown", help="导出完整对话历史为Markdown文件" )4.4 处理代码块和特殊格式
对于技术对话,代码块的格式化特别重要。我们需要确保AI生成的代码在Markdown中正确显示:
def format_code_blocks(text): """ 自动检测和格式化代码块 """ # 简单的代码块检测逻辑 lines = text.split('\n') formatted_lines = [] in_code_block = False code_language = "" for line in lines: # 检测代码块开始 if line.strip().startswith("```"): if not in_code_block: # 提取编程语言 lang = line.strip()[3:].strip() code_language = lang if lang else "text" formatted_lines.append(f"```{code_language}") in_code_block = True else: formatted_lines.append("```") in_code_block = False else: formatted_lines.append(line) return '\n'.join(formatted_lines) # 在生成Markdown时应用代码块格式化 answer_content_formatted = format_code_blocks(answer_content)5. 完整集成示例
下面是把所有功能集成到原有DeepSeek对话助手中的完整示例:
import streamlit as st from transformers import AutoModelForCausalLM, AutoTokenizer import torch from datetime import datetime import re # 页面配置 st.set_page_config( page_title="DeepSeek-R1对话助手", page_icon="", layout="wide" ) # 初始化模型(使用缓存) @st.cache_resource def load_model(): model_path = "/root/ds_1.5b" st.info(f" 正在加载模型: {model_path}") tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( model_path, device_map="auto", torch_dtype="auto", trust_remote_code=True ) return model, tokenizer # 初始化会话状态 if "messages" not in st.session_state: st.session_state.messages = [] if "export_history" not in st.session_state: st.session_state.export_history = "" if "conversation_count" not in st.session_state: st.session_state.conversation_count = 0 # 侧边栏 with st.sidebar: st.title(" DeepSeek-R1") st.markdown("基于DeepSeek-R1-Distill-Qwen-1.5B的本地对话助手") st.markdown("---") st.subheader("对话管理") # 清空按钮 if st.button("🧹 清空对话", use_container_width=True): st.session_state.messages = [] st.session_state.export_history = "" st.session_state.conversation_count = 0 st.rerun() # 导出按钮(仅在对话历史时显示) if st.session_state.export_history: # 生成Markdown内容 md_content = f"""# DeepSeek-R1对话记录 生成时间:{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} 对话轮数:{len(st.session_state.messages) // 2} {st.session_state.export_history} """ st.download_button( label=" 导出为Markdown", data=md_content, file_name=f"deepseek_conversation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md", mime="text/markdown", use_container_width=True ) st.markdown("---") st.subheader("模型信息") st.markdown(""" - **模型**: DeepSeek-R1-Distill-Qwen-1.5B - **参数**: 1.5B(超轻量) - **运行**: 完全本地化 - **特性**: 思维链推理优化 """) # 主界面 st.title(" DeepSeek-R1对话助手") st.caption("完全本地运行的智能对话助手,支持对话历史导出") # 显示对话历史 for message in st.session_state.messages: with st.chat_message(message["role"]): st.markdown(message["content"]) # 对话处理函数 def format_conversation_markdown(user_input, think_content, answer_content): """格式化单轮对话为Markdown""" st.session_state.conversation_count += 1 md = f"## 第{st.session_state.conversation_count}轮对话\n\n" md += f"**用户**: {user_input}\n\n" if think_content: md += f"### 🤔 思考过程\n{think_content}\n\n" md += f"### AI回答\n{answer_content}\n\n" md += "---\n\n" return md # 用户输入 if prompt := st.chat_input("考考 DeepSeek R1..."): # 添加用户消息 st.session_state.messages.append({"role": "user", "content": prompt}) with st.chat_message("user"): st.markdown(prompt) # 生成AI回复 with st.chat_message("assistant"): with st.spinner("思考中..."): try: # 加载模型 model, tokenizer = load_model() # 构建对话历史 conversation_text = "" for msg in st.session_state.messages: if msg["role"] == "user": conversation_text += f"用户: {msg['content']}\n" else: conversation_text += f"助手: {msg['content']}\n" # 生成回复 inputs = tokenizer(conversation_text, return_tensors="pt").to(model.device) with torch.no_grad(): outputs = model.generate( **inputs, max_new_tokens=2048, temperature=0.6, top_p=0.95, do_sample=True ) response = tokenizer.decode(outputs[0], skip_special_tokens=True) # 提取新的回复部分 new_response = response[len(conversation_text):].strip() # 处理思考过程标签 think_content = "" answer_content = new_response think_match = re.search(r'<think>(.*?)</think>', new_response, re.DOTALL) if think_match: think_content = think_match.group(1).strip() answer_content = new_response[think_match.end():].strip() # 显示思考过程(如果有) if think_content: with st.expander("查看思考过程", expanded=False): st.markdown(think_content) # 显示最终回答 st.markdown(answer_content) # 保存到对话历史 st.session_state.messages.append({ "role": "assistant", "content": f"{think_content}\n\n{answer_content}" if think_content else answer_content }) # 生成并保存Markdown格式 conversation_md = format_conversation_markdown(prompt, think_content, answer_content) st.session_state.export_history += conversation_md except Exception as e: st.error(f"生成回复时出错: {str(e)}")6. 实际效果展示
6.1 对话界面增强
添加导出功能后,侧边栏会多出一个"导出为Markdown"按钮。只有当有对话历史时,这个按钮才会显示,避免用户误操作。
在对话过程中,AI的思考过程会被折叠显示(通过st.expander),点击可以展开查看详细的推理步骤。这既保持了界面的简洁,又提供了深度查看的能力。
6.2 导出的Markdown文件示例
导出的Markdown文件内容清晰、结构完整:
# DeepSeek-R1对话记录 生成时间:2024-01-15 14:30:25 对话轮数:3 ## 第1轮对话 **用户**: 用Python写一个快速排序算法 ### 🤔 思考过程 用户需要快速排序算法的Python实现。快速排序是经典的排序算法,使用分治策略。我需要解释算法原理,然后提供代码实现。代码应该包括分区函数和递归排序函数,并添加适当的注释。 ### AI回答 ```python def quick_sort(arr): """ 快速排序算法 """ if len(arr) <= 1: return arr pivot = arr[len(arr) // 2] left = [x for x in arr if x < pivot] middle = [x for x in arr if x == pivot] right = [x for x in arr if x > pivot] return quick_sort(left) + middle + quick_sort(right) # 示例使用 numbers = [3, 6, 8, 10, 1, 2, 1] sorted_numbers = quick_sort(numbers) print(f"排序前: {numbers}") print(f"排序后: {sorted_numbers}")第2轮对话
用户: 解释一下时间复杂度
🤔 思考过程
用户询问快速排序的时间复杂度。我需要解释最好、平均和最坏情况下的时间复杂度,以及空间复杂度。最好用简单的语言说明,避免太专业的数学术语。
AI回答
快速排序的时间复杂度:
- 平均情况: O(n log n) - 大多数情况下
- 最好情况: O(n log n) - 每次都能均匀划分
- 最坏情况: O(n²) - 每次选择的基准都是最大或最小值
空间复杂度为O(log n),因为递归调用需要栈空间。
(后续对话继续...)
### 6.3 文件使用场景 导出的Markdown文件可以直接用于: 1. **技术文档**:把AI讲解的技术概念保存为学习笔记 2. **代码库**:把生成的代码片段保存到项目中 3. **会议纪要**:把技术讨论保存为会议记录 4. **知识库**:积累常见问题的解决方案 5. **分享材料**:把有价值的对话分享给团队成员 ## 7. 进阶功能建议 ### 7.1 导出格式扩展 除了Markdown,还可以考虑支持其他格式: ```python def export_to_html(md_content): """将Markdown转换为HTML""" import markdown html_content = markdown.markdown(md_content, extensions=['fenced_code', 'tables']) # 添加基本样式 html_template = f""" <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>DeepSeek对话记录</title> <style> body {{ font-family: Arial, sans-serif; line-height: 1.6; }} pre {{ background: #f4f4f4; padding: 10px; }} code {{ background: #f4f4f4; }} </style> </head> <body> {html_content} </body> </html> """ return html_template def export_to_pdf(md_content): """将Markdown转换为PDF(需要额外库)""" # 可以使用weasyprint或reportlab等库 pass7.2 对话历史管理
可以添加更复杂的历史管理功能:
# 保存多个对话会话 if "conversation_sessions" not in st.session_state: st.session_state.conversation_sessions = [] def save_current_session(): """保存当前对话为一个会话""" session = { "id": len(st.session_state.conversation_sessions) + 1, "title": f"对话会话_{datetime.now().strftime('%Y%m%d_%H%M')}", "messages": st.session_state.messages.copy(), "export_history": st.session_state.export_history, "created_at": datetime.now().isoformat() } st.session_state.conversation_sessions.append(session) # 在侧边栏显示历史会话 with st.sidebar.expander("历史会话"): for session in st.session_state.conversation_sessions: if st.button(f" {session['title']}", key=f"session_{session['id']}"): # 加载选中的会话 st.session_state.messages = session["messages"].copy() st.session_state.export_history = session["export_history"] st.rerun()7.3 自动保存功能
可以添加定时自动保存功能,防止意外丢失:
import time # 自动保存逻辑 AUTO_SAVE_INTERVAL = 300 # 5分钟 if "last_save_time" not in st.session_state: st.session_state.last_save_time = time.time() # 在适当的地方检查是否需要自动保存 current_time = time.time() if current_time - st.session_state.last_save_time > AUTO_SAVE_INTERVAL: if st.session_state.messages: save_current_session() st.session_state.last_save_time = current_time st.toast("对话已自动保存", icon="💾")8. 总结
8.1 功能价值总结
通过为DeepSeek-R1-Distill-Qwen-1.5B对话助手添加Markdown导出功能,我们解决了本地AI对话工具的一个关键痛点:对话记录的持久化和可用性。这个功能的价值体现在:
- 提升效率:一键导出代替手动复制粘贴,节省大量时间
- 保证质量:自动格式化处理,确保输出内容的结构清晰
- 增强可用性:导出的Markdown文件可以直接用于文档、笔记、分享
- 保护投资:用户与AI对话的时间投入变成了可重复使用的数字资产
8.2 技术实现要点
在实现过程中,有几个关键的技术决策值得注意:
- 实时处理:在每轮对话时同步生成格式化内容,而不是导出时再处理
- 状态管理:合理扩展Streamlit的会话状态,管理原始对话和格式化内容
- 格式转换:智能处理模型输出的特殊标签和代码块
- 用户体验:导出按钮的显示条件、文件命名、进度提示等细节
8.3 实际应用建议
如果你也在构建类似的AI对话应用,我建议:
- 尽早考虑导出功能:不要等到用户抱怨时才添加
- 选择合适格式:Markdown对于技术对话是最佳选择
- 保持格式一致:确保导出的内容和界面显示的内容格式一致
- 提供多种选项:根据用户需求,可以提供不同详细程度的导出选项
8.4 未来展望
这个导出功能还可以进一步扩展:
- 云同步:将对话记录同步到云端,多设备访问
- 搜索功能:在历史对话中搜索特定内容
- 标签分类:给对话打标签,方便分类管理
- 分享协作:生成分享链接,与他人协作编辑
最重要的是,这个功能让AI对话从"一次性交互"变成了"可持续的知识积累过程"。每次与AI的对话都不再是孤立的,而是可以保存、回顾、复用和分享的宝贵资源。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。