1. 为什么我要用 Streamlit + MiniMax-M2 重写 Markdown 工作流
写技术文档的人大概都有过这种体验:Markdown 语法本身不复杂,真正耗时间的是「把想法变成结构清晰、表达准确的段落」。我平时写教程、写项目 README、写周报,经常卡在润色和续写上,于是想做一个自己能用的智能 Markdown 编辑器:左边写、右边实时预览,选中一段就能让模型帮我润色、续写、整理成表格。
这个编辑器适合三类人:一是经常写技术博客或文档、想减少重复润色的开发者;二是想学 Streamlit 快速搭 AI 小工具的人;三是手里有多个模型 API、希望用统一入口调用的同学。核心检索词就是 MiniMax-M2、Markdown 编辑器、Streamlit、Python 和统一 API 接入。
MiniMax-M2 是 MiniMax 开源的新一代文本大模型,采用 MoE 架构,总参数规模很大但激活参数少,推理速度和成本控制得不错,指令遵循和代码理解能力在开源模型里属于第一梯队。用它来做 Markdown 润色、续写、格式整理这类任务,响应快、输出稳定,特别适合嵌进编辑器这种需要「边写边等」的交互场景。
我这次没有直接对接各家模型的原始接口,而是走 TaoToken 的统一 API 通道。原因很实际:编辑器里我可能今天用 MiniMax-M2,明天想换成别的模型对比效果,如果每换一个模型就改一遍鉴权、改一遍请求体,维护成本太高。TaoToken 提供 OpenAI 兼容的调用方式,Base URL 和 Key 配一次,模型 ID 换一下就行,Streamlit 里的客户端代码几乎不用动。
下面我会从环境准备讲到完整可运行的 Streamlit 页面代码,再到本地启动验证和常见报错排查。你跟着做,最后能拿到一个能跑起来的智能 Markdown 编辑器,支持流式输出、润色、续写和表格生成。
2. TaoToken 统一 API 前置准备与 Key 配置
在写代码之前,先把「通道」打通。这一步不复杂,但配置项要写对,否则后面调试会浪费很多时间。
2.1 获取 API Key 与确认 Base URL
TaoToken 的 API 入口是https://taotoken.net/api,它兼容 OpenAI 的/v1/chat/completions调用格式。你需要先在控制台创建一个 API Key,然后把它放进环境变量,不要硬编码在代码里。
我建议在项目根目录建一个.env文件,内容如下:
TAOTOKEN_API_KEY=sk-你的实际Key TAOTOKEN_BASE_URL=https://taotoken.net/api/v1 TAOTOKEN_MODEL=MiniMax-M2这里有个细节要注意:Base URL 我写的是https://taotoken.net/api/v1,因为 OpenAI 兼容客户端通常会在后面拼/chat/completions。如果你的客户端库要求 Base URL 不带/v1,那就改成https://taotoken.net/api,具体以你用的 SDK 文档为准。我下面用requests手写请求,所以会显式拼完整路径。
注意:Key 只放在服务端环境变量或本地
.env里,不要提交到 Git,也不要在前端代码里暴露。Streamlit 应用如果部署到公网,务必用st.secrets或服务器环境变量管理。
2.2 三件套对照表:Base URL、Key、Model ID
不管你是用 requests、OpenAI SDK 还是其他兼容库,接入任何模型都离不开这三件套。我整理成表格,方便你对照检查:
| 配置项 | 值 | 说明 |
|---|---|---|
| Base URL | https://taotoken.net/api/v1 | OpenAI 兼容入口,请求路径为/chat/completions |
| API Key | 控制台生成的sk-开头字符串 | 放在环境变量TAOTOKEN_API_KEY |
| Model ID | MiniMax-M2 | 请求体里的model字段 |
如果你后面想换成别的模型,只改 Model ID 即可,Base URL 和 Key 不用动。这就是统一通道的价值:编辑器代码里只认「三件套」,不认具体厂商。
2.3 安装依赖与项目结构
创建项目目录并安装依赖:
mkdir smart-markdown-editor cd smart-markdown-editor python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install streamlit requests python-dotenv项目结构建议这样组织,后面代码按这个结构放:
smart-markdown-editor/ ├── .env ├── app.py ├── llm_client.py └── requirements.txtllm_client.py负责封装 TaoToken 调用,app.py负责 Streamlit 界面。这样拆开的好处是:以后换界面框架,客户端逻辑可以复用;换模型,界面代码不用动。
3. 可复制的 Streamlit 页面与 TaoToken 配置片段
这一节是全文的核心,我会给出完整可运行的代码。你可以直接复制,改一下.env里的 Key 就能跑。
3.1 封装 TaoToken 客户端(llm_client.py)
先写客户端。它要支持普通调用和流式调用两种模式,因为编辑器里润色适合一次性返回,续写适合流式输出让用户看到「正在打字」的效果。
import os import json import requests from dotenv import load_dotenv load_dotenv() class TaoTokenClient: def __init__(self): self.api_key = os.getenv("TAOTOKEN_API_KEY") self.base_url = os.getenv("TAOTOKEN_BASE_URL", "https://taotoken.net/api/v1") self.model = os.getenv("TAOTOKEN_MODEL", "MiniMax-M2") if not self.api_key: raise ValueError("TAOTOKEN_API_KEY 未设置,请检查 .env 文件") def _headers(self): return { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } def chat(self, messages, temperature=0.7, max_tokens=2000): payload = { "model": self.model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, "stream": False, } resp = requests.post( f"{self.base_url}/chat/completions", headers=self._headers(), json=payload, timeout=60, ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] def chat_stream(self, messages, temperature=0.7, max_tokens=2000): payload = { "model": self.model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens, "stream": True, } resp = requests.post( f"{self.base_url}/chat/completions", headers=self._headers(), json=payload, timeout=60, stream=True, ) resp.raise_for_status() for line in resp.iter_lines(): if not line: continue line = line.decode("utf-8") if line.startswith("data: "): chunk = line[6:] if chunk.strip() == "[DONE]": break try: obj = json.loads(chunk) delta = obj["choices"][0]["delta"].get("content", "") if delta: yield delta except json.JSONDecodeError: continue这段代码里有两个容易踩的点。第一,stream=True时iter_lines返回的是字节,要decode。第二,SSE 数据以data:开头,[DONE]是结束标记,解析时要跳过空行。我实测下来,MiniMax-M2 通过 TaoToken 返回的流式格式和 OpenAI 一致,按上面处理就行。
3.2 Streamlit 主界面(app.py)
界面部分用左右两栏:左边编辑,右边预览,侧边栏放 AI 操作按钮。
import streamlit as st from llm_client import TaoTokenClient st.set_page_config(page_title="智能 Markdown 编辑器", layout="wide") @st.cache_resource def get_client(): return TaoTokenClient() client = get_client() if "content" not in st.session_state: st.session_state.content = "# 新文档\n\n在这里开始写你的 Markdown..." def polish(text): messages = [ {"role": "system", "content": "你是专业的中文技术文档编辑,只输出润色后的 Markdown 正文,不要解释。"}, {"role": "user", "content": f"请润色以下 Markdown,保持格式不变:\n\n{text}"}, ] return client.chat(messages, temperature=0.3) def continue_write(text): messages = [ {"role": "system", "content": "你是技术写作助手,续写要自然连贯,只输出续写内容。"}, {"role": "user", "content": f"根据以下内容续写一段:\n\n{text}"}, ] return client.chat(messages, temperature=0.7) st.title("智能 Markdown 编辑器") with st.sidebar: st.header("AI 操作") if st.button("润色当前文档"): with st.spinner("润色中..."): st.session_state.content = polish(st.session_state.content) st.rerun() if st.button("续写一段"): with st.spinner("续写中..."): addition = continue_write(st.session_state.content) st.session_state.content += "\n\n" + addition st.rerun() col1, col2 = st.columns(2) with col1: st.subheader("编辑区") edited = st.text_area( "Markdown 内容", value=st.session_state.content, height=500, label_visibility="collapsed", ) if edited != st.session_state.content: st.session_state.content = edited with col2: st.subheader("预览区") st.markdown(st.session_state.content, unsafe_allow_html=True)跑起来之后,你在左边输入内容,右边会实时渲染。点侧边栏的「润色当前文档」,模型会返回润色后的版本并替换编辑区内容。
3.3 加入流式续写效果
上面的续写是一次性返回,等待期间界面没反馈。改成流式会更像「AI 在打字」。把续写按钮的逻辑换成下面这样:
if st.button("流式续写"): placeholder = st.empty() buffer = st.session_state.content + "\n\n" messages = [ {"role": "system", "content": "你是技术写作助手,续写要自然连贯。"}, {"role": "user", "content": f"根据以下内容续写一段:\n\n{st.session_state.content}"}, ] for delta in client.chat_stream(messages): buffer += delta placeholder.markdown(buffer) st.session_state.content = buffer st.rerun()st.empty()创建一个占位符,每次收到增量就重新渲染,用户能看到文字逐段出现。这个体验比转圈等待好很多,也是我最后保留的方案。
4. 本地启动与验证请求是否成功
代码写完了,接下来验证通道是否真的通了。这一步很重要,很多人卡在「代码没问题但请求失败」,其实是配置或网络细节。
4.1 启动 Streamlit
在项目目录下执行:
streamlit run app.py终端会输出一个本地地址,通常是http://localhost:8501。浏览器打开后,如果页面正常显示左右两栏,说明 Streamlit 部分没问题。
4.2 先用命令行验证 API 通道
在写界面之前,我建议先用一段最小脚本验证 TaoToken 通道是否可用,避免把配置问题和界面问题混在一起排查。
from llm_client import TaoTokenClient client = TaoTokenClient() resp = client.chat([ {"role": "user", "content": "用一句话介绍 Markdown。"} ]) print(resp)如果终端打印出模型返回的一句话,说明 Base URL、Key、Model ID 三件套都对了。如果报错,对照下一节的排查表。
4.3 验证流式输出
再验证流式:
for chunk in client.chat_stream([ {"role": "user", "content": "数到五,每个数字一行。"} ]): print(chunk, end="", flush=True)正常的话你会看到数字逐个出现,而不是等全部生成完才一次性打印。这一步通过,说明编辑器里的流式续写也能正常工作。
4.4 一次完整的生成效果演示
我在编辑器里输入一段半成品:
## 项目背景 这个工具的目标是帮助开发者更快地写文档。点「流式续写」后,MiniMax-M2 返回了类似下面的内容,并且是逐字出现的:
它通过统一 API 通道调用大模型,把润色、续写、格式整理这些重复劳动交给模型处理。 你只需要关注内容本身,剩下的排版和表达优化可以交给编辑器完成。预览区同步渲染出标题和段落,整个过程不需要刷新页面。这就是我想要的效果:写和改在同一个界面里完成。
5. 本篇常见报错排查:401、local proxy failed、reading choices
配置和调用过程中,最容易遇到下面几类报错。我把真实报错和对应原因整理出来,你对照着查。
5.1 401 Unauthorized
报错长这样:
requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://taotoken.net/api/v1/chat/completions原因通常是 Key 没读到或写错了。检查顺序:第一,.env文件是否在项目根目录,load_dotenv()是否在读取环境变量之前调用;第二,Key 是否有多余空格或换行;第三,环境变量名是否和代码里一致,我代码里用的是TAOTOKEN_API_KEY。如果部署在服务器上,确认环境变量真的注入到了运行进程里,而不是只写在某个 shell 配置里。
5.2 local proxy failed 或连接超时
报错类似:
requests.exceptions.ProxyError: HTTPConnectionPool(host='...', port=...): Max retries exceeded这类问题多半是本机代理设置干扰了请求。检查你的系统环境变量里有没有HTTP_PROXY、HTTPS_PROXY,如果有,在运行脚本前临时清掉:
unset HTTP_PROXY unset HTTPS_PROXY或者在代码里显式禁用代理:
session = requests.Session() session.trust_env = False然后把这个 session 传给请求。我本地调试时遇到过这个坑,清掉代理环境变量后请求立刻正常。
5.3 reading 'choices' 报错
报错长这样:
KeyError: 'choices'或者:
TypeError: 'NoneType' object is not subscriptable这说明返回的 JSON 里没有choices字段。常见原因有三个:一是请求体格式不对,比如messages写成了字符串而不是列表;二是模型 ID 写错,服务端返回了错误信息而不是正常结果;三是流式解析时把非数据行也当成了 JSON。排查方法是在resp.json()之后先打印完整响应:
data = resp.json() print(data)看清楚返回结构再取字段。如果是错误响应,通常会带error字段,里面会写明原因。
5.4 流式输出卡住或乱码
如果流式输出一直不结束,或者出现乱码,检查两点:一是iter_lines解码时是否用了utf-8;二是是否正确识别了[DONE]标记。有些兼容实现会在最后一行不带data:前缀,所以解析前先判断line.startswith("data: ")更稳妥。
5.5 三件套自查清单
遇到任何调用问题,先按这个清单过一遍:
| 检查项 | 正确值 | 常见错误 |
|---|---|---|
| Base URL | https://taotoken.net/api/v1 | 漏了/v1或多了斜杠 |
| API Key | sk-开头,无空格 | 复制时带了换行 |
| Model ID | MiniMax-M2 | 大小写不一致或拼错 |
| 请求路径 | /chat/completions | 拼成/completions |
| 请求方法 | POST | 误用 GET |
把这几项确认一遍,大部分报错都能定位。
6. 继续扩展:把编辑器用起来并接入更多模型
基础版本跑通后,你可以按自己的需求继续加功能。我列几个我实际加过的方向,都是低成本、高回报的。
第一个是「选中文本操作」。Streamlit 的text_area本身不返回选中范围,但你可以加一个输入框让用户粘贴要处理的段落,或者用st.text_area配合「处理最后一段」的按钮。我最后用的是后者,简单够用。
第二个是「格式整理」。把整篇文档丢给模型,让它统一标题层级、规范列表符号、修正表格对齐。提示词里明确要求「只输出整理后的 Markdown,不要解释」,输出质量会稳定很多。
第三个是「多模型对比」。因为走的是 TaoToken 统一通道,你只需要在侧边栏加一个下拉框,把 Model ID 作为参数传进客户端,就能在 MiniMax-M2 和其他模型之间切换,对比同一段文字的润色效果。这对选型很有帮助。
第四个是「导出」。Streamlit 自带下载按钮,把st.session_state.content编码后提供下载即可:
st.download_button( "下载 Markdown", data=st.session_state.content, file_name="document.md", mime="text/markdown", )如果你打算长期用这套方案写代码或做 Agent 类应用,可以了解一下 Coding Plan,它在高频编码场景下更划算;如果只是想先验证模型效果,直接打开模型对话页面试几句最直观。需要管理多个 Key 或查看用量,去控制台和 API Keys 页面操作即可。接入细节和参数说明都在接入文档里,遇到不确定的字段先查文档再改代码,比反复试错快得多。
这套编辑器的价值不在于功能多复杂,而在于它把「调用模型」这件事变成了编辑器里的一个按钮。你写文档时不用切窗口、不用复制粘贴到聊天框,选中、点击、结果直接落回文档。我用了两周之后,写教程的初稿时间大概缩短了一半,剩下的时间可以花在真正需要思考的内容结构上。