在之前的分享中,很多朋友对 DeepSeek harness 输出的纯文本结果感到头疼:明明模型已经生成了漂亮的 SVG 图形代码、图表配置和 Markdown 结构化内容,却只能在控制台里看到一堆“冰冷”的源码。想预览效果,要么复制到在线工具,要么本地起服务,非常影响调试和日常使用。今天带来的第二弹大更新,就是围绕渲染能力做的一次系统升级:即插即用、支持 SVG 与图表直出、Markdown 一键渲染 HTML。本文会结合完整示例,拆解插件设计和实现思路。
1. 背景与核心概念
1.1 从一串 Markdown 到一张图:渲染插件解决了什么
先还原一个非常常见的场景。
你在使用 DeepSeek harness 跑一个数据分析任务,模型返回了这样一段内容:
根据上面的数据,我绘制了一张柱状图,以下是 ECharts 配置: { "xAxis": ["1月", "2月", "3月"], "series": [12, 19, 3] }如果没有渲染能力,你只能在终端里看到 JSON 原文,然后再手动复制到一个 HTML 文件中,引入 ECharts,粘贴配置,刷新浏览器。一次两次还能忍,如果每天都在调 prompt、调工具、做 Agent 任务编排,这种低效操作会严重打断开发节奏。
渲染插件要解决的就是这个问题:把模型输出的 SVG、图表配置、Markdown 文本,自动识别并渲染成可视化的内容,让“结果”直接呈现在用户面前。说得直白一点,就是让 AI 的产出从“代码”变成“画面”。
1.2 什么是 DeepSeek harness
先说 harness 这个词。在 AI 工程领域,harness 可以理解为“套件”或“工作台”,它不只是简单调用大模型 API,而是提供了一套完整的环境:上下文管理、工具调用、代码执行、任务编排、结果输出等。
DeepSeek harness 就是以 DeepSeek 模型为底座的这种工作台,适合用来搭建 Agent 应用、自动化脚本、数据处理流水线。你可以把它理解成一个中间层,连接“模型大脑”和“外部工具”。
而输出渲染就是 harness 的重要能力之一。模型生成的内容如果只能以文本形式返回,能力会大打折扣。一旦接入渲染层,模型就能直接生成图表、流程图、数据看板,甚至完整的小型网页。
1.3 第二弹大更新包含什么
本次更新围绕“即插即用渲染”做了几件事:
- SVG 直接渲染:模型输出的 SVG 标签不再当成纯文本展示,而是直接绘制成矢量图。
- 图表配置渲染:识别常见的 ECharts / Chart.js 配置结构,自动生成交互式图表。
- Markdown 渲染 HTML:支持代码高亮、表格、引用、列表等常用 Markdown 语法。
- 更快的渲染管线:合并资源加载,减少重复初始化。
- 插件 API 调整:注册方式更统一,支持按类型路由。
下面我会先讲解核心原理,再给出一个完整可运行的渲染插件示例。
2. 环境准备与版本说明
本文的示例以常见环境为基础,重点演示核心思路。版本号需要根据你的实际项目调整,不建议直接照搬。
2.1 运行环境
- 操作系统:Windows 10/11、macOS、Linux 均可。
- Python:3.10 或更高版本。
- Node.js:18 或更高版本(用于前端资源构建和本地预览)。
- DeepSeek harness:需要支持插件注册机制,不同版本 API 可能有差异。
- 浏览器:Chrome、Edge 等现代浏览器,用于查看渲染结果。
如果你的 harness 版本较旧,可能不支持某些插件接口,建议先升级到最新版。
2.2 技术选型
渲染层用 Python + HTML 模板实现,前端渲染统一交给浏览器环境。这样做的好处是:
- 模型输出的 SVG、HTML 片段可以直接借助浏览器解析。
- 图表库只需要在 HTML 中引入一次,后续复用。
- 避免在 Python 端逐行解析 SVG 的繁琐工作。
插件结构上,核心部分包括:插件清单、渲染入口、前端模板、样式文件。
3. 核心功能原理解析
3.1 SVG 为什么需要独立渲染
SVG(Scalable Vector Graphics)是基于 XML 的矢量图形格式,它直接描述了图形的坐标、路径、文字等要素。模型生成 SVG 代码并不难,难的是如何“安全、高效”地展示它。
通常模型输出有两种情况:
第一种,完整的 SVG 文档:
<svg width="300" height="200" xmlns="http://www.w3.org/2000/svg"> <rect x="10" y="10" width="100" height="80" fill="#4A90D9" /> <text x="20" y="140" font-size="14">Hello SVG</text> </svg>第二种,不完整的片段:
<rect x="10" y="10" width="100" height="80" fill="#4A90D9" />渲染插件需要做两件事:识别内容是否为 SVG;对不完整的片段进行补齐包装,比如外面套一层<svg>标签。
另外,SVG 本质是 HTML 可嵌入内容,直接插入页面时可以正常显示,但要特别注意其中的<script>标签和外部引用,这是安全风险点。
3.2 图表渲染:从 JSON 配置到交互式图表
图表渲染是本次更新的重点。
模型输出图表的常用方式有两种:
第一种方式是直接生成 HTML 代码,里面用<script>引入 ECharts 并初始化。
第二种方式是只给一个配置对象,例如:
{ "type": "bar", "data": { "categories": ["1月", "2月", "3月", "4月"], "series": [ { "name": "销量", "data": [120, 200, 150, 80] } ] }, "options": { "title": "季度销量统计" } }插件要做的,就是把上面的配置转换成 ECharts 的标准 option,再调用图表库渲染。
简单来说,渲染链路如下:
- 从输出文本中提取图表配置 JSON。
- 转换成 ECharts 标准 option 结构。
- 在 HTML 容器中执行
echarts.init和setOption。 - 监听窗口大小变化,自动
resize。
3.3 Markdown 渲染 HTML 的安全边界
Markdown 渲染看起来简单,但安全边界要特别注意。
模型生成的 Markdown 中可能包含原始 HTML 标签。默认情况下不应直接透传,否则可能引入 XSS 漏洞。典型的风险代码:
<img src="x" onerror="alert(document.cookie)">渲染时应做到:
- 优先解析标准 Markdown 语法,不直接执行内联 HTML。
- 代码块高亮时,避免把用户输入当 HTML 解析。
- 如果必须支持原始 HTML,需要使用白名单过滤方案(如 DOMPurify)。
安全是第一位的。千万不要因为追求渲染效果而关闭过滤,尤其在代理工具链中,模型输出的内容可能来自不可信来源。
4. 完整实战案例:写一个 DeepSeek harness 渲染插件
下面我们从头搭建一个最小可用的渲染插件。这个插件支持三种类型:svg、charts、markdown。
4.1 创建插件目录结构
推荐按下面的结构组织文件:
render-plugin/ ├── manifest.json ├── renderer.py ├── templates/ │ └── renderer.html └── assets/ ├── echarts.min.js └── style.css说明:
manifest.json:插件清单,声明插件名称、版本、支持的渲染类型。renderer.py:插件主逻辑,负责分发渲染请求。templates/renderer.html:渲染模板,浏览器端负责把内容画出来。assets/:存放前端资源。
4.2 编写插件清单 manifest.json
{ "name": "render-plugin", "version": "2.0.0", "description": "DeepSeek harness 渲染插件,支持 SVG、图表、Markdown 渲染 HTML", "render_types": ["svg", "charts", "markdown"], "entry": "renderer.py", "template": "templates/renderer.html" }这个清单告诉 harness:这个插件能处理什么类型的数据,入口文件是什么。
如果你的 harness 版本使用不同的插件规范,manifest.json的字段名可能需要调整。核心思路是“声明能力 + 声明入口”。
4.3 编写后端渲染核心 renderer.py
这部分负责判断输出内容属于哪种类型,然后调用对应逻辑生成渲染所需的 payload。
# 文件路径:render-plugin/renderer.py import json import re from typing import Any, Dict def detect_content_type(content: str) -> str: """ 判断输出内容的类型。 优先级:charts > svg > markdown """ content = content.strip() # 尝试解析 JSON,判断是否为图表配置 if content.startswith("{"): try: data = json.loads(content) if "series" in data or "data" in data: return "charts" except json.JSONDecodeError: pass # 判断是否为 SVG 标签 if "<svg" in content or "<rect" in content or "<circle" in content: return "svg" # 默认按 Markdown 渲染 return "markdown" def wrap_svg(content: str) -> str: """ 对不完整的 SVG 片段进行补齐。 如果内容缺少外层 <svg> 标签,自动补一个。 """ if "<svg" not in content: return ( '<svg width="600" height="400" ' 'xmlns="http://www.w3.org/2000/svg">' + content + "</svg>" ) return content def build_charts_payload(content: str) -> Dict[str, Any]: """ 将输入内容转换为 ECharts 标准 option。 这里演示一个最简转换逻辑,实际使用时可扩展更多图表类型。 """ data = json.loads(content) categories = data.get("data", {}).get("categories", []) series = data.get("data", {}).get("series", []) option = { "title": {"text": data.get("options", {}).get("title", "")}, "tooltip": {}, "xAxis": {"data": categories}, "yAxis": {}, "series": series, } return option def render(content: str) -> Dict[str, Any]: """ 渲染插件的统一入口。 harness 会调用这个方法,并传入模型输出内容。 """ content_type = detect_content_type(content) if content_type == "svg": svg_content = wrap_svg(content) return { "type": "svg", "content": svg_content, } if content_type == "charts": option = build_charts_payload(content) return { "type": "charts", "content": option, } # markdown 类型 return { "type": "markdown", "content": content, }关键点在于detect_content_type函数。实际场景中模型输出格式可能更复杂,比如 Markdown 代码块里包着 SVG 代码。进阶方案是先检测是否存在代码块标记,再做二次判断。
这里为了演示清晰,先做了一个简化版本。在真实项目中,建议使用更严格的内容识别策略,比如优先检测代码块语言。
4.4 编写前端渲染模板 renderer.html
前端模板负责把render()方法返回的 payload 可视化渲染。
<!-- 文件路径:render-plugin/templates/renderer.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Render Plugin Preview</title> <link rel="stylesheet" href="../assets/style.css" /> <script src="../assets/echarts.min.js"></script> </head> <body> <div id="app"></div> <script> // 这个函数由 harness 注入 payload function renderPayload(payload) { const app = document.getElementById('app'); app.innerHTML = ''; if (payload.type === 'svg') { renderSVG(app, payload.content); } else if (payload.type === 'charts') { renderCharts(app, payload.content); } else if (payload.type === 'markdown') { renderMarkdown(app, payload.content); } } function renderSVG(container, svgContent) { // 使用 DOMParser 解析 SVG,避免直接 innerHTML 插入带来的风险 const parser = new DOMParser(); const doc = parser.parseFromString(svgContent, 'image/svg+xml'); const svg = doc.documentElement; if (svg.nodeName !== 'svg') { container.innerHTML = '<p style="color:#c00">SVG 解析失败</p>'; return; } container.appendChild(svg); } function renderCharts(container, option) { const div = document.createElement('div'); div.style.width = '100%'; div.style.height = '460px'; container.appendChild(div); const chart = echarts.init(div); chart.setOption(option); window.addEventListener('resize', () => chart.resize()); } function renderMarkdown(container, markdownContent) { // 这里用简单的转义方式处理 HTML,避免 XSS // 生产环境建议使用 markdown-it + DOMPurify const escaped = markdownContent .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>'); const html = markedFallback(escaped); container.innerHTML = html; } // 极简 Markdown 转换,仅演示思路 // 工程化场景请使用 markdown-it 等成熟库 function markedFallback(text) { const lines = text.split('\n'); let html = ''; for (const line of lines) { if (line.startsWith('# ')) { html += `<h1>${line.slice(2)}</h1>`; } else if (line.startsWith('## ')) { html += `<h2>${line.slice(3)}</h2>`; } else if (line.startsWith('```')) { html += '<pre><code>代码块开始,省略实现</code></pre>'; } else if (line.trim() === '') { html += '<br/>'; } else { html += `<p>${line}</p>`; } } return html; } </script> </body> </html>上面这段代码有一个重点:渲染 SVG 时没有直接使用container.innerHTML,而是通过DOMParser解析。原因是直接插入 HTML 时,浏览器可能对 SVG 的某些标签解析不准确,而且在含<script>标签的情况下会产生可执行风险。
Markdown 部分的转换为了演示做了极简化处理,实际工程中建议使用markdown-it解析,再配合DOMPurify做白名单过滤。可以参考如下思路:
npm install markdown-it dompurify然后在前端代码中引入即可。
4.5 注册插件到 harness
不同版本的 harness 注册方式不同,但大体流程一致:
- 将
render-plugin目录放到 harness 的插件目录下。 - 在 harness 配置文件中启用该插件。
- 重启 harness 服务。
以常见配置为例:
# config.yaml plugins: - name: render-plugin path: ./plugins/render-plugin enabled: true如果你的 harness 支持动态加载,也可以通过命令注册:
harness plugin add ./plugins/render-plugin注册成功后,你可以在 harness 的输出面板中看到渲染后的内容,而不是纯文本源码。
4.6 运行与验证
我们来模拟一次完整调用。
假设模型输出了下面的 SVG 内容:
<svg width="300" height="200"> <!-- 画一个蓝色矩形和一个橙色圆 --> <rect x="20" y="20" width="100" height="80" fill="#4A90D9" /> <circle cx="220" cy="60" r="40" fill="#F5A623" /> </svg>插件detect_content_type检测到<svg字符串,返回svg类型。前端模板把内容直接解析成 SVG 图形,你会看到浏览器里出现一个蓝色矩形和一个橙色圆形。
再测试图表能力,假设模型输出:
{ "data": { "categories": ["1月", "2月", "3月"], "series": [ { "name": "销量", "type": "bar", "data": [120, 200, 150] }, { "name": "利润", "type": "line", "data": [30, 80, 45] } ] }, "options": { "title": "月度销售与利润" } }插件会将 JSON 转换为 ECharts option,并渲染出柱线混合图。
最后测试 Markdown,模型输出:
## 结论 - 本月销量上涨 20% - 主要增长来自华东区域插件会把内容渲染为带标题和列表的 HTML 页面。
5. 常见问题与排查思路
5.1 SVG 显示空白
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| SVG 区域空白 | 模型输出的 SVG 缺少宽高属性 | 在 wrap_svg 中补充默认宽高 |
| 图形显示不全 | SVG 坐标超出视口范围 | 包裹外层时设置 viewBox |
| 标签显示为文字 | 后端未识别为 svg 类型 | 检查 detect_content_type 的匹配规则 |
最稳妥的做法是在前端渲染时检查svg.getAttribute('width'),为空则统一设置默认值。同时给svg添加viewBox属性,可以避免坐标越界问题。
5.2 图表渲染提示 echarts is not defined
这个报错通常是echarts.min.js没加载成功。重点排查:
- 确认
assets目录下确实有echarts.min.js文件。 - 检查 HTML 模板中
<script>标签的路径是否正确。 - 如果 harness 使用沙箱环境,可能需要把 ECharts 资源打包进模板,而不是外部引用。
- 确认资源是否有跨域限制。
如果是内网环境,建议把 ECharts 下载到本地,不要在模板中引用 CDN 链接。
5.3 Markdown 渲染出现乱码或样式异常
先看两件事:
第一,模板文件是否设置了<meta charset="UTF-8" />。如果没有,中文内容很可能出现乱码。
第二,转换函数是否正确处理了符号转义。模型输出的 Markdown 里可能包含<、>、&等字符,如果不转义,会被浏览器误解析为 HTML 标签。
5.4 插件注册后不生效
优先检查插件清单:
harness plugin list看插件是否处于 enabled 状态。如果显示加载失败,查看 harness 日志中的具体错误信息。常见原因包括:
manifest.json格式错误。- 入口文件路径写错。
- Python 依赖缺失。
- 插件目录权限不足。
5.5 模型输出的图表 JSON 解析失败
这个问题很常见。模型的输出可能带有额外的说明文字,例如:
这是图表配置: { "series": [...] }这种情况下直接json.loads会失败。解决办法是提取代码块:
import re def extract_json_from_text(text: str) -> str: pattern = r"```(?:json)?\s*(.*?)\s*```" match = re.search(pattern, text, re.DOTALL) if match: return match.group(1) return text在使用json.loads之前,先尝试正则提取代码块内容。
6. 最佳实践与工程建议
6.1 安全第一:不要在渲染层信任模型输出
这是渲染插件最重要的原则。
模型是一个概率系统,它输出的内容不一定可信任。即使模型本身经过安全对齐,也不能保证输出内容不包含恶意构造的代码。渲染层应该对所有内容做隔离和过滤:
- SVG 不要直接
innerHTML插入,优先使用 DOMParser 解析。 - Markdown 转换后必须经过 HTML 白名单过滤。
- 如果要在 iframe 中预览,加上
sandbox属性。 - 对于包含外部 URL 的图片、链接,设置
referrerpolicy="no-referrer"。
一个比较稳妥的预览方案:
<iframe sandbox="allow-scripts" src="/preview-sandbox.html"></iframe>在沙箱 iframe 中渲染模型输出,即使出现异常脚本,也不会影响 host 页面。
6.2 类型识别要做“渐进式判断”
模型输出的格式千变万化,不要只依赖单一规则。
推荐判断顺序:
- 是否包含代码块标记?语言是 json、svg、html 还是 markdown?
- 去掉首尾空白后,是否以
{开头?能否解析为 JSON? - 是否包含
<svg、<rect、<circle等 SVG 特征标签? - 是否包含 Markdown 标题、列表、表格等语法特征?
识别越精准,误判越少。建议为每种类型增加一个置信度评分,而不是简单的真/假判断。
6.3 把渲染逻辑收敛到单一入口
插件应该有一个统一入口,所有渲染请求都走这个入口。不要在一个代码文件中散落多个可被调用的方法。这样既便于维护,也方便 harness 框架做统一拦截和审计。
看一个对比:
不推荐的做法:
def render_svg_content(content): pass def render_charts_content(content): pass def render_markdown_content(content): pass推荐的做法:
def render(content): # 统一入口 pass统一入口的好处是:你可以封装日志、统计、权限检查、内容审计等横切逻辑,而不需要修改每个渲染函数。
6.4 前端资源尽量本地化
无论是 ECharts、markdown-it 还是 DOMPurify,只要是生产环境使用的渲染资源,都建议下载到插件目录中本地引用。
原因很简单:
- 内网环境往往无法访问公网 CDN。
- 依赖 CDN 会让渲染结果受网络波动影响。
- 公网资源存在被替换或投毒的风险,尤其是供应链攻击。
- 版本固定,避免 CDN 资源意外更新导致兼容问题。
6.5 为渲染结果增加缓存
如果模型输出的内容比较大,比如一个复杂的 SVG 图形,渲染前建议做内容摘要,相同内容直接复用上一次渲染结果。
cache_key = hashlib.md5(content.encode("utf-8")).hexdigest() if cache_key in cache: return cache[cache_key]缓存可以放在内存中,也可以落在磁盘上。对于频繁调试的用户来说,这个优化能明显提升体验。
6.6 完善日志
日志是排查问题的关键。
插件运行时要记录:
- 输入内容的类型判断结果。
- 渲染耗时。
- 渲染失败的原因。
- 是否发生了安全拦截。
建议使用 Python 标准库的logging,生产环境也可以接入更完整的日志系统。
import logging logger = logging.getLogger("render-plugin") logger.info("content_type=%s, length=%d", content_type, len(content))6.7 不要为了“大而全”牺牲稳定性
渲染插件不是功能越多越好。每一个新增的渲染类型,都意味着新的解析逻辑、新的安全风险、新的兼容性问题。
建议第一版只做最核心的三种类型:SVG、图表、Markdown。等稳定运行一段时间,再逐步增加流程图、数学公式、表格透视等高级能力。
7. 总结与后续扩展
这次渲染插件的核心价值,是把 DeepSeek harness 的输出从“人类阅读的源码”升级为“直接可用的可视化结果”。整个方案聚焦在三个层面:内容类型识别、安全渲染执行、前端可视化展示。
本文完整实现了一个最小可用的渲染插件,覆盖了:
- 插件清单
manifest.json的声明方式。 - Python 入口
renderer.py的类型识别与内容包装。 - 前端模板
renderer.html的 SVG / 图表 / Markdown 渲染逻辑。 - 常见问题排查思路。
- 安全与工程最佳实践。
接下来的扩展方向可以围绕几个方面:引入成熟 Markdown 解析库和完善 HTML 白名单过滤;增加 ECharts 配置的智能纠错能力;支持更多图表类型和主题定制;把渲染结果导出为 PNG 或 PDF。如果你正在做 Agent 工具链或自动化报告系统,这个渲染插件可以直接作为基础模块迭代下去。拿起代码跑一个示例,剩下的交给你的业务场景来驱动。