Qwen3-VL-8B Web系统实战:Markdown渲染+代码块高亮+数学公式支持

1. 为什么这个聊天界面值得多看一眼?

你有没有试过用AI聊天工具,输入一段带代码的提问,结果返回的文字里代码全挤成一团、没有缩进、也没有颜色?或者写个数学推导,公式直接变成乱码或纯文本“x^2 + y^2 = r^2”?更别说贴张图问“这张流程图里第三步逻辑对吗”,结果模型根本“看不见”图片……

Qwen3-VL-8B Web系统不是又一个套壳聊天页。它从第一天设计起,就瞄准了一个被很多开源项目忽略的硬需求:让AI输出真正可读、可理解、可交付

这不是炫技,而是工程落地的真实门槛——
当你用它写技术文档,需要嵌入Python片段并高亮关键行;
当你教学生微积分,得把LaTeX公式原样渲染成清晰分式;
当你分析产品截图,要让模型一边看图一边在回复里用Markdown表格对比优劣……
这些,它都默认支持,开箱即用。

下面带你从零跑通整套系统,重点讲清楚三件事:
前端怎么把AI吐出的混排内容(文字+代码+公式+图片)安全、准确、美观地呈现出来;
代理层如何不丢字符、不截断、不破坏原始格式地透传vLLM响应;
你改哪几行代码,就能让自己的部署也拥有同款专业级渲染能力。

不讲抽象架构图,只聊你敲命令、开浏览器、看到效果的每一步。

2. 系统到底长什么样?先看真实界面

2.1 一个能“读懂”格式的聊天页

打开 http://localhost:8000/chat.html,你会看到一个极简但功能完整的PC端界面:左侧是消息流区域,右侧是模型控制面板。但真正让它与众不同的,是消息气泡里的内容渲染效果。

比如你输入:

请用Python实现快速排序,并用LaTeX写出其时间复杂度公式。

它返回的不是一整段纯文本,而是自动识别并处理:

  • 所有Python代码块被包裹在 <pre><code class="language-python"> 中,前端通过Prism.js自动上色;
  • O(n \log n) 这类表达式被识别为LaTeX,经MathJax实时渲染为标准数学字体;
  • 如果回复中包含图片URL(如Qwen-VL生成的图表),会自动转为 <img> 标签并添加懒加载;
  • 列表、引用、表格等Markdown语法全部按规范解析,不依赖后端预处理。

这背后没有魔法——只有三处关键配合:前端解析器、代理层保真转发、vLLM响应格式约定。

2.2 渲染能力拆解:谁负责什么?

