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( fdiv classai-message{full_response}/div, unsafe_allow_htmlTrue )关键问题在于{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( fdiv classai-message{full_response}/div, unsafe_allow_htmlTrue ) # 修改后的代码 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_htmlTrue) # 初始化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, tokenizeFalse, add_generation_promptTrue ) inputs tokenizer([text], return_tensorspt).to(model.device) # 创建streamer streamer TextIteratorStreamer(tokenizer, skip_promptTrue, timeout20.0) # 启动生成线程 generation_kwargs dict(inputs, streamerstreamer, max_new_tokens1024) thread Thread(targetmodel.generate, kwargsgeneration_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( 模型思考过程, expandedFalse): 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 常见问题排查如果在测试中遇到问题可以检查以下几点问题1Markdown没有渲染检查是否使用了st.markdown()而不是st.write()或HTML检查CSS是否正确加载查看浏览器开发者工具问题2样式冲突检查CSS选择器是否正确确保不会影响其他部分尝试在浏览器中检查元素查看应用的样式问题3流式输出闪烁确保使用message_placeholder.markdown()更新内容检查是否在每次更新时都重新渲染了整个Markdown问题4特殊字符问题如果内容包含HTML特殊字符如,,Streamlit会自动处理如果需要显示原始HTML使用unsafe_allow_htmlTrue参数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_htmlTrue)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_htmlTrue) # 然后就可以在Markdown中使用LaTeX了 # 例如$E mc^2$ 或 $$\sum_{i1}^n i \frac{n(n1)}{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星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。