一. 前言:本地模型跑起來,才是真的開始 #
如果你用 Apple Silicon Mac,最近一定很常看到幾個關鍵字:
MLX、Ollama、本地 LLM、量化模型、Apple unified memory。
看起來很熱鬧,但真正要開始寫程式時,問題通常會變成:
「所以我到底要怎麼在 Python 裡載入模型、送 prompt、拿回結果?」
今天拍拍君要介紹的是 MLX-LM。
它是 Apple MLX 生態裡專門處理大型語言模型推論的工具。
你可以把它想成 MLX 生態裡專門處理模型載入、文字生成、量化與指令列操作的那一層。
如果你之前看過 Python MLX 入門,那篇主要在講 MLX 的陣列、惰性求值、自動微分與基本模型。
如果你看過 MLX + Embeddings 語意搜尋,那篇則是把文字轉成向量,做搜尋和小型 RAG 索引。
這篇換一個更窄、更實用的角度:
用 MLX-LM 在 Mac 上跑文字生成模型,做出可以整合進工具鏈的本地推論流程。
不要急著追最大模型。
先把一個小模型跑穩,知道它吃多少記憶體、怎麼串流、怎麼包成 CLI,才是本地 AI 工作流真正好用的起點。
二. 什麼時候選 MLX-LM,而不是 Ollama? #
拍拍君先講結論:Ollama 和 MLX-LM 不是互斥關係。 它們比較像兩種不同層級的工具。 Ollama 適合「快速把模型服務化」。 你可以用 HTTP API 呼叫、快速切模型、做聊天 App,很適合一般本地 LLM 使用者。 MLX-LM 則更適合「在 Python 裡控制推論細節」。 例如你想要:
- 在 Python script 裡直接載入模型
- 測試不同 decoding 參數
- 做批次 prompt 實驗
- 研究 token streaming
- 自己包 CLI 或小型 worker
- 在 Apple Silicon 上用 MLX 格式模型做推論
如果目標只是開一個聊天介面,Ollama 可能更省心。
如果目標是寫程式控制本地模型,MLX-LM 會更貼近 Python 開發者。
簡單說:一般聊天和 HTTP API 優先選 Ollama;Python 推論實驗、批次處理和自製工具很適合 MLX-LM;embedding 搜尋則交給
mlx-embedding-models。 這篇不會重複 Ollama 的內容。 我們會直接進到 MLX-LM 的實作。
三. 建立專案與安裝 #
先建立一個乾淨專案。
拍拍君推薦用 uv,因為它處理虛擬環境和套件速度都很舒服。
mkdir mlx-lm-playground
cd mlx-lm-playground
uv init
uv add mlx-lm rich typer
確認安裝:
uv run python -c "import mlx_lm; print('mlx-lm ready')"
如果這一步報錯,通常是環境不是 Apple Silicon、macOS 太舊,或 Python 版本不合。 MLX 生態主要面向 Apple Silicon。 Intel Mac、Linux、Windows 不是這篇的目標平台。 接著確認指令列工具能用:
uv run mlx_lm.generate --help
你應該會看到 –model、–prompt、–max-tokens 等參數。
這代表 CLI 已經可以開始跑。
四. 選第一個模型:小、快、可測試 #
本地模型最大的坑,就是一開始選太大。 模型名稱看起來都很誘人,但 7B、14B、32B 跑起來對記憶體和等待時間都很現實。 第一次測 MLX-LM,拍拍君建議先用小模型,例如:
mlx-community/Qwen2.5-1.5B-Instruct-4bit
這類 1.5B、4-bit 的模型不一定最聰明,但下載短、記憶體壓力小,最適合快速測 CLI、Python API 和串流輸出。 等流程穩了,再換 3B、7B 或 coding model。 不要第一步就拿最大模型折磨自己。 本地 AI 的快樂,常常來自「可迭代」,不是「一次塞爆記憶體」。
五. 用 CLI 跑第一個 prompt #
先用最小指令測試:
uv run mlx_lm.generate \
--model mlx-community/Qwen2.5-1.5B-Instruct-4bit \
--prompt "用三句話解釋什麼是 MLX-LM。" \
--max-tokens 200
第一次執行會下載模型。 模型通常會被 cache 起來,之後再跑就不用重抓。 如果成功,你會看到模型輸出一段文字。 這一步的目標不是追求回答品質,而是確認模型下載、載入和 Apple Silicon 推論路徑都能跑通。 如果速度比想像中慢,先別慌。 第一次載入模型會花比較久。 真正需要觀察的是第二次、第三次生成時的體感。
六. 控制生成長度與溫度 #
本地模型不只是送 prompt 拿回答。 你通常會想控制輸出長度、隨機性和重複程度。 常用參數包含:
uv run mlx_lm.generate \
--model mlx-community/Qwen2.5-1.5B-Instruct-4bit \
--prompt "列出三個適合用本地 LLM 處理的工程任務。" \
--max-tokens 300 \
--temp 0.4
–max-tokens 控制最多生成多少 token。
它不是字數,也不是中文字數,而是模型內部的 token 數。
中文、英文、標點和空白都會被 tokenizer 切成不同單位。
–temp 是 temperature。
數值低一點,回答比較穩定。
數值高一點,回答比較發散。
拍拍君通常這樣抓:摘要、分類、格式化用 0.1 到 0.3;教學、解釋、一般問答用 0.3 到 0.7;腦力激盪或標題候選才拉到 0.7 以上。
本地模型的品質波動比大型雲端模型明顯。
所以 prompt 和參數要一起調,不要只怪模型笨。
七. 用 Python API 載入模型 #
CLI 很方便,但真正要整合到工具裡,通常會用 Python API。
建立 generate_once.py:
from __future__ import annotations
from mlx_lm import generate, load
MODEL_NAME = "mlx-community/Qwen2.5-1.5B-Instruct-4bit"
def main() -> None:
model, tokenizer = load(MODEL_NAME)
prompt = "請用繁體中文,用五個 bullet points 解釋 MLX-LM 適合做什麼。"
answer = generate(
model,
tokenizer,
prompt=prompt,
max_tokens=300,
verbose=False,
)
print(answer)
if __name__ == "__main__":
main()
執行:
uv run python generate_once.py
這個版本很直覺:
先 load(),再 generate()。
但要注意一件事:
模型載入很貴,不要每次 request 都重新 load。
如果你要做 CLI,載入一次還可以接受。
如果你要做 server 或 worker,就應該在 process 啟動時載入模型,後面重複使用同一個 model 和 tokenizer。
八. 用 chat template 寫對話 prompt #
Instruct 模型通常不是單純吃一段文字。 它們更習慣 messages 格式:
messages = [
{"role": "system", "content": "你是精簡、可靠的繁體中文工程助手。"},
{"role": "user", "content": "幫我整理三個 MLX-LM 使用情境。"},
]
不同模型的特殊 token 和格式可能不同。 所以不要自己手刻 User 和 Assistant 標籤。 應該讓 tokenizer 套用 chat template:
from __future__ import annotations
from mlx_lm import generate, load
MODEL_NAME = "mlx-community/Qwen2.5-1.5B-Instruct-4bit"
def build_prompt(tokenizer, question: str) -> str:
messages = [
{
"role": "system",
"content": "你是精簡、可靠、偏工程實用的繁體中文助手。",
},
{"role": "user", "content": question},
]
return tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
)
def main() -> None:
model, tokenizer = load(MODEL_NAME)
prompt = build_prompt(tokenizer, "用三句話說明 MLX-LM 和 Ollama 的差別。")
print(generate(model, tokenizer, prompt=prompt, max_tokens=240, verbose=False))
if __name__ == "__main__":
main()
這個小細節很重要。 同一個問題,用正確 chat template 和亂拼 prompt,回答品質可能差很多。
九. 串流輸出:讓答案一段一段出現 #
本地模型如果等整段生成完才印出來,體感會很慢。
聊天工具通常會改用 streaming。
建立 stream_answer.py:
from __future__ import annotations
from mlx_lm import load, stream_generate
MODEL_NAME = "mlx-community/Qwen2.5-1.5B-Instruct-4bit"
def main() -> None:
model, tokenizer = load(MODEL_NAME)
prompt = tokenizer.apply_chat_template(
[
{"role": "system", "content": "你是精簡的繁體中文工程助手。"},
{"role": "user", "content": "請解釋什麼是量化模型,限制在五句內。"},
],
tokenize=False,
add_generation_prompt=True,
)
for chunk in stream_generate(
model,
tokenizer,
prompt=prompt,
max_tokens=240,
):
print(chunk.text, end="", flush=True)
print()
if __name__ == "__main__":
main()
如果你的 mlx-lm 版本沒有從頂層匯出 stream_generate,先更新套件:
uv add --upgrade mlx-lm
串流不是只為了好看。 它會直接改善使用者體感。 尤其本地模型第一個 token 出來後,你就知道它正在工作,而不是卡住。
十. 做成一個簡單聊天 CLI #
接著可以把 streaming 範例包成互動式 CLI。 完整程式不需要很複雜,核心狀態只有兩個:
model/tokenizer:process 啟動時載入一次messages:保存最近幾輪對話 最小架構長這樣:
from mlx_lm import load, stream_generate
model, tokenizer = load("mlx-community/Qwen2.5-1.5B-Instruct-4bit")
messages: list[dict[str, str]] = []
while True:
question = input("you> ").strip()
if question in {"/exit", "/quit"}:
break
messages.append({"role": "user", "content": question})
prompt = tokenizer.apply_chat_template(
[{"role": "system", "content": "你是精簡的繁體中文工程助手。"}, *messages],
tokenize=False,
add_generation_prompt=True,
)
pieces: list[str] = []
print("ai> ", end="")
for chunk in stream_generate(model, tokenizer, prompt=prompt, max_tokens=500):
pieces.append(chunk.text)
print(chunk.text, end="", flush=True)
print()
messages.append({"role": "assistant", "content": "".join(pieces)})
這個版本已經能用。
之後要加 /reset、/save、–model,都只是工程包裝。
重點是三件事:模型不要重複載入、prompt 用 chat template、輸出用 streaming。
十一. 對話歷史不能無限長 #
上面的 CLI 有一個故意留下的問題: 對話歷史會越來越長。 本地模型有 context window 限制,越長也越慢。 實務上你需要做一個簡單修剪,例如只保留最近幾輪:
def trim_messages(messages: list[dict[str, str]], max_turns: int = 6) -> list[dict[str, str]]:
max_messages = max_turns * 2
return messages[-max_messages:]
在產生 prompt 前套用:
session.messages = trim_messages(session.messages)
prompt = session.prompt(tokenizer)
這不是最聰明的記憶方式,但很可靠。 如果你需要長期記憶,應該把舊內容摘要化,或搭配 embedding 搜尋。 也就是把今天這篇的 MLX-LM,和前一篇 MLX embeddings 語意搜尋 串起來。 簡單說:
- 最近對話:直接留在 prompt
- 舊筆記和文件:用 embeddings 找相關片段
- 生成回答:交給 MLX-LM 這就是一個小型本地 RAG assistant 的雛形。
十二. 批次任務:比聊天更適合本地模型 #
本地模型最實用的場景,常常不是聊天,而是批次處理。 例如整理一堆 Markdown 標題、幫 log 分類、替短文產生摘要。 這類任務不需要即時等待聊天回覆,可以晚上慢慢跑,資料留在本機,prompt 和輸出也能版本控制。 如果你在公司或研究環境處理敏感文件,這種本地批次流程會比把資料丟到外部 API 更容易控管邊界。 當然,本地不等於免安全風險。 但至少資料流比較清楚。
十三. 記憶體、量化與模型大小 #
MLX-LM 能跑多大的模型,取決於你的 Mac 記憶體、模型格式、量化程度和你願意等多久。 粗略來說:
1B - 3B 很適合測流程、小工具、批次摘要
7B - 8B 品質明顯好一些,記憶體壓力也更高
14B 以上 需要更認真看 unified memory 與速度
量化模型會犧牲一些精度,換取更小的體積和更低的記憶體需求。 常見名稱裡會看到 4bit、8bit。 第一次使用時,選 4-bit 小模型比較省事。 等你確定工作流值得投資,再比較不同大小模型的品質。 拍拍君會建議用一個固定測試集比較,而不是憑感覺。 例如準備 20 個你真的會問的 prompt,包含摘要、分類、程式碼解釋、中文改寫和幾個容易出錯的邊界案例。 然後用同一套 prompt 比較模型輸出。 這比「今天感覺它好像比較聰明」可靠多了。
十四. 常見錯誤與排查 #
1. 模型下載很慢 #
第一次下載通常最慢。 確認網路正常,並盡量先用小模型測。 如果下載中斷,可以重新執行同一個命令。
2. 記憶體壓力太大 #
換更小的模型,或換 4-bit 量化版本。
也可以降低 max_tokens,避免一次生成太長。
3. 回答格式很飄 #
先降低 temperature。 再把 system prompt 寫清楚。 如果要固定 JSON 格式,請給明確 schema,並在程式端做解析和驗證。 不要相信模型永遠乖乖輸出合法 JSON。
4. 中文品質不穩 #
換更適合中英文的 instruct model。 同時把 prompt 寫成繁體中文,並明確要求「使用繁體中文」。 小模型的語言穩定度通常比較有限,這是模型能力,不是 MLX-LM 的錯。
5. 第一次 token 出來很慢 #
模型載入、prompt 長度、context window 都會影響。 如果是服務型工具,請在 process 啟動時載入模型。 如果是聊天工具,使用 streaming 改善體感。
十五. 什麼時候不要用 MLX-LM? #
MLX-LM 很好玩,但不是所有問題的答案。
如果你需要跨平台、穩定 HTTP API、多人共用服務、最強推理能力,或只是要 embeddings,拍拍君會優先考慮 Ollama、llama.cpp server、雲端模型或 mlx-embedding-models。
工具選擇不用有宗教戰爭。
在 Mac 上,MLX-LM 的位置很清楚:
它是 Apple Silicon + Python + 本地 LLM 推論的舒服選項。
十六. 下一步:把它接進自己的工作流 #
今天我們完成了 MLX-LM 安裝、CLI 推論、Python API、chat template、streaming、簡單聊天 CLI,以及模型大小和記憶體取捨的整理。 下一步可以很務實。 不要急著做巨大 AI assistant。 先挑一個你每天真的會用的小任務:
- 摘要剪貼簿文字
- 整理會議筆記
- 批次改寫 Markdown 標題
- 幫 commit message 產生候選
- 從 log 裡整理錯誤摘要 只要這個工具每天省你五分鐘,它就值得存在。 本地模型的重點不是炫耀「我離線也能聊天」。 而是讓模型安靜地接進你的工作流,替你處理那些重複、瑣碎、又不該丟到外部服務的文字任務。 拍拍君覺得,這才是 Apple Silicon 上跑本地模型最迷人的地方。