组件 职责 关键保障点
vLLM推理后端 生成含原始格式标记的文本(如 python\nprint("hello")\n$$O(n\log n)$$ 必须禁用HTML转义,保持反引号和美元符原样输出
proxy_server.py 接收vLLM JSON响应,原样透传给前端,不做字符串替换 关键代码:response.json() 直接 return JSONResponse(content=content),不调用json.dumps()二次编码
chat.html 加载Prism.js + MathJax,监听消息DOM变化,动态渲染新内容 初始化时执行 Prism.highlightAll()MathJax.typeset()

注意:很多项目失败就败在代理层——比如用Flask的jsonify()封装响应,会自动把\n转成\\n,导致代码块换行丢失;或用FastAPI的JSONResponse但未设media_type="application/json",触发默认HTML转义。本系统全程规避这类陷阱。

3. 前端如何实现“所见即所得”的渲染?

3.1 chat.html 的核心渲染逻辑

打开 /root/build/chat.html,找到消息渲染函数(通常在renderMessage()或类似命名方法中)。它不直接innerHTML = response.content,而是分三步走:

  1. 安全插入基础文本
    使用textContent插入纯文字部分,杜绝XSS风险;
  2. 标记化提取富文本区块
    用正则识别代码块(lang...)、LaTeX($$...$$$...$)、图片(![]());
  3. 动态创建DOM并注入渲染器
    对每个代码块创建<pre><code>节点,设置data-language属性;对LaTeX包裹<span class="math-tex">

关键代码片段(已简化):

<!-- 在chat.html底部 -->
<script>
function renderContent(content) {
  // 步骤1:基础文本(防XSS)
  const container = document.createElement('div');
  container.textContent = content; // 先清空所有HTML标签
  
  // 步骤2:提取并替换代码块
  const codeRegex = /```(\w+)?\n([\s\S]*?)\n```/g;
  let lastIndex = 0;
  let match;
  while ((match = codeRegex.exec(content)) !== null) {
    // 插入纯文本前段
    if (match.index > lastIndex) {
      const textNode = document.createTextNode(content.slice(lastIndex, match.index));
      container.appendChild(textNode);
    }
    
    // 创建代码块节点
    const pre = document.createElement('pre');
    const code = document.createElement('code');
    code.className = `language-${match[1] || 'text'}`;
    code.textContent = match[2];
    pre.appendChild(code);
    container.appendChild(pre);
    
    lastIndex = codeRegex.lastIndex;
  }
  
  // 步骤3:处理LaTeX(略,逻辑类似)
  // 步骤4:处理图片(略)
  
  return container;
}
</script>

3.2 为什么不用marked.js?Prism.js + MathJax组合的优势

  • marked.js:强大但过度设计——它把整个Markdown转成HTML,而我们只需要精准控制代码和公式,其余文字保持纯文本更安全;
  • Prism.js:轻量(仅25KB)、支持190+语言、高亮不依赖CSS类名(用data-language驱动);
  • MathJax v3:比KaTeX更兼容复杂公式(如矩阵、多行方程),且支持异步加载,不影响首屏速度。

chat.html中,它们这样协作:

<!-- 加载CDN资源 -->
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-core.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>

<!-- 渲染后触发 -->
<script>
// 每次新消息插入DOM后执行
function onMessageRendered() {
  Prism.highlightAll(); // 代码高亮
  MathJax.typeset();    // 公式渲染
}
</script>

实测效果:单条含3个代码块+2个公式的回复,从收到JSON到完全渲染完成平均耗时127ms(测试环境:i5-1135G7 + 16GB RAM),无卡顿感。

4. 代理服务器:如何当好“不添乱”的中间人?

4.1 proxy_server.py 的三个生死原则

本系统的代理服务器(/root/build/proxy_server.py)只有128行代码,但它严格遵守三条铁律:

  1. 绝不修改响应体内容
    不做response.replace("```", "<code>")这类危险操作;
  2. 保持原始HTTP头完整
    尤其Content-Type: application/jsonTransfer-Encoding: chunked
  3. 错误时不吞掉原始信息
    vLLM返回500时,代理直接透传其JSON错误详情,而非返回“服务异常”。

核心转发逻辑(Flask风格):

@app.route("/v1/chat/completions", methods=["POST"])
def chat_completions():
    # 1. 原样转发请求体
    vllm_response = requests.post(
        f"http://localhost:{VLLM_PORT}/v1/chat/completions",
        headers={"Content-Type": "application/json"},
        data=request.get_data(),  # 关键:不解析JSON,避免编码损失
        timeout=300
    )
    
    # 2. 原样返回响应(包括状态码、头、体)
    response = Response(
        vllm_response.content,  # 关键:直接用二进制content
        status=vllm_response.status_code,
        headers=dict(vllm_response.headers)  # 复制所有头
    )
    return response

4.2 为什么request.get_data()request.json更可靠?

  • request.json:Flask自动解析JSON,若vLLM返回非UTF-8编码(如含特殊符号),会抛异常;
  • request.get_data():原始字节流,100%保真,后续由前端JS处理解码。

同样,vllm_response.content直接透传,避免json.loads()json.dumps()造成的双次序列化失真。

踩坑记录:早期版本用request.json,遇到Qwen-VL生成含emoji的回复时,中文和emoji混排出现乱码;改用字节流后问题消失。

5. vLLM后端:你需要调整的关键参数

5.1 启动命令中的隐藏开关

run_app.sh中vLLM启动命令需确保两项配置:

vllm serve "$ACTUAL_MODEL_PATH" \
    --disable-log-requests \          # 避免日志中打印原始JSON(含敏感内容)
    --enable-chunked-prefill \        # 关键!支持流式响应,让前端实时渲染
    --max-model-len 32768 \
    --gpu-memory-utilization 0.6

其中--enable-chunked-prefill是启用SSE(Server-Sent Events)流式输出的前提。没有它,前端只能等整条回复生成完毕才开始渲染,体验断层。

5.2 模型侧的输出格式约定

Qwen3-VL-8B默认输出符合OpenAI API规范,但需确认两点:

  • 代码块必须用反引号包裹,且语言标识明确(如python而非py);
  • LaTeX公式必须用双美元符$$...$$),单美元符($...$)在流式场景下易被截断。

