news 2026/5/10 23:54:26

Nanbeige 4.1-3B Streamlit WebUI实战教程:添加Markdown渲染支持

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Nanbeige 4.1-3B Streamlit WebUI实战教程:添加Markdown渲染支持

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)

以及:

  1. 首先安装必要的依赖
  2. 然后配置环境变量
  3. 最后运行启动命令

这样的改进不仅仅是美观问题,它直接提升了用户体验:

  • 代码可读性:语法高亮让代码结构一目了然
  • 内容结构化:标题、列表、引用等格式让长回复更易阅读
  • 信息层次:不同的格式帮助用户快速抓住重点
  • 专业感:格式良好的回复让整个应用看起来更专业

2.3 技术挑战

在Streamlit中实现Markdown渲染听起来简单,但实际上有几个技术挑战需要解决:

  1. 流式输出的处理:我们的WebUI使用流式输出,Markdown内容是一点点显示出来的,不能等全部内容接收完再一次性渲染
  2. CSS样式冲突:原有的聊天气泡样式可能会与Markdown的默认样式冲突
  3. 性能考虑:频繁地重新渲染Markdown可能会影响流式输出的流畅度
  4. 特殊字符转义:需要正确处理Markdown中的特殊字符,避免破坏HTML结构

不用担心,接下来的章节会逐一解决这些问题。

3. 环境准备与代码分析

3.1 检查现有环境

在开始修改之前,确保你的开发环境已经准备好。如果你还没有运行过这个WebUI,需要先完成基础环境的搭建。

打开终端,检查是否安装了必要的依赖:

# 检查Python版本 python --version # 检查已安装的包 pip list | grep -E "streamlit|torch|transformers"

如果你还没有安装,使用以下命令安装:

pip install streamlit torch transformers accelerate

3.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 识别需要修改的部分

我们需要修改的主要是两个方面:

  1. 显示逻辑:改变AI回复的显示方式,从纯HTML文本变为Streamlit的Markdown组件
  2. 样式调整:确保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 完整的代码修改

现在让我们实现完整的修改。我们需要做两件事:

  1. 修改CSS以支持Markdown样式
  2. 修改显示逻辑以使用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格式,确保它们都能正确显示:

  1. 代码块测试:问模型“用Python写一个Hello World程序”
  2. 列表测试:问模型“列出安装Streamlit的三个步骤”
  3. 表格测试:问模型“创建一个简单的产品价格表”
  4. 标题和引用测试:问模型“用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 主要改进

  1. 从HTML文本到Markdown渲染:将AI回复的显示方式从简单的HTML文本包装改为Streamlit的原生Markdown渲染,让代码块、列表、表格等格式能够正确显示。

  2. 样式兼容性:通过精心设计的CSS,确保Markdown内容在原有的聊天气泡样式中显示正常,不会破坏整体的UI设计。

  3. 功能保持:保留了原有的思考过程折叠功能,并确保它在Markdown渲染下仍然正常工作。

  4. 性能优化:添加了防抖机制,避免在流式输出过程中频繁重新渲染导致的性能问题。

7.2 实际效果对比

修改前后最明显的区别在于代码显示。以前,一段Python代码看起来是这样的:

def hello(): print("Hello World")

现在,同样的代码看起来是这样的:

def hello(): print("Hello World")

不仅有语法高亮,还有正确的缩进和代码块背景,大大提升了可读性。

7.3 进一步优化建议

如果你想让这个WebUI更加完善,可以考虑以下方向:

  1. 主题切换:添加深色/浅色主题切换功能,让用户可以根据喜好选择
  2. 导出功能:添加将对话导出为Markdown或PDF的功能
  3. 多模型支持:扩展UI以支持切换不同的模型
  4. 对话管理:添加对话重命名、分类、搜索等功能
  5. 插件系统:设计一个插件架构,让其他人可以轻松添加新功能

7.4 最终代码获取

如果你在实现过程中遇到问题,或者想直接获取完整的修改后的代码,可以访问项目的GitHub仓库(如果有的话),或者基于本教程的代码片段组合出完整的解决方案。

记住,最好的学习方式是自己动手实现一遍。通过这个练习,你不仅学会了如何为Streamlit应用添加Markdown支持,还深入理解了Streamlit的工作原理和CSS样式的应用。

现在,你的Nanbeige WebUI不仅外观精美,功能也更加完善了。快去试试和模型进行技术对话,享受格式良好的代码和结构化回复吧!


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/17 8:36:44

ESP01S待机功耗从1.8W降到0.5W:HomeKit智能开关省电改造全记录

ESP01S待机功耗深度优化&#xff1a;从1.8W到0.5W的智能开关改造实战 智能家居设备的24小时待机功耗一直是玩家们关注的焦点。以ESP01S为核心的HomeKit智能开关为例&#xff0c;原方案待机功耗高达1.8W&#xff0c;一年下来仅待机就要消耗近16度电。经过系统改造后&#xff0c;…

作者头像 李华
网站建设 2026/4/16 20:48:33

StructBERT中文语义匹配:手把手教你搭建本地应用

StructBERT中文语义匹配&#xff1a;手把手教你搭建本地应用 1. 工具概述与核心价值 StructBERT中文语义匹配工具是基于阿里达摩院开源的StructBERT-Large模型开发的本地化解决方案。这个工具专门针对中文文本相似度计算场景&#xff0c;能够精准判断两个句子在语义层面的相似…

作者头像 李华
网站建设 2026/4/16 12:50:07

网盘下载太慢?这款直链助手让你告别龟速时代

网盘下载太慢&#xff1f;这款直链助手让你告别龟速时代 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 &#xff0c;支持 百度网盘 / 阿里云盘 / 中国移动云盘 / 天翼云盘 / 迅…

作者头像 李华
网站建设 2026/4/17 23:54:57

解锁144帧体验:EldenRingFPSUnlockAndMore全面优化指南

解锁144帧体验&#xff1a;EldenRingFPSUnlockAndMore全面优化指南 【免费下载链接】EldenRingFpsUnlockAndMore A small utility to remove frame rate limit, change FOV, add widescreen support and more for Elden Ring 项目地址: https://gitcode.com/gh_mirrors/el/El…

作者头像 李华
网站建设 2026/5/5 4:55:17

具身智能(8):EtherCAT IGH+ROS2扩展:ROS2-Controller

一、ROS2-Controllers 完整集成(工业标准接口) 1. 核心目标 实现 joint_trajectory_controller(轨迹跟踪)、joint_state_broadcaster(状态广播)与 IgH 主站的对接,兼容 ROS2 运动控制生态,支持 MoveIt! 规划器直接下发轨迹。 2. 依赖安装 # 安装 ROS2-Controllers …

作者头像 李华