一个在Flask Web应用中安全渲染LLM输出的本地图片路径的方法
1. 问题背景:浏览器安全模型与本地文件访问限制
在构建基于大型语言模型(LLM
)的 Web
应用时,一个常见的场景是 LLM
的输出中包含了指向服务器本地文件系统的图片路径。例如,在一个本地搭建的 RAG
系统中,LLM
可能会根据上下文生成一段包含标准 Markdown
图片语法的回答:
1 |  |
然而,如果将这段 Markdown
直接转换为 HTML
(例如 <img src="D:\project\data\images\package_diagram.png">
),并试图在用户浏览器中渲染,该请求将会失败出现报错:
1 | Not allowed to load local resource: file:///D:/... |
这是因为现代浏览器的安全模型,特别是同源策略(Same-Origin Policy
)和安全沙箱机制,严格禁止网页中的JavaScript代码直接访问用户的本地或远程服务器的任意文件系统。这种限制是保障用户安全的基石。
因此,必须设计一个安全且有效的机制,作为浏览器和服务器文件系统之间的“中间人”,以实现图片的正确渲染。
2. 解决方案架构:建立一个“受信任的中间人”
既然前端浏览器不能直接去服务器硬盘上“拿”图片,我们就需要一个“中间人”来代劳。这个中间人必须是服务器自己,因为服务器有权限访问自己的文件系统。
这个思路引出了一种经典的架构模式:代理服务(Proxy Service
)。
我们的方法论可以分解为以下几个步骤:
约定一个暗号:前端和后端约定一个特殊的
URL
格式,作为请求图片的“暗号”。这个URL
不会暴露真实的文件路径,而是指向我们即将创建的后端服务。创建后端代理端点:在
Flask
中创建一个API
端点(例如/image
)。这个端点是唯一有权访问服务器上特定图片目录的“守门人”。前端发起代理请求:当前端需要显示一张本地图片时,它不直接使用文件路径,而是向这个
/image
端点发起一个请求,并将真实的文件路径作为参数传递。后端验证并服务:后端代理收到请求后,首先进行严格的安全检查,确保请求的路径是合法的。验证通过后,它才去硬盘上读取图片文件。
安全的数据传输:后端不直接发送文件,而是将图片内容编码为
Base64
字符串。这是一种能将二进制数据转换为纯文本的编码方式,非常适合在JSON
中传输。前端动态渲染:前端接收到包含Base64数据的JSON后,用这些数据动态地构建一个
Data URL
,并将其赋给<img>
标签的src
属性,最终将图片显示出来。
这个流程的核心在于,将一个不安全的直接文件访问,转变为一个安全的、受控的 API
调用。
3. 代码实现详解
3.1. 后端 (app.py): 构建安全的文件代理与Base64编码器
核心改动是在Flask应用中增加 /image
路由。
1 | import os |
安全检查:
IMAGE_BASE_DIR
的设置和startswith()
的验证是此端点的安全核心,它将文件访问权限严格限制在预设的目录范围内。Base64编码: 将图片转换为
Base64
格式的Data URL
,允许图片数据作为文本嵌入到JSON
响应中,并能被浏览器直接渲染,避免了前端需要为获取图片而发起额外的HTTP
请求。
3.2. 前端 (index.html): 自定义Markdown渲染与异步加载
前端的核心工作在 appendAiMessage
函数中,通过重写 marked.js
的图片渲染逻辑来实现。
1 | function appendAiMessage(data, fullLog = '') { |
渲染逻辑分离:
HTML
的初始渲染和图片的最终加载是分离的。marked.js
首先创建了包含代理URL
的HTML
结构。异步非阻塞: 图片的加载过程是异步的,不会阻塞用户界面的其他操作。用户可以立即看到文本内容,而图片会在后台加载完成后显示。
3.2.1. 正则表达式详解
在前端代码中,用于判断路径是否为本地绝对路径的正则表达式 /^(?:[a-zA-Z]:[\\\/]|\/)/
是一个关键部分。让我们来详细解析它的工作原理:
^
: 这个符号代表“字符串的开始”。它确保我们只匹配路径的起始部分,而不是路径中间的某个位置。(?: ... )
: 这是一个非捕获组(non-capturing group
)。它的作用是将括号内的多个模式组合成一个单元,但不会像普通括号那样“捕获”匹配到的内容。在这里,它将两种主要的路径格式组合在一起。[a-zA-Z]:[\\\/]
: 这是用来匹配Windows绝对路径的部分。[a-zA-Z]
: 匹配任意一个大写或小写字母(即盘符,如 C, D)。:
: 匹配紧跟在盘符后面的冒号。[\\\/]
: 匹配一个路径分隔符。\ 是对反斜杠 \ 的转义,/ 是正斜杠。这个部分意味着无论是D:\...
还是D:/...
格式的路径都能被识别。|
: 这是一个“或”操作符。它表示匹配左边的模式 或者 右边的模式。/
: 这是用来匹配Unix/Linux绝对路径的部分。所有Unix-like系统的绝对路径都以一个正斜杠 / 开始(例如/home/user/image.png
)。
4. 对LLM的输出引导
最后一步是确保LLM能够持续生成符合我们预期的、包含本地路径的 Markdown
图片语法。这通过在系统提示(System Prompt)中提供清晰的指令和示例来完成。
指令示例:
1 | 图片处理: 如果一个节点的值是文件路径(例如以 .png, .jpg 等结尾),你应该说“它的封装图如下所示:”或类似的话,然后另起一行,使用标准的Markdown语法  来格式化并展示图片。 |
成功在前端渲染 markdown
图片效果展示: