快轉到主要內容
  1. 教學文章/

MLX-LM 實戰:在 Apple Silicon 上跑本地模型推論

·9 分鐘· loading · loading · ·
Mlx MLX-LM LLM Apple-Silicon Python Local AI
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
科技觀點 - 本文屬於一個選集。
§ 6: 本文

featured

一. 前言:本地模型跑起來,才是真的開始
#

如果你用 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 啟動時載入模型,後面重複使用同一個 modeltokenizer

八. 用 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 上跑本地模型最迷人的地方。

延伸閱讀
#

科技觀點 - 本文屬於一個選集。
§ 6: 本文

相關文章

本地 AI App 架構:Streamlit、Ollama、MLX 怎麼分工
·10 分鐘· loading · loading
LLM Local AI Streamlit Ollama Mlx Python Architecture
在 Mac/iPhone 生態跑本地 AI:Ollama、MLX 與行動端工作流
·9 分鐘· loading · loading
LLM Ollama Mlx Apple-Silicon IPhone Local AI
MLX + Embeddings:在 Apple Silicon 上打造本地語意搜尋
·7 分鐘· loading · loading
Mlx Embeddings Semantic Search Apple-Silicon Python
Streamlit + Ollama:打造本地 LLM Chatbot App
·11 分鐘· loading · loading
LLM Ollama Streamlit Python Local AI Chatbot
本地 LLM 實戰:Ollama + Python 打造自己的小助手
·9 分鐘· loading · loading
LLM Ollama Python AI Cli
Docker Compose + Ollama:一鍵啟動本地 AI 開發環境
·8 分鐘· loading · loading
Docker Docker-Compose Ollama LLM Local AI Devops