Nanbeige 4.1-3B Streamlit WebUI实战教程:添加Markdown渲染支持
1. 引言
如果你已经体验过那个极简清爽的Nanbeige 4.1-3B Streamlit WebUI,可能会发现一个美中不足的地方:AI回复的内容都是纯文本格式。当模型输出代码块、列表、标题等Markdown格式的内容时,它们只是以普通文本的形式显示,失去了原本的结构和可读性。
想象一下这样的场景:你问模型“用Python写一个快速排序算法”,它确实给出了正确的代码,但代码在界面上显示为一大段没有语法高亮、没有缩进格式的普通文本。或者当模型输出一个步骤列表时,所有项目都挤在一起,完全没有列表应有的清晰结构。
这就是我们今天要解决的问题。本教程将手把手教你如何为这个已经很好看的WebUI添加完整的Markdown渲染支持,让AI的回复不仅内容正确,而且格式美观、易于阅读。
学习目标:
- 理解如何在Streamlit中渲染Markdown内容
- 学会修改现有的WebUI代码以支持Markdown格式
- 掌握处理流式输出中Markdown内容的技术要点
- 获得一个功能更完善、体验更好的对话界面
前置知识:只需要基本的Python知识,了解一点HTML/CSS会有帮助但不是必须的。即使你是前端小白,也能跟着教程一步步完成。
2. 为什么需要Markdown渲染支持
2.1 当前界面的局限性
让我们先看看当前界面在处理格式内容时的问题。当你使用现有的WebUI与Nanbeige 4.1-3B模型对话时,如果模型回复中包含Markdown格式,你会看到类似这样的效果:
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)这段代码在界面上显示为普通的文本段落,没有语法高亮,没有代码块的背景色,缩进也不明显。对于开发者来说,这样的代码可读性很差。
同样,如果模型输出一个步骤列表:
1. 首先安装必要的依赖 2. 然后配置环境变量 3. 最后运行启动命令你会看到数字和文字连在一起,完全没有列表的视觉效果。
2.2 Markdown渲染的价值
添加Markdown渲染支持后,同样的内容会变成这样:
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)以及:
- 首先安装必要的依赖
- 然后配置环境变量
- 最后运行启动命令
这样的改进不仅仅是美观问题,它直接提升了用户体验:
- 代码可读性:语法高亮让代码结构一目了然
- 内容结构化:标题、列表、引用等格式让长回复更易阅读
- 信息层次:不同的格式帮助用户快速抓住重点
- 专业感:格式良好的回复让整个应用看起来更专业
2.3 技术挑战
在Streamlit中实现Markdown渲染听起来简单,但实际上有几个技术挑战需要解决:
- 流式输出的处理:我们的WebUI使用流式输出,Markdown内容是一点点显示出来的,不能等全部内容接收完再一次性渲染
- CSS样式冲突:原有的聊天气泡样式可能会与Markdown的默认样式冲突
- 性能考虑:频繁地重新渲染Markdown可能会影响流式输出的流畅度
- 特殊字符转义:需要正确处理Markdown中的特殊字符,避免破坏HTML结构
不用担心,接下来的章节会逐一解决这些问题。
3. 环境准备与代码分析
3.1 检查现有环境
在开始修改之前,确保你的开发环境已经准备好。如果你还没有运行过这个WebUI,需要先完成基础环境的搭建。
打开终端,检查是否安装了必要的依赖:
# 检查Python版本 python --version # 检查已安装的包 pip list | grep -E "streamlit|torch|transformers"如果你还没有安装,使用以下命令安装:
pip install streamlit torch transformers accelerate3.2 理解现有代码结构
让我们先快速浏览一下现有的app.py文件,理解它的工作原理。打开你的app.py文件,找到显示AI回复的关键部分。
在原始代码中,AI的回复是通过st.markdown()函数显示的,但注意看,它实际上是把整个回复内容包装在一个HTML div中,用于实现聊天气泡的样式:
# 原始代码片段(简化版) with st.chat_message("assistant"): message_placeholder = st.empty() full_response = "" for chunk in stream_response: full_response += chunk # 这里只是简单地在div中显示文本 message_placeholder.markdown( f'<div class="ai-message">{full_response}</div>', unsafe_allow_html=True )关键问题在于:{full_response}中的Markdown内容被直接当作HTML文本处理了,Streamlit的Markdown渲染器没有机会解析其中的Markdown语法。
3.3 识别需要修改的部分
我们需要修改的主要是两个方面:
- 显示逻辑:改变AI回复的显示方式,从纯HTML文本变为Streamlit的Markdown组件
- 样式调整:确保Markdown内容在聊天气泡中显示正常,不会破坏现有的UI设计
让我们先备份一下原始文件,以防修改过程中出现问题:
# 备份原始文件 cp app.py app.py.backup现在我们可以放心地开始修改了。
4. 实现Markdown渲染支持
4.1 基础方案:直接使用st.markdown()
最简单的解决方案是直接使用Streamlit的st.markdown()函数来显示内容。Streamlit内置了完整的Markdown解析和渲染能力,我们只需要把内容传递给它就行了。
修改AI回复部分的代码:
# 修改前的代码 message_placeholder.markdown( f'<div class="ai-message">{full_response}</div>', unsafe_allow_html=True ) # 修改后的代码 message_placeholder.markdown(full_response)是的,就是这么简单!Streamlit会自动识别并渲染Markdown格式。但是,这样修改会带来一个问题:我们失去了聊天气泡的样式。
4.2 保留气泡样式的解决方案
我们需要在保持聊天气泡样式的同时支持Markdown渲染。解决方案是使用CSS来为Markdown内容添加样式,而不是依赖HTML包装。
首先,我们需要修改CSS,为Markdown内容定义样式。找到app.py中的CSS部分(通常在文件开头),添加以下样式:
/* 为Markdown内容添加样式 */ .stMarkdown { font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; line-height: 1.6; } /* Markdown代码块样式 */ .stMarkdown pre { background-color: #f6f8fa; border-radius: 6px; padding: 16px; overflow: auto; border: 1px solid #e1e4e8; } .stMarkdown code { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace; padding: 0.2em 0.4em; margin: 0; font-size: 85%; background-color: rgba(175, 184, 193, 0.2); border-radius: 6px; } /* Markdown列表样式 */ .stMarkdown ul, .stMarkdown ol { padding-left: 2em; margin: 1em 0; } .stMarkdown li { margin: 0.5em 0; } /* Markdown引用样式 */ .stMarkdown blockquote { border-left: 4px solid #dfe2e5; padding-left: 1em; margin: 1em 0; color: #6a737d; } /* Markdown表格样式 */ .stMarkdown table { border-collapse: collapse; margin: 1em 0; width: 100%; } .stMarkdown th, .stMarkdown td { border: 1px solid #dfe2e5; padding: 6px 13px; } .stMarkdown th { background-color: #f6f8fa; font-weight: 600; }4.3 完整的代码修改
现在让我们实现完整的修改。我们需要做两件事:
- 修改CSS以支持Markdown样式
- 修改显示逻辑以使用Streamlit的Markdown渲染
以下是修改后的关键代码部分:
import streamlit as st import torch from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer from threading import Thread import time # 模型路径配置 MODEL_PATH = "/your/model/path/here" # 修改为你的实际路径 # 自定义CSS - 添加Markdown支持 st.markdown(""" <style> /* 原有的气泡样式保持不变 */ .chat-message { padding: 1rem; border-radius: 0.5rem; margin-bottom: 1rem; display: flex; flex-direction: row; align-items: flex-start; } .chat-message.user { background-color: #2b313e; justify-content: flex-end; } .chat-message.assistant { background-color: #475063; justify-content: flex-start; } /* 新增:Markdown内容容器样式 */ .markdown-container { width: 100%; max-width: 800px; margin: 0 auto; } /* Markdown通用样式 */ .stMarkdown { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif; line-height: 1.6; color: #e0e0e0; } /* 代码块样式 */ .stMarkdown pre { background-color: #1e1e1e; border-radius: 8px; padding: 16px; overflow: auto; border: 1px solid #404040; margin: 1em 0; } .stMarkdown code { font-family: 'Cascadia Code', 'Consolas', 'Monaco', 'Courier New', monospace; padding: 0.2em 0.4em; margin: 0; font-size: 90%; background-color: rgba(100, 100, 100, 0.3); border-radius: 4px; color: #d4d4d4; } /* 确保代码块内的代码也有样式 */ .stMarkdown pre code { background-color: transparent; padding: 0; } /* 列表样式 */ .stMarkdown ul, .stMarkdown ol { padding-left: 2em; margin: 1em 0; } .stMarkdown li { margin: 0.5em 0; color: #e0e0e0; } /* 引用样式 */ .stMarkdown blockquote { border-left: 4px solid #555; padding-left: 1em; margin: 1em 0; color: #aaa; font-style: italic; } /* 表格样式 */ .stMarkdown table { border-collapse: collapse; margin: 1em 0; width: 100%; background-color: #2d2d2d; } .stMarkdown th, .stMarkdown td { border: 1px solid #555; padding: 8px 12px; color: #e0e0e0; } .stMarkdown th { background-color: #3d3d3d; font-weight: 600; } /* 链接样式 */ .stMarkdown a { color: #64b5f6; text-decoration: none; } .stMarkdown a:hover { text-decoration: underline; } /* 标题样式 */ .stMarkdown h1, .stMarkdown h2, .stMarkdown h3 { color: #ffffff; margin-top: 1.5em; margin-bottom: 0.5em; font-weight: 600; } .stMarkdown h1 { font-size: 1.8em; border-bottom: 2px solid #555; padding-bottom: 0.3em; } .stMarkdown h2 { font-size: 1.5em; border-bottom: 1px solid #555; padding-bottom: 0.2em; } .stMarkdown h3 { font-size: 1.3em; } </style> """, unsafe_allow_html=True) # 初始化session state if "messages" not in st.session_state: st.session_state.messages = [] # 加载模型(原有代码保持不变) @st.cache_resource def load_model(): # ... 原有的模型加载代码 ... pass # 流式生成函数(原有代码保持不变) def stream_generate(prompt, model, tokenizer, streamer): # ... 原有的生成代码 ... pass # 主界面 st.title("🌸 Nanbeige 4.1-3B Chat") # 显示历史消息 - 修改这部分以支持Markdown for message in st.session_state.messages: with st.chat_message(message["role"]): # 使用st.markdown()而不是直接HTML st.markdown(message["content"]) # 用户输入 if prompt := st.chat_input("请输入你的问题..."): # 添加用户消息 st.session_state.messages.append({"role": "user", "content": prompt}) with st.chat_message("user"): st.markdown(prompt) # 生成AI回复 with st.chat_message("assistant"): message_placeholder = st.empty() full_response = "" # 获取模型和tokenizer model, tokenizer = load_model() # 准备输入 messages = [{"role": "user", "content": prompt}] text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) inputs = tokenizer([text], return_tensors="pt").to(model.device) # 创建streamer streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, timeout=20.0) # 启动生成线程 generation_kwargs = dict(inputs, streamer=streamer, max_new_tokens=1024) thread = Thread(target=model.generate, kwargs=generation_kwargs) thread.start() # 流式显示 for chunk in streamer: full_response += chunk # 关键修改:使用st.markdown()渲染内容 message_placeholder.markdown(full_response) # 保存到历史 st.session_state.messages.append({"role": "assistant", "content": full_response})4.4 处理思考过程(CoT)的折叠
原有的WebUI有一个很好的功能:自动折叠模型的思考过程(<think>...</think>之间的内容)。我们需要确保这个功能在Markdown渲染下仍然正常工作。
修改思考过程的处理逻辑:
# 在流式显示部分,添加对思考过程的特殊处理 for chunk in streamer: full_response += chunk # 检查是否包含思考过程 if "</think>" in full_response: # 分割思考过程和最终回复 if "<think>" in full_response and "</think>" in full_response: thought_start = full_response.find("<think>") thought_end = full_response.find("</think>") + 2 thought_content = full_response[thought_start:thought_end] final_response = full_response[thought_end:] # 创建可折叠的思考过程 with st.expander("🤔 模型思考过程", expanded=False): st.markdown(thought_content.replace("<think>", "").replace("</think>", "")) # 显示最终回复 message_placeholder.markdown(final_response) else: message_placeholder.markdown(full_response) else: message_placeholder.markdown(full_response)5. 测试与验证
5.1 启动修改后的WebUI
保存所有修改后,让我们启动WebUI进行测试:
streamlit run app.py浏览器会自动打开http://localhost:8501,如果一切正常,你应该能看到原有的界面,但现在AI的回复应该能够正确渲染Markdown格式了。
5.2 测试不同格式的Markdown
让我们测试一些常见的Markdown格式,确保它们都能正确显示:
- 代码块测试:问模型“用Python写一个Hello World程序”
- 列表测试:问模型“列出安装Streamlit的三个步骤”
- 表格测试:问模型“创建一个简单的产品价格表”
- 标题和引用测试:问模型“用Markdown格式写一段技术文档”
你应该看到:
- 代码块有语法高亮和背景色
- 列表有正确的缩进和项目符号
- 表格有边框和交替的行背景色
- 标题有不同的大小和样式
- 引用有左侧边框和斜体样式
5.3 常见问题排查
如果在测试中遇到问题,可以检查以下几点:
问题1:Markdown没有渲染
- 检查是否使用了
st.markdown()而不是st.write()或HTML - 检查CSS是否正确加载(查看浏览器开发者工具)
问题2:样式冲突
- 检查CSS选择器是否正确,确保不会影响其他部分
- 尝试在浏览器中检查元素,查看应用的样式
问题3:流式输出闪烁
- 确保使用
message_placeholder.markdown()更新内容 - 检查是否在每次更新时都重新渲染了整个Markdown
问题4:特殊字符问题
- 如果内容包含HTML特殊字符(如
<,>,&),Streamlit会自动处理 - 如果需要显示原始HTML,使用
unsafe_allow_html=True参数
6. 进阶优化与个性化
6.1 自定义代码高亮主题
如果你对默认的代码高亮颜色不满意,可以自定义语法高亮主题。Streamlit使用Prism.js进行代码高亮,我们可以通过CSS覆盖默认样式:
/* 自定义代码高亮颜色 */ .stMarkdown pre[class*="language-"] { background-color: #1a1a1a !important; } .token.comment, .token.prolog, .token.doctype, .token.cdata { color: #6a9955 !important; } .token.punctuation { color: #d4d4d4 !important; } .token.property, .token.tag, .token.boolean, .token.number, .token.constant, .token.symbol, .token.deleted { color: #b5cea8 !important; } .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #ce9178 !important; } .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string { color: #d4d4d4 !important; } .token.atrule, .token.attr-value, .token.keyword { color: #569cd6 !important; } .token.function, .token.class-name { color: #dcdcaa !important; }6.2 添加复制代码按钮
对于代码块,添加一个复制按钮会很有用。虽然Streamlit本身不支持这个功能,但我们可以通过一些JavaScript技巧来实现:
# 在CSS之后添加JavaScript st.markdown(""" <script> // 为所有代码块添加复制按钮 document.addEventListener('DOMContentLoaded', function() { // 等待Streamlit渲染完成 setTimeout(function() { const codeBlocks = document.querySelectorAll('pre code'); codeBlocks.forEach((codeBlock) => { // 创建复制按钮 const copyButton = document.createElement('button'); copyButton.className = 'copy-code-button'; copyButton.textContent = '复制'; copyButton.style.cssText = ` position: absolute; top: 8px; right: 8px; padding: 4px 8px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; opacity: 0.7; transition: opacity 0.3s; `; // 添加悬停效果 copyButton.addEventListener('mouseenter', () => { copyButton.style.opacity = '1'; }); copyButton.addEventListener('mouseleave', () => { copyButton.style.opacity = '0.7'; }); // 添加点击事件 copyButton.addEventListener('click', () => { const code = codeBlock.textContent; navigator.clipboard.writeText(code).then(() => { const originalText = copyButton.textContent; copyButton.textContent = '已复制!'; copyButton.style.background = '#45a049'; setTimeout(() => { copyButton.textContent = originalText; copyButton.style.background = '#4CAF50'; }, 2000); }); }); // 将按钮添加到代码块的父元素 const preElement = codeBlock.parentElement; preElement.style.position = 'relative'; preElement.appendChild(copyButton); }); }, 1000); // 延迟1秒确保内容已渲染 }); </script> """, unsafe_allow_html=True)6.3 优化流式Markdown渲染性能
当Markdown内容很长时,频繁重新渲染可能会影响性能。我们可以添加一个简单的防抖机制:
# 在流式显示部分添加防抖 import time # ... 其他代码 ... message_placeholder = st.empty() full_response = "" last_render_time = time.time() render_interval = 0.1 # 每0.1秒渲染一次 for chunk in streamer: full_response += chunk current_time = time.time() # 防抖:只有超过一定时间间隔才重新渲染 if current_time - last_render_time >= render_interval: message_placeholder.markdown(full_response) last_render_time = current_time # 最后确保渲染最终结果 message_placeholder.markdown(full_response)6.4 支持LaTeX数学公式
如果你的使用场景需要显示数学公式,可以启用Streamlit的LaTeX支持:
# 在文件开头添加LaTeX支持 st.markdown(r""" <style> .katex { font-size: 1.1em; } </style> """, unsafe_allow_html=True) # 然后就可以在Markdown中使用LaTeX了 # 例如:$E = mc^2$ 或 $$\sum_{i=1}^n i = \frac{n(n+1)}{2}$$7. 总结
通过本教程,我们成功地为Nanbeige 4.1-3B Streamlit WebUI添加了完整的Markdown渲染支持。让我们回顾一下实现的关键点:
7.1 主要改进
从HTML文本到Markdown渲染:将AI回复的显示方式从简单的HTML文本包装改为Streamlit的原生Markdown渲染,让代码块、列表、表格等格式能够正确显示。
样式兼容性:通过精心设计的CSS,确保Markdown内容在原有的聊天气泡样式中显示正常,不会破坏整体的UI设计。
功能保持:保留了原有的思考过程折叠功能,并确保它在Markdown渲染下仍然正常工作。
性能优化:添加了防抖机制,避免在流式输出过程中频繁重新渲染导致的性能问题。
7.2 实际效果对比
修改前后最明显的区别在于代码显示。以前,一段Python代码看起来是这样的:
def hello(): print("Hello World")现在,同样的代码看起来是这样的:
def hello(): print("Hello World")不仅有语法高亮,还有正确的缩进和代码块背景,大大提升了可读性。
7.3 进一步优化建议
如果你想让这个WebUI更加完善,可以考虑以下方向:
- 主题切换:添加深色/浅色主题切换功能,让用户可以根据喜好选择
- 导出功能:添加将对话导出为Markdown或PDF的功能
- 多模型支持:扩展UI以支持切换不同的模型
- 对话管理:添加对话重命名、分类、搜索等功能
- 插件系统:设计一个插件架构,让其他人可以轻松添加新功能
7.4 最终代码获取
如果你在实现过程中遇到问题,或者想直接获取完整的修改后的代码,可以访问项目的GitHub仓库(如果有的话),或者基于本教程的代码片段组合出完整的解决方案。
记住,最好的学习方式是自己动手实现一遍。通过这个练习,你不仅学会了如何为Streamlit应用添加Markdown支持,还深入理解了Streamlit的工作原理和CSS样式的应用。
现在,你的Nanbeige WebUI不仅外观精美,功能也更加完善了。快去试试和模型进行技术对话,享受格式良好的代码和结构化回复吧!
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。