可在start_all.sh中添加后处理钩子(可选):

# 启动vLLM后,检查模型是否支持格式化输出
if ! curl -s http://localhost:3001/v1/models | grep -q "Qwen3-VL-8B"; then
  echo "警告:模型可能未正确加载格式化支持"
fi

6. 实战:三分钟验证你的部署是否合格

别急着写代码,先用这组测试用例验证系统是否真正支持富文本:

6.1 测试1:代码高亮

输入

用Python打印斐波那契数列前10项,并用注释说明时间复杂度

预期输出特征

  • 代码块有language-python类;
  • def fib(n):等关键字为蓝色,字符串为红色,数字为橙色;
  • 注释# O(2^n) 递归解法正常显示,不被高亮引擎误判为代码。

6.2 测试2:数学公式

输入

写出勾股定理的向量形式,并用LaTeX表示

预期输出特征

  • $$\|\mathbf{a} + \mathbf{b}\|^2 = \|\mathbf{a}\|^2 + \|\mathbf{b}\|^2$$ 渲染为居中公式;
  • 字母ab为斜体,\|显示为双竖线,^2为上标。

6.3 测试3:混合内容

输入

分析这张图(上传一张含折线图的PNG),然后用表格对比三种拟合方法的R²值

预期输出特征

  • 图片URL被转为<img src="...">并显示;
  • 表格用<table>渲染,有边框和对齐;
  • R²值列保留小数点后3位(如0.987),不显示科学计数法。

验证口诀:能看图、能亮码、能显公式——三者全通,你的Qwen3-VL-8B Web系统才算真正“活”了。

7. 进阶:自定义渲染规则(5行代码搞定)

想让代码块自动添加“复制”按钮?想把> 提示转成黄色警示框?只需改chat.html

<!-- 在renderContent()函数末尾添加 -->
function enhanceElements(container) {
  // 为所有pre添加复制按钮
  container.querySelectorAll('pre').forEach(pre => {
    const btn = document.createElement('button');
    btn.textContent = '复制';
    btn.onclick = () => navigator.clipboard.writeText(pre.querySelector('code').textContent);
    pre.parentNode.insertBefore(btn, pre);
  });
  
  // 将>开头的段落转为警示框
  container.querySelectorAll('p').forEach(p => {
    if (p.textContent.trim().startsWith('> ')) {
      p.className = 'alert alert-warning';
      p.innerHTML = '<strong>提示:</strong>' + p.innerHTML.slice(2);
    }
  });
}

无需重启服务,刷新页面即生效。这才是前端可控性的价值。

8. 总结:富文本渲染不是“锦上添花”,而是“雪中送炭”

Qwen3-VL-8B Web系统的价值,从来不在它用了多大的模型或多快的GPU,而在于它把AI输出的信息密度,真正转化成了人类可消费的认知效率

  • 当工程师看到高亮代码,能3秒定位关键逻辑,而不是逐行肉眼扫描;
  • 当教师看到渲染公式,能直接截图进课件,不用再手动重排版;
  • 当产品经理看到图文混排分析,能立刻抓住数据结论,不必在文字迷宫里找数字。

这套方案没有引入任何商业组件,全部基于成熟开源库,部署成本为零,维护难度为低。它证明了一件事:最好的AI应用,往往藏在最朴素的HTML和JavaScript里

你现在要做的,只是打开终端,运行那句supervisorctl start qwen-chat,然后亲自验证——当第一行带颜色的Python代码在浏览器里跳出来时,你会明白,什么叫“所见即所得”的AI。


获取更多AI镜像

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

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