更新 “本周有趣项目,Mermaid 流程图生成器!” — V2 及更多…
发布: (2025年12月7日 GMT+8 17:29)
7 min read
原文: Dev.to
Source: Dev.to
介绍
在之前的文章中,我演示了如何使用 Ollama 和本地 LLM 生成 Mermaid 图表(因为我实在受不了那些要求订阅付费的网站),于是决定对我的应用进行增强和更新。原因有两个:
- 我的第一个应用把要使用的 LLM 硬编码进了代码。
- 该应用不是迭代式的。
关键增强
本次增强的核心是抽象化 LLM 的选择,并创建一个干净、可重复的工作流。
动态 LLM 选择(解决硬编码问题)
不要只使用单一的硬编码模型,应用应当能够动态发现并使用本地 Ollama 实例中可用的任何模型。
- 实现模型发现 – 向 Ollama 的
/api/tags接口(http://localhost:11434/api/tags)发送请求。该接口会返回本地已安装模型的 JSON 列表。 - 创建选择界面 – CLI – 将发现的模型列表按编号展示,并提示用户输入编号进行选择。
- 创建选择界面 – GUI – 使用下拉框或单选按钮组(例如 Streamlit),将检索到的模型名称填入。
- 传递模型名称 – 选中的模型名称(例如
llama3:8b-instruct-q4_0)必须作为变量放入后续所有对/api/chat接口的请求负载中。
迭代工作流与错误处理(解决非迭代性)
非迭代式的应用每生成一个图表都需要重新启动,体验极差。迭代性不仅仅是循环,更是要在同一会话中优雅地处理成功与失败。
- 主执行循环 – 将核心逻辑(
提示用户 → 调用 LLM → 生成图片)包装在while True循环中,只有用户明确选择退出时才跳出。 - 会话状态(GUI) – 使用像 Streamlit 这样的 GUI 框架时,利用
st.session_state在按钮点击和页面重新渲染之间保持生成的 Mermaid 代码和图片路径。 - 输入校验 – 检查用户的提示是否为空。
- 连接检查 – 在尝试获取模型或生成代码之前,先确认 Ollama 服务器已启动。
- 文件处理安全 – 由于会为
mmdc创建临时文件,确保清理逻辑可调试(例如,仅在DEBUG_MODE关闭时删除临时文件)。
V3 想法(未完待续)
| 增强项 | 描述 | 价值主张 |
|---|---|---|
| 代码审查/修复模式 | 若 mmdc 因语法错误渲染失败,自动将 Mermaid 代码 以及 mmdc 错误日志发送回 LLM(使用特定系统提示),请求其修复语法。 | 减少用户挫败感,自动修复常见的 LLM 引发的语法错误。 |
| 图表历史 | 将生成的文本提示、输出代码以及对应的图片文件路径存入本地简易数据库(如 SQLite)或结构化文件(如 JSON/YAML)。 | 让用户能够轻松回顾并复用过去的图表,无需重新生成。 |
| 输出格式选项 | 增加除 PNG 之外的输出格式,如 SVG(更好缩放)或 PDF。 | 为需要高质量矢量图的用户提供更高的灵活性。 |
| 持久化设置 | 将上次使用的 LLM 模型保存到配置文件(如 config.json)。 | 启动时自动选中用户偏好的模型,节省时间。 |
代码与实现
1 — 控制台版本
创建虚拟 Python 环境并安装所需依赖:
pip install --upgrade pip
pip install requests
npm install -g @mermaid-js/mermaid-cli
代码 🧑💻
# app_V3.py
import subprocess
import os
import requests
import json
import re
import glob
import sys
import time
from pathlib import Path
DEBUG_MODE = True
OLLAMA_BASE_URL = "http://localhost:11434"
OLLAMA_CHAT_URL = f"{OLLAMA_BASE_URL}/api/chat"
OLLAMA_TAGS_URL = f"{OLLAMA_BASE_URL}/api/tags"
INPUT_DIR = Path("./input")
OUTPUT_DIR = Path("./output")
def check_mmdc_installed():
"""Checks if 'mmdc' is installed."""
try:
subprocess.run(['mmdc', '--version'], check=True, capture_output=True, timeout=5)
return True
except (FileNotFoundError, subprocess.TimeoutExpired, subprocess.CalledProcessError):
print("Error: Mermaid CLI (mmdc) not found or misconfigured.")
print("Try: npm install -g @mermaid-js/mermaid-cli")
return False
# MODEL SELECTION
def get_installed_models():
"""Fetches locally installed Ollama models."""
try:
response = requests.get(OLLAMA_TAGS_URL, timeout=5)
response.raise_for_status()
return sorted([m['name'] for m in response.json().get('models', [])])
except:
return []
def select_model_interactive():
"""Interactive menu to choose a model."""
print("\n--- Ollama Model Selection ---")
models = get_installed_models()
if not models:
return input("No models found. Enter model name manually (e.g., llama3): ").strip() or "llama3"
for idx, model in enumerate(models, 1):
print(f"{idx}. {model}")
while True:
choice = input(f"\nSelect a model (1-{len(models)}) or type custom name: ").strip()
if choice.isdigit() and 1 <= int(choice) <= len(models):
return models[int(choice) - 1]
elif choice:
return choice
def clean_mermaid_code(code_string):
"""Clean common LLM formatting errors from Mermaid code."""
cleaned = code_string.replace(u'\xa0', ' ').replace(u'\u200b', '')
cleaned = cleaned.replace("```mermaid", "").replace("```", "")
cleaned = re.sub(r'[ \t\r\f\v]+', ' ', cleaned)
lines = cleaned.splitlines()
rebuilt = []
for line in lines:
s_line = line.strip()
if s_line:
rebuilt.append(s_line)
final = '\n'.join(rebuilt)
final = re.sub(r'(\])([A-Za-z0-9])', r'\1\n\2', final)
return final.strip()
def generate_mermaid_code(user_prompt, model_name):
"""Calls Ollama to generate the code."""
system_msg = (
"You are a Mermaid Diagram Generator. Output ONLY valid Mermaid code. "
"Do not include explanations. Start with 'graph TD' or 'flowchart LR'. "
"Use simple ASCII characters for node IDs."
)
payload = {
"model": model_name,
"messages": [{"role": "system", "content": system_msg}, {"role": "user", "content": user_prompt}],
"stream": False,
"options": {"temperature": 0.1}
}
try:
print(f"Thinking ({model_name})...")
response = requests.post(OLLAMA_CHAT_URL, json=payload, timeout=60)
response.raise_for_status()
content = response.json().get("message", {}).get("content", "").strip()
match = re.search(r"```mermaid\\n(.*?)```", content, re.DOTALL)
if match:
return clean_mermaid_code(match.group(1))
else:
return clean_mermaid_code(content)
except Exception as e:
print(f"Error generating Mermaid code: {e}")
return None
def render_mermaid_to_png(mermaid_code, output_path):
"""Uses mmdc to render Mermaid code to PNG."""
temp_mmd = INPUT_DIR / "temp.mmd"
temp_mmd.write_text(mermaid_code, encoding="utf-8")
try:
subprocess.run(
["mmdc", "-i", str(temp_mmd), "-o", str(output_path)],
check=True,
capture_output=True,
timeout=30,
)
return True
except subprocess.CalledProcessError as e:
print(f"mmdc error: {e.stderr.decode()}")
return False
finally:
if not DEBUG_MODE:
try:
temp_mmd.unlink()
except FileNotFoundError:
pass
def main():
if not check_mmdc_installed():
sys.exit(1)
INPUT_DIR.mkdir(exist_ok=True)
OUTPUT_DIR.mkdir(exist_ok=True)
model_name = select_model_interactive()
while True:
user_prompt = input("\nEnter a description for your diagram (or 'quit' to exit): ").strip()
if user_prompt.lower() in {"quit", "exit"}:
print("Goodbye!")
break
if not user_prompt:
print("Prompt cannot be empty.")
continue
mermaid_code = generate_mermaid_code(user_prompt, model_name)
if not mermaid_code:
continue
timestamp = int(time.time())
output_file = OUTPUT_DIR / f"diagram_{timestamp}.png"
if render_mermaid_to_png(mermaid_code, output_file):
print(f"Diagram saved to: {output_file}")
else:
print("Failed to render diagram.")
if __name__ == "__main__":
main()
如果需要 GUI 版本(例如 Streamlit),或者想要实现上面列出的 V3 想法,完全可以在此基础上进行改造。