PGFPlotsEdt 大模型应用混合部署方式
探讨通过传统架构、无服务器、端侧推理三种模式以尽可能低的成本提供大模型应用服务。
2025-10-12
缘起:智能编辑
PGFPlotsEdt 是一款 \(\rm\LaTeX\) PGFPlots 代码式统计绘图在线绘制工具,旨以模块化的图形式菜单引导用户快速调配出统计图代码,并通过快速编译机制尽快得到预览统计图以供调试。
尽管图形化菜单为用户提供了便捷的操作方式,但受限于功能适配的复杂性(PGFPlots 文档 长达 500 多页,PGFPlotsEdt 的菜单仅覆盖了其中一部分功能),可通过菜单生成的代码空间相对有限。对于开发者而言,持续将更多功能集成到菜单中不仅工作量巨大,还可能导致界面臃肿、影响易用性。因此,PGFPlotsEdt 也提供了“手动编辑”按钮,允许用户直接灵活地修改代码。然而,为了满足更复杂的定制需求,用户往往需要频繁查阅宏包文档,这使得编写个性化统计图代码依然存在一定门槛和难度。
幸运的是,随着大模型(LLM)技术的持续进步,代码生成迎来了全新的解决方案。代码本身具备高度结构化和明确语义,使其成为用户与大模型高效交互的理想载体。当前主流大模型已能够生成高质量、可编译的代码,基本满足用户的实际需求。因此,PGFPlotsEdt 在 3.5 版本(2024-05-05)首次推出了“智能编辑”功能,允许用户通过自然语言提示,引导大模型自动修改和完善代码。经过多个版本的迭代升级,如今的“智能编辑”功能已融合检索增强生成(RAG)技术,能够结合相关文档内容,进一步提升大模型生成 PGFPlots 代码的准确性和实用性。
原理:RAG 工作流
PGFPlotsEdt 4.5 版本 具体的 RAG 工作流大致如下:
graph LR
A1[/User Query/] --> B;
A2[/Current Code/] --> D;
B{Transformation?};
B -- No (Direct Input) --> C1[Final Query];
B -- Yes (Transform) --> T[\LLM/];
T --> |Transformed Query| C1;
C1 -->|Text Input| C2[/Embedding Model\];
C2 --> |Query Vector| E[(Vector DB)];
E --> |Vector Search| F@{ shape: docs, label: "Related chunks"};
F --> D[\LLM/];
C1 --> D;
D --> G[Final Code Output];
style A1 fill:#f9f,stroke:#333,stroke-width:2px
style A2 fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#ccf,stroke:#333
style T fill:#9f9,stroke:#333
style C1 fill:#f0c0ff,stroke:#333
style C2 fill:#9f9,stroke:#333
style E fill:#fcc,stroke:#333
style F fill:#ffffcc,stroke:#333
style D fill:#9f9,stroke:#333
style G fill:#f9f,stroke:#333,stroke-width:2px
RAG 部分的源代码实现于此,简单来讲:
- 问题改写 相比于直接使用 LLM,为了实现与文档最好的匹配效果,这里先对用户问题进行改写(Query Transformation),由于后续的文档库片段来自 PGFPlots 英文 \(\rm\LaTeX\) 源代码,所以这里会要求大模型先翻译成英文,并且将一些 Unicode 数学符号转换成 \(\rm\LaTeX\) 命令,如果不包含非 ASCII 字符则不需要转换,所以如果进入问题改写分支就会一些花费额外的时间(+可选的较长首token时间);
- 检索阶段 然后进入检索阶段,这里统一使用 BAAI/bge-small-en-v1.5 这个只有 33.4M 参数量的模型生成 384 维嵌入向量,通过余弦相似度在向量数据库中匹配文档块,这里的文档切分算法主要以分节为界,每个块不超过 500 字符(后续应该可以通过切分更为完整的 \(\rm\LaTeX\) 环境块进行优化),捞出 Top 3 的文档块(必要时移除余弦相似度小于 0.75 的块),由于文档空间并不大(共 5054 个嵌入向量),所以这一步目前的时间和空间开销都不是很大(+较短首token时间);
- 生成阶段 最后进入生成阶段,将改写问题(或者是用户原始问题提升内容忠实度?但是这里用到的 Llama 3 对于英文更强一些,Llama 3.1 对多语种的支持会更好,当然跟改写阶段用的是同一个大模型罢了)、文档块文件名和内容(遵循 llama-index 的通用做法)、当前的代码,要求 LLM 根据用户问题对代码进行改善并不进行任何解释。这里使用 Llama 3-8B 大模型(后来的 Llama 3.1-8B 测试后并没有达到这个场景的预期效果,笔者认为是在同参数量规模下塞入更多的功能可能会减少部分功能的能力),因为参数量足够少并且 MLC LLM 框架、Cloudflare Serverless Workers AI 适配较好,小参数量本地推理开销少;如果是主站会使用 GLM-4.6,使用现代化一些的大模型可以有更好的效果。在这个阶段,大模型的流式输出结果会经过代码过滤器传到前端输出。
该 RAG 流程相较于直接询问 LLM,可以提升生成内容的准确性,补充大模型在 PGFPlots 预训练语料的不足;相较于通过微调(fine-tuning)LLM 的方式,可以更为方便且更模块化地提升生成的内容的效果,也可以有更多公共资源可以选择(微调后的模型一般需要自行部署,而通用大模型通常公网有很多的服务商进行服务)。当然 PGFPlotsEdt 也提供了实验性的微调大模型 Llama-3-8B-Instruct-pgfplots-finetune,可以在本地 MLC LLM 框架中加载试用。
部署:减少开销
但是,转型到大模型应用后,由于大模型需要大量的算力,对于一个个人非商业项目而言,这种算力带来的成本是无法忽略的。因此,PGFPlotsEdt 将尽可能使用低价甚至免费的多种部署方式,提供优质服务的同时减少不必要的个人开销。
对于 PGFPlotsEdt 这套 RAG 工作流而言,需要三个高耗费资源:向量数据库、嵌入模型服务和大模型服务。对于这些服务,PGFPlotsEdt 对于不同的访问途径分为三套不同的资源及系统架构进行服务:
block
columns 3
block:group1:1
columns 3
pg[("PostgreSQL")] embed[/"text-embeddings-inference"\] glm[\"Zhipu API"/]
end
block:group2:1
columns 3
vec[("Vectorize")] cfe[/"Cloudflare Embedding"\] cfl[\"Cloudflare Llama 3"/]
end
block:group3:1
columns 3
inmem[("in-mem")] he[/"Huggingface Embedding Model"\] mlc[\"MLC LLM"/]
end
li["llama-index"] wai["Workers AI"] li2["llama-index"]
gunicorn Workers flask
nginx ghp["GitHub Pages"] ls["live server"]
b["logcreative.tech"] c["logcreative.github.io"] d["local server"]
a["PGFPlotsEdt frontend"]:3
国内:传统架构
国内站 https://logcreative.tech/PGFPlotsEdt 使用传统的服务架构。
基础架构 使用 2 台腾讯云轻量服务器、云数据库 PostgreSQL 作为基础架构。PostgreSQL 启用 pgvector 插件后可以作为向量数据库使用;基于 Rust 的 text-embeddings-inference 嵌入模型推理工具可以更好地释放 CPU 推理性能,并提供 OpenAI 兼容接口;使用智谱 GLM-4.6 模型 OpenAI 兼容接口作为国内合规大模型服务。
智能体框架 使用 llama-index 这个更偏向于 RAG 的智能体框架,可以通过 PGVectorStore 适配 PostgreSQL 数据库,通过 OpenAILikeEmbedding 连接 OpenAI 兼容接口的嵌入模型,并通过 OpenAILike 连接 OpenAI 兼容接口的大模型。(不能省略 Like,否则只能连接 OpenAI 官方模型)
中间件 使用 gunicorn 对 Flask 应用进行生产级优化,该框架只能在 Linux 或 MacOS 上使用;当然现在更流行的 Python 中间件是 uvicorn,后续可能会迁移到该框架上。该层还会通过预编译头缓存加速编译准备阶段的时间,基于相同的初始 \(\rm\LaTeX\) 模板种子,用户的起始编译代码编译头大致相同或有缓存,对于多用户的服务会有更快的最初编译响应。
负载均衡 这里使用 nginx 作为负载均衡中间件,前端直接使用 nginx 进行静态文件服务,后端使用 proxy_pass 代理到对应的服务上。这里对 \(\rm\LaTeX\) 编译服务和大模型服务的处理是不同的,一个示例的配置文件片段如下:
location /compile {
proxy_pass http://pgfplotsedt-backend/compile;
}
location /llm {
proxy_buffering off; # to make it streamable
proxy_read_timeout 3m;
proxy_pass http://localhost:5678/llm;
}
/compile对应的编译服务不需要流式返回,但是编译需要服务器计算资源,这里采用两个云服务器通过 docker compose 同时部署上述的服务,暴露出端口后,nginx 进行负载均衡;/llm对应的大模型服务需要流式返回数据,Flask 文档中也提示过这样的话语:但请注意,某些 WSGI 中间件可能会破坏流式传输。这里也不例外,对于 nginx 来说,需要关闭proxy_buffering功能,否则 nginx 会尝试缓存这些流式响应然后再一起返回,减少了响应性。nginx 这一层会默认将响应类型变更为Application/octet-stream,保险起见并为了方便本地调试,Flask 层也应当将流式的部分响应类型变更为Application/octet-stream。
最终通过 logcreative.tech 这个域名服务出去。这套传统的部署方式符合大多数软件服务的成熟服务方式,也符合相关监管要求。
国际:无服务器
国际站 https://logcreative.github.io/PGFPlotsEdt 使用无服务器(serverless)架构。
基础架构 使用 Cloudflare Workers、Cloudflare Vectorize 作为基础架构。Vectorize 提供的向量数据库可以很好地集成 Cloudflare Workers 中,但是导入数据需要使用 wrangler 命令 进行,PGFPlotsEdt 使用上文通过 llama-index 导入 PostgreSQL 中的向量化数据处理后进行导入,详见导入脚本;嵌入模型和大模型均由 Workers AI 提供,每日有 10k 的免费神经元限额。
智能体框架 智能体框架直接采用适用于 Cloudflare 无服务器环境的 Workers AI 原生重写。
这种方式部署出来的服务,会有跨域问题(CORS),即由于域名不同(实际上该问题需要协议、域名、端口都相同), PGFPlotsEdt 前端无法正常访问 Cloudflare 服务,这里通过对后端响应添加相关响应头解决。(如果是传统架构,还可以使用 nginx 解决这个问题。)
export default {
async fetch(request, env, ctx) {
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type,Authorization'
};
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders });
}
if (request.method === "POST") {
// ...
return new Response(stream, {
status: 200,
headers: {
...corsHeaders,
'Content-Type': 'application/octet-stream'
}
});
} else {
return new Response("...", { status: 200, headers: corsHeaders });
}
}
}
部署平台 直接使用 GitHub Pages 作为前端部署平台,编译服务直接使用 LaTeXOnline 提供 \(\rm\LaTeX\) 的编译服务,由于没有编译相关的优化,编译上可能会增加一倍的时间,具体的评测信息可以参见 pgfplots-benchmark。
最终就可以直接通过 logcreative.github.io 部署出去。这套无服务器的部署方案可以尽可能使用国际网络生态中的免费或低价资源进行部署,减少了额外的部署开销。
本地:端侧推理
本地站 http://localhost:5678 使用简单的 Flask 应用架构。
基础架构 只使用用户自己的计算机。基于 llama-index 提供的 SimpleVectorStore 内存向量数据库每次启动时进行向量化索引(由于文件块不多,所以这种开销是可以接受的);嵌入模型使用 SentenceTransfomer 基于 CPU 进行推理;Llama3-8b 大模型使用 MLC LLM 框架在各种异构 GPU 上进行大模型推理。
智能体框架 使用 llama-index 作为智能体框架,但是需要连接本地的向量库、嵌入模型、大模型。其中大模型的部分由于其并没有提供对 MLC LLM 框架的原生支持,所以需要以 CustomLLM 基类自行实现:
from mlc_llm import MLCEngine
from llama_index.core.llms import (
CustomLLM,
CompletionResponse,
CompletionResponseGen,
LLMMetadata,
)
from llama_index.core.llms.callbacks import llm_completion_callback
engine = MLCEngine(model)
class MLCLLM(CustomLLM):
context_window: int = engine.max_input_sequence_length
num_output: int = engine.engine_config.max_num_sequence
model_name: str = model
@property
def metadata(self) -> LLMMetadata:
"""Get LLM metadata."""
return LLMMetadata(
context_window=self.context_window,
num_output=self.num_output,
model_name=self.model_name,
)
@llm_completion_callback()
def complete(self, prompt: str, **kwargs: Any) -> CompletionResponse:
response = engine.chat.completions.create(
messages=[{"role": "user", "content": prompt}],
model=model,
stream=False,
)
text = response.choices[0].message.content
return CompletionResponse(text=text)
@llm_completion_callback()
def stream_complete(
self, prompt: str, **kwargs: Any
) -> CompletionResponseGen:
full_response = ""
for response in engine.chat.completions.create(
messages=[{"role": "user", "content": prompt}],
model=model,
stream=True,
):
delta_response = response.choices[0].delta.content
full_response += delta_response
yield CompletionResponse(text=full_response, delta=delta_response)
部署 如果必须使用大模型,就必须启动 Flask LLM 服务器进行启动;否则如果需要编译优化,启动普通 Flask 服务器即可;当然,也可以直接通过 index.html 或通过 live server 启动前端界面也可以进行操作。(当没有部署大模型时,会尝试使用前一节的 Serverless LLM 进行服务。)这主要受益于 PGFPlotsEdt 在架构上的分层设计,只将部分层赋予本地仍然可以进行服务。
graph LR
subgraph gunicorn-deploy.py
direction LR
A[Production Deployment]
subgraph ppedt_server_llm.py
direction LR
B[LLM & Agent Pipeline]
subgraph ppedt_server.py
direction LR
C[Server Interface]
end
end
end
A --> B
B --> C
最终就可以直接通过 http://localhost:5678 本地访问,这也是最早实现的一种方式,本地部署可以尽可能地保证数据隐私。
总结:降本增效
PGFPlotsEdt 采用多种模式实现了对于这一场景 RAG 的部署实现,降低成本的同时,提升了生成效果。最近,有利用多模态大模型将示意图转换成 \(\rm\LaTeX\) TikZ(PGFPlots 的底层宏包)代码的工作 DeTikZfy;也有利用多模态大模型生成统计图的工作,其数据集包含了使用 \(\rm\LaTeX\) 绘制的图表。如何将多模态大模型集成进 PGFPlotsEdt 的场景中也是一个未来值得探讨的问题。
