在开始写 LangChain 代码之前,先要准备一个可以稳定调用的大模型。
如果模型本身还不能正常加载,后面遇到问题时,就很难判断究竟是 LangChain 代码写错了,还是模型、运行环境、网络接口出了问题。因此,本系列先从最底层开始:把模型下载到本地,并确认它可以独立完成一次推理。
本文使用一台配备 64GB 统一内存的 M1 Max Mac,通过 Python 3.12、ModelScope 和 MLX-LM 部署 Qwen3-14B-AWQ-4bit-MLX。这里不讨论训练、微调和企业生产服务,而是完成模型下载、资源检查、直接推理验证,并把模型启动为只允许本机访问的 HTTP 服务。
如果还不清楚 LangChain、LangGraph 和 LangSmith 分别解决什么问题,可以先阅读 LangChain 系列 1《先搞懂 LangChain、LangGraph、LangSmith 是什么》。
整个部署过程可以拆成四个阶段:
环境准备
-> 模型下载与文件检查
-> MLX-LM 直接加载与推理验证
-> 启动本机 HTTP 服务并验证聊天接口
这四个阶段不是只追求“命令没有报错”。最终还要确认模型文件完整、MLX-LM 能够加载、模型可以生成中文、HTTP 聊天接口能够返回真实回答,并且资源占用在机器可接受的范围内。
1. 先认识模型社区
1.1 Hugging Face
Hugging Face 是常见的开放模型社区。开发者会把模型权重、配置、Tokenizer、数据集、示例代码和模型说明发布到 Hugging Face Hub。
下载一个模型时,拿到的通常不只是一个权重文件,而是一整个模型仓库。后面会看到,模型目录中除了 .safetensors 权重,还包括 config.json、Tokenizer、聊天模板和生成参数等文件。
1.2 魔塔社区 ModelScope
魔塔社区 ModelScope 是国内常用的模型社区和工具链,也提供模型浏览、下载、推理、训练和评测等能力。
模型地址:
mlx-community/Qwen3-14B-AWQ-4bit-MLX
选择 ModelScope 不是因为 Hugging Face 不能下载这个模型,而是因为本文的下载代码使用 ModelScope,并且在国内网络环境下通常更方便。
2. 为什么选择 Qwen3-14B-AWQ-4bit-MLX
模型 ID 是:
mlx-community/Qwen3-14B-AWQ-4bit-MLX
这个名称包含了本次选型的几个关键信息。
| 名称 | 含义 |
|---|---|
| Qwen3 | 模型系列 |
| 14B | 参数规模约为 140 亿 |
| AWQ | 一种权重量化方式 |
| 4bit | 模型权重主要按 4bit 保存 |
| MLX | 已转换为适合 Apple Silicon 使用的 MLX 格式 |
选择这个模型主要有三个原因。
第一,MLX 是面向 Apple Silicon 的数组计算框架。对于 M1、M2、M3、M4 系列 Mac,使用 MLX-LM 加载已经转换好的 MLX 模型,安装和推理过程都比较直接。
第二,4bit 量化可以显著降低模型权重占用。14B 模型如果使用 FP16 或 BF16,单是权重的理论体积就接近 28GB;使用 4bit 后,理论体积约为 7GB,更适合本地实验和单用户推理。
第三,14B 在模型能力和本地资源之间比较均衡。更小的模型占用更低,但中文问答、代码解释和后续 Agent 实验能力也会下降;更大的模型可能有更强的能力,但加载、上下文和并发会占用更多统一内存。
3. 部署前估算资源
下载模型之前,需要先判断机器能不能运行它。
部署前可以使用 ApX VRAM Calculator 估算显存需求。不过在线计算器只能给出参考值,最终仍然要看本机运行日志和活动监视器。
3.1 模型权重的粗略计算
最基础的计算公式是:
模型权重大小 ≈ 参数量 × 每个参数占用的字节数
常见精度可以这样估算:
| 精度 | 每个参数约占用 |
|---|---|
| FP32 | 4 字节 |
| FP16 / BF16 | 2 字节 |
| INT8 | 1 字节 |
| 4bit | 0.5 字节 |
因此,Qwen3-14B 4bit 的理论权重大小约为:
14B × 0.5 byte ≈ 7GB
3.2 运行内存不等于模型文件大小
7GB 只是权重的粗略下限。模型运行时还需要考虑:
| 资源 | 作用 |
|---|---|
| 量化元数据 | 保存 scale、bias 等量化信息 |
| KV Cache | 保存上下文计算结果,上下文越长,占用越高 |
| Tokenizer | 负责文本编码和解码 |
| MLX 与 Python 运行时 | 加载代码、张量和中间计算结果 |
| batch 与并发 | 同时处理的请求越多,占用越高 |
| macOS 系统占用 | 系统进程与模型共享统一内存 |
Apple Silicon 使用统一内存,因此不能简单地把 64GB 全部当成模型可用“显存”。操作系统、应用程序、模型权重和运行时缓存都使用同一块内存。
统一内存的组成可以这样理解:

图中各项不是固定比例。上下文越长、batch 越大、并发请求越多,KV Cache 和运行时开销通常越高,因此目录大小只能用于部署前粗估,不能替代运行时观察。
本次模型目录实测为 7.8G,MLX-LM 生成日志中的 Peak memory 为 8.510 GB。这个指标反映 MLX 运行过程中的峰值内存,不等于整台 Mac 的总统一内存占用;判断系统压力时还要结合活动监视器和 vm_stat。对 M1 Max 64GB 来说,本次单用户推理有较充足的余量,但这不代表可以无限增加上下文和并发。
4. 项目目录
项目代码统一放在:
/path/to/llm-learning
第一阶段目录结构如下:
llm-learning/
├── .venv_langchain/
├── Qwen3-14B-AWQ-4bit-MLX/
├── requirements.txt
└── langchain/
└── p02_local_models/
├── 01_download_model.py
├── 02_test_generate.py
├── README.md
└── requirements.txt
代码位于 langchain/p02_local_models/,模型入口放在项目根目录的 Qwen3-14B-AWQ-4bit-MLX/。目录名直接展示了模型名称、量化方式和运行格式,后续阅读启动命令与客户端代码时,不需要再猜测 model/ 代表哪一个模型。首次部署时,下载脚本会创建这个普通目录并把模型文件保存在其中。后续 LangChain、LangGraph 和 LangSmith 的代码都可以复用同一份模型,不需要每个阶段复制一份 7.8GB 的权重。
为了复用此前已经下载的权重,本机将这个描述性目录设置成了符号链接:
llm-learning/Qwen3-14B-AWQ-4bit-MLX -> /Users/bianhn/Documents/git/llm/qwen3/model
这只是作者本机的目录安排,不是运行本文的前置条件。代码始终访问项目根目录的 Qwen3-14B-AWQ-4bit-MLX/;第一次部署时不需要创建符号链接,下载脚本会直接创建同名普通目录并保存模型文件。
5. 使用 Python 3.12 创建虚拟环境
进入项目目录:
cd /path/to/llm-learning
明确使用 Python 3.12 创建同名虚拟环境:
python3.12 \
-m venv --prompt llm-learning .venv_langchain
激活虚拟环境:
source .venv_langchain/bin/activate
检查 Python 版本和解释器位置:
python -V
which python
本机输出:
Python 3.12.11
/Users/bianhn/Documents/git/llm-learning/.venv_langchain/bin/python
必须检查 which python。只看终端前面的环境名称并不可靠,尤其是在 Conda 的 (base) 环境中再次激活 venv 时,要以解释器实际路径为准。
6. 安装并锁定依赖
先升级 pip:
python -m pip install -U pip
第一阶段的 requirements.txt 内容如下:
mlx-lm==0.28.3
mlx==0.29.3
modelscope==1.31.0
transformers==4.57.1
安装依赖:
python -m pip install \
-r langchain/p02_local_models/requirements.txt
安装完成后,使用包元数据检查真实版本:
python - <<'PY'
from importlib.metadata import version
import platform
import sys
# 检查当前 Python,避免依赖被装到其他环境。
print("python", platform.python_version())
print("executable", sys.executable)
# mlx 没有稳定的 __version__ 属性,使用包元数据读取版本。
for package in ["mlx", "mlx-lm", "modelscope", "transformers"]:
print(package, version(package))
PY
本机输出:
python 3.12.11
executable /Users/bianhn/Documents/git/llm-learning/.venv_langchain/bin/python
mlx 0.29.3
mlx-lm 0.28.3
modelscope 1.31.0
transformers 4.57.1
本篇只安装本地推理需要的组件。LangChain、LangGraph 等依赖放到后续阶段安装,这样出现问题时更容易判断是模型环境还是应用框架造成的。
7. 使用 ModelScope 下载模型
下载脚本是 langchain/p02_local_models/01_download_model.py:
from pathlib import Path
import shutil
import sys
import time
from modelscope import snapshot_download
# 固定模型 ID,避免后续脚本误下载到原始 FP16/BF16 或其他量化版本。
MODEL_ID = "mlx-community/Qwen3-14B-AWQ-4bit-MLX"
MODEL_REVISION = "67a0161a28eb07263a862fa6b3a63fcd84b55c14"
# 当前脚本位于 llm-learning/langchain/p02_local_models/ 下。
# parents[2] 就是 llm-learning 项目根目录。
ROOT_DIR = Path(__file__).resolve().parents[2]
MODEL_DIR = ROOT_DIR / "Qwen3-14B-AWQ-4bit-MLX"
# 4bit 权重约 7-8GB,但下载临时文件、缓存和日志也要占空间。
# 这里预留 25GB,磁盘不足时提前失败,比下载到一半报错更容易排查。
MIN_FREE_GB = 25
def free_gb(path: Path) -> float:
"""返回指定目录所在磁盘的剩余空间,单位为 GiB。"""
usage = shutil.disk_usage(path)
return usage.free / 1024**3
def main() -> int:
# 模型放在项目根目录,后续 LangChain、LangGraph 等代码都可以共用这一份模型。
ROOT_DIR.mkdir(parents=True, exist_ok=True)
MODEL_DIR.mkdir(parents=True, exist_ok=True)
available = free_gb(ROOT_DIR)
print(f"Target directory: {ROOT_DIR}")
print(f"Model directory: {MODEL_DIR}")
print(f"Model ID: {MODEL_ID}")
print(f"Model revision: {MODEL_REVISION}")
print(f"Free disk: {available:.2f} GiB")
if available < MIN_FREE_GB:
print(
f"ERROR: free disk is lower than {MIN_FREE_GB} GiB. "
"Clean disk space before downloading the model.",
file=sys.stderr,
)
return 2
started = time.time()
# snapshot_download 会把模型仓库文件下载到本地目录。
# 如果中途网络中断,保留同一个 MODEL_DIR 后重新运行,通常可以复用已下载内容。
model_dir = snapshot_download(
model_id=MODEL_ID,
revision=MODEL_REVISION,
local_dir=str(MODEL_DIR),
)
elapsed = time.time() - started
print(f"Downloaded to: {model_dir}")
print(f"Elapsed seconds: {elapsed:.1f}")
print("Top-level files:")
# 下载后列出顶层文件,便于确认 safetensors、tokenizer、config 是否齐全。
for item in sorted(MODEL_DIR.iterdir()):
print(f" {item.name}")
return 0
if __name__ == "__main__":
# 用退出码表达下载结果,方便 shell、日志和后续自动化判断是否成功。
raise SystemExit(main())
运行脚本:
cd /path/to/llm-learning
source .venv_langchain/bin/activate
python langchain/p02_local_models/01_download_model.py
初次下载时,ModelScope SDK 共下载 15 个顶层文件,其中最耗时的是两个 .safetensors 权重分片。本次初次下载日志为:
Downloading: 100%|██████████| 15/15 [49:12<00:00, 196.86s/file]
Downloaded to: /Users/bianhn/Documents/git/llm-learning/Qwen3-14B-AWQ-4bit-MLX
Elapsed seconds: 2956.4
初次下载约 49 分钟。实际时间会受到网络速度、ModelScope 服务状态和磁盘写入速度影响,不能把这个数据当成固定值。
8. 模型目录中的 15 个文件
下载完成后,可以先确认顶层文件数量:
find -L Qwen3-14B-AWQ-4bit-MLX -maxdepth 1 -type f | wc -l
本机输出:
15
这 15 个文件的内容和作用如下。
| 文件 | 实测大小 | 内容与作用 |
|---|---|---|
| .gitattributes | 2.2K | Git LFS 和仓库文件属性配置。它不参与推理,但记录了模型仓库中的大文件应如何管理。 |
| README.md | 830B | 模型仓库说明,通常记录模型来源、转换方式和基础用法。它不是推理必需文件。 |
| added_tokens.json | 707B | 额外 token 与 token id 的映射,包括聊天、thinking 和工具调用可能使用的特殊 token。 |
| chat_template.jinja | 4.6K | 聊天模板。它负责把 system、user、assistant 消息转换成模型真正接收的 prompt。 |
| config.json | 80K | 模型主体配置,包括网络结构、层数、注意力头、上下文长度和量化参数等。 |
| configuration.json | 73B | ModelScope 平台使用的任务和框架元信息。 |
| generation_config.json | 237B | 默认生成参数,例如 temperature、top_p、top_k 和结束 token。 |
| merges.txt | 1.6M | BPE Tokenizer 的子词合并规则,与 vocab.json 一起决定文本怎样被切成 token。 |
| model-00001-of-00002.safetensors | 5.0G | 第一个模型权重分片,保存量化后的模型张量。 |
| model-00002-of-00002.safetensors | 2.8G | 第二个模型权重分片。两个分片合在一起才是完整权重。 |
| model.safetensors.index.json | 84K | 权重索引,记录每个张量位于哪个 .safetensors 分片中。 |
| special_tokens_map.json | 614B | 定义结束符、填充符、聊天边界等特殊 token 的语义。 |
| tokenizer.json | 11M | Tokenizer 的完整序列化文件,负责把文本编码为 token,并把输出 token 解码为文本。 |
| tokenizer_config.json | 5.3K | Tokenizer 配置,记录特殊 token、解码规则和聊天模板相关设置。 |
| vocab.json | 2.6M | Tokenizer 词表,记录 token 字符串与 token id 的对应关系。 |
可以把它们简单分成四类:
| 类型 | 主要文件 | 作用 |
|---|---|---|
| 模型权重 | model-*.safetensors | 保存模型参数 |
| 模型结构 | config.json、权重索引 | 告诉 MLX-LM 怎样构建并加载模型 |
| 文本处理 | tokenizer*、vocab.json、merges.txt | 在文本和 token 之间转换 |
| 对话与生成 | chat_template.jinja、generation_config.json | 组织聊天格式和默认生成参数 |
只看到两个大权重文件并不代表模型目录完整。缺少配置、索引或 Tokenizer 文件时,模型仍然可能无法加载。
9. 使用 MLX-LM 测试模型
测试脚本是 langchain/p02_local_models/02_test_generate.py:
from pathlib import Path
import subprocess
import time
from mlx_lm import generate, load
# 当前脚本位于 llm-learning/langchain/p02_local_models/ 下。
# 直接写出模型目录名称,让读者一眼看出当前加载的模型。
ROOT_DIR = Path(__file__).resolve().parents[2]
MODEL_DIR = ROOT_DIR / "Qwen3-14B-AWQ-4bit-MLX"
def run(command: list[str]) -> str:
"""执行系统命令并返回输出,这里只用于记录 du/vm_stat 等资源信息。"""
result = subprocess.run(command, check=False, text=True, capture_output=True)
return (result.stdout or result.stderr).strip()
def main() -> None:
if not MODEL_DIR.exists():
raise FileNotFoundError(f"Model directory does not exist: {MODEL_DIR}")
model_size_path = MODEL_DIR.resolve() if MODEL_DIR.is_symlink() else MODEL_DIR
# 先记录模型目录大小和加载前内存状态,方便和生成后的资源占用对比。
print(f"Model directory: {MODEL_DIR}")
if MODEL_DIR.is_symlink():
print(f"Model target: {model_size_path}")
print(f"Model size: {run(['du', '-sh', str(model_size_path)])}")
print("vm_stat before loading:")
print(run(["vm_stat"]))
started = time.time()
# load 会读取 MLX 权重和 tokenizer;如果模型文件不完整,通常会在这里暴露问题。
model, tokenizer = load(str(MODEL_DIR))
load_elapsed = time.time() - started
print(f"Model load elapsed seconds: {load_elapsed:.1f}")
messages = [
{"role": "system", "content": "你是一个老师。"},
{"role": "user", "content": "请用一句话说明唐诗和宋词的区别。"},
]
prompt = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True,
# Qwen3 默认可能先输出 <think> 思考过程。
# 连通性测试更关心模型能否快速返回答案,所以这里关闭 thinking。
enable_thinking=False,
)
generate_started = time.time()
response = generate(
model,
tokenizer,
prompt=prompt,
# 这里限制输出长度,避免测试脚本因为长回答占用过多时间和内存。
max_tokens=256,
# verbose=True 会打印 token 数、生成速度和 peak memory,是本次验证的关键指标。
verbose=True,
)
generate_elapsed = time.time() - generate_started
print("\nGenerated response:")
print(response)
print(f"\nGenerate elapsed seconds: {generate_elapsed:.1f}")
print("vm_stat after generation:")
print(run(["vm_stat"]))
if __name__ == "__main__":
main()
运行测试:
cd /path/to/llm-learning
source .venv_langchain/bin/activate
python langchain/p02_local_models/02_test_generate.py
本次重新运行后的关键输出:
Model directory: /Users/bianhn/Documents/git/llm-learning/Qwen3-14B-AWQ-4bit-MLX
Model target: /Users/bianhn/Documents/git/llm/qwen3/model
Model size: 7.8G /Users/bianhn/Documents/git/llm/qwen3/model
Model load elapsed seconds: 1.2
Prompt: 32 tokens, 36.757 tokens-per-sec
Generation: 38 tokens, 15.371 tokens-per-sec
Peak memory: 8.510 GB
Generate elapsed seconds: 3.7
模型实际回答:
唐诗以格律严谨、意境宏大见长,注重对仗与声律,而宋词则更重情感表达与音乐性,形式灵活,题材广泛。
这次测试的目的不是系统评估回答质量,而是确认模型文件完整、MLX-LM 可以加载权重、聊天模板能够正确组织消息,并且模型可以生成中文内容。脚本执行结束后进程会退出,模型也不再提供服务,因此还需要继续把它启动成常驻 HTTP 服务。
到这里可以确认:模型目录可访问、15 个文件可以被正确读取、MLX-LM 能够加载权重,Tokenizer 和聊天模板也能正常工作。
加载耗时和生成速度会随着机器负载、系统缓存、提示词长度和输出长度变化,因此文章记录的是本次实测值,不是性能保证。
10. 选择哪一种本地推理服务
下载模型只是把权重和配置保存到磁盘。要让 OpenAI SDK、LangChain 或其他程序通过统一地址调用模型,还需要启动一个持续运行的推理服务。
| 对比维度 | MLX-LM Server | Ollama | vLLM |
|---|---|---|---|
| 主要定位 | Apple Silicon 上的 MLX 模型推理与本地 HTTP 接口 | 简化本地模型下载、管理和运行 | 面向吞吐量、批处理和服务化的推理引擎 |
| 常见硬件与系统 | Apple Silicon Mac | macOS、Linux、Windows,可使用 CPU 或受支持的 GPU | 常见于 Linux 与服务器加速器;其他平台的支持取决于当前版本和硬件插件 |
| 模型产物 | 直接加载 MLX 格式权重 | 使用 Ollama 模型或其支持导入的模型格式 | 使用 vLLM 当前版本支持的模型架构与权重格式 |
| 接口 | 提供与 OpenAI Chat API 相近的 HTTP 接口 | 提供原生 API,也兼容部分 OpenAI API | 提供 OpenAI 兼容服务接口 |
| 并发与吞吐 | 适合本地开发和基础调用,本文不做高并发承诺 | 支持并行请求和队列,实际能力取决于内存、上下文和并发配置 | 连续批处理等能力更适合高吞吐场景,但仍需容量测试 |
| 部署复杂度 | 已有 MLX 模型时最直接 | 日常本地使用和模型管理较方便 | 参数和运行环境较多,生产部署还需要外围基础设施 |
| 安全边界 | 官方说明只提供基础安全检查,不建议直接用于生产 | 默认本机使用;对外暴露时仍需认证和网络防护 | 推理引擎不等于完整安全网关,生产环境仍需认证、TLS、限流和审计 |
对比依据可以查看 MLX-LM Server 官方说明、Ollama 模型导入文档、Ollama 并发说明和 vLLM 安装说明。这些工具仍在持续更新,因此表格说明的是选型方向,不是永久不变的兼容矩阵。
还需要注意,GGUF 是一种模型文件格式,不等于量化方法。GGUF 文件既可以保存不同精度的权重,也可以使用 Q4、Q8 等量化版本。类似地,本文目录中虽然也有 .safetensors 文件,但其中保存的是已经转换过的 MLX 量化张量,不能只根据扩展名判断它可以被 Ollama 或 vLLM 直接加载。
本文已经下载的是 MLX 格式模型,并且运行机器是 M1 Max,所以继续使用 MLX-LM Server 可以避免重新下载或转换另一份权重。
11. 启动本地私有化服务
这里的“私有化服务”是指模型文件和推理过程都位于本机,HTTP 服务也只监听本机回环地址。它可以供本机的 OpenAI SDK 和 LangChain 调用,但不等同于已经完成企业生产部署。
11.1 启动 MLX-LM Server
在第一个终端进入项目并激活虚拟环境:
cd /path/to/llm-learning
source .venv_langchain/bin/activate
启动服务:
"$VIRTUAL_ENV/bin/python" -m mlx_lm server \
--model Qwen3-14B-AWQ-4bit-MLX \
--host 127.0.0.1 \
--port 18080 \
--chat-template-args '{"enable_thinking": false}'
这里显式使用虚拟环境中的 Python,避免终端误调用 Conda Base 或其他环境中的 MLX-LM。参数含义如下。
| 参数 | 含义 |
|---|---|
--model Qwen3-14B-AWQ-4bit-MLX |
从项目根目录的同名模型目录读取权重、配置和 Tokenizer |
--host 127.0.0.1 |
只监听本机回环地址,局域网和公网不能直接访问 |
--port 18080 |
将 HTTP 服务端口固定为 18080 |
enable_thinking: false |
基础连通性测试关闭 Qwen3 thinking 输出 |
启动后会看到类似日志:
UserWarning: mlx_lm.server is not recommended for production as it only implements basic security checks.
INFO - Starting httpd at 127.0.0.1 on port 18080...
出现启动日志后,这个终端仍需保持运行。它表示 HTTP 服务已经监听端口,但还不能单独证明模型能够完成生成。
11.2 检查 HTTP 服务
在第二个终端执行:
curl --fail-with-body --silent --show-error \
http://127.0.0.1:18080/v1/models | python -m json.tool
该接口在当前环境返回:
{
"object": "list",
"data": []
}
HTTP 200 说明端口和路由可以访问。这个版本返回空的 data,不代表没有指定模型,也不能证明模型已经成功生成内容,因此还要继续请求聊天接口。
11.3 验证聊天接口
执行一次最小聊天请求:
curl --fail-with-body --silent --show-error \
http://127.0.0.1:18080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "Qwen3-14B-AWQ-4bit-MLX",
"messages": [
{"role": "system", "content": "你是一个老师。"},
{"role": "user", "content": "请用一句话说明唐诗和宋词的区别。"}
],
"temperature": 0,
"max_tokens": 96
}' | python -c \
'import json, sys; data = json.load(sys.stdin); print(data["choices"][0]["message"]["content"]); print(data["usage"])'
复测输出:
唐诗以格律严谨、意境宏大见长,注重对仗与声律,而宋词则更重情感表达与音乐性,形式灵活,题材广泛。
{'prompt_tokens': 32, 'completion_tokens': 38, 'total_tokens': 70}
聊天接口返回 HTTP 200、非空中文回答和 token 用量,才能确认 HTTP 服务、模型加载、聊天模板与生成流程都已经连通。请求中的 Qwen3-14B-AWQ-4bit-MLX 与启动命令的 --model Qwen3-14B-AWQ-4bit-MLX 保持一致;它既是当前项目中的模型目录名,也是客户端明确声明的模型名称。
11.4 停止服务与安全边界
测试完成后,在运行 MLX-LM Server 的第一个终端按 Ctrl+C 停止服务。停止后再次访问 18080 端口应该连接失败。
本文特意使用 127.0.0.1,而不是 0.0.0.0。MLX-LM 官方也明确提示这个服务只实现了基础安全检查,不建议直接用于生产环境。不要把 18080 端口直接暴露到局域网或公网。
企业环境除了推理引擎,还需要认证、TLS、反向代理、访问控制、限流、日志审计、监控告警和容量评测。本文只验证单机本地开发链路,不实现这些生产设施。
12. 怎样观察资源占用
12.1 检查模型文件大小
当前环境使用符号链接,所以检查它指向的真实模型目录:
du -sh /Users/bianhn/Documents/git/llm/qwen3/model
输出:
7.8G /Users/bianhn/Documents/git/llm/qwen3/model
12.2 查看系统内存
可以在另一个终端执行:
vm_stat
top -o mem
ps aux | grep '[p]ython'
也可以直接打开 macOS 的“活动监视器”,观察:
- 内存压力是否变成黄色或红色。
- Python 进程占用了多少内存。
- Swap 使用量是否持续增加。
- 生成结束后内存是否能够回收。
vm_stat 中的 Swapins、Swapouts 是系统启动以来的累计计数,不能只看到非零值就判断本次推理正在大量使用 Swap。判断当前状态时,应结合活动监视器中的内存压力和 Swap 使用量。
13. 本次遇到的问题
13.1 系统 python3 不是 Python 3.12
第一次直接执行:
python3 -m venv .venv_langchain
创建出来的是 Python 3.9 环境,并出现了 urllib3 与 LibreSSL 相关警告。
解决方法是删除错误环境,然后明确使用 Python 3.12:
rm -rf .venv_langchain
python3.12 \
-m venv --prompt llm-learning .venv_langchain
source .venv_langchain/bin/activate
python -V
which python
13.2 未固定版本导致依赖组合发生变化
直接执行不带版本约束的安装命令,会随着软件仓库更新而得到不同依赖组合。需要重建环境时,应优先使用项目的 requirements.txt;下面的命令用于强制重新安装其中记录的依赖:
python -m pip install --force-reinstall \
'mlx-lm==0.28.3' \
'mlx==0.29.3' \
'modelscope==1.31.0' \
'transformers==4.57.1'
因此,后续复现应优先使用仓库中的 requirements.txt,不要无条件升级所有依赖。
13.3 ModelScope 大文件下载中断
下载两个权重分片时出现过网络中断和 SSL 抖动:
IncompleteRead(...), will retry
SSLError(1, '[SSL: WRONG_VERSION_NUMBER] wrong version number'), will retry
处理时不要删除已经下载的 Qwen3-14B-AWQ-4bit-MLX/,也不要临时更换下载目录。先让 ModelScope SDK 自动重试;如果脚本最终退出,再使用相同模型 ID 和相同目录重新运行,避免重新下载已有文件。
13.4 Qwen3 的 thinking 占满输出长度
第一次测试没有关闭 thinking,模型先生成较长的
本文只是做连通性测试,因此在聊天模板中设置:
enable_thinking=False
这不会改变模型文件,只影响本次 prompt 的聊天模板。后续确实需要观察推理过程时,可以再开启 thinking,并相应提高 max_tokens。
13.5 移动项目后找不到模型
模型文件很大,不适合随着每个示例目录复制。作者本机通过项目根目录的 Qwen3-14B-AWQ-4bit-MLX 符号链接复用原来的权重:
ls -ld Qwen3-14B-AWQ-4bit-MLX
如果链接失效,可以重新创建:
rm Qwen3-14B-AWQ-4bit-MLX
ln -s /Users/bianhn/Documents/git/llm/qwen3/model Qwen3-14B-AWQ-4bit-MLX
如果它不是符号链接,而是正常的模型目录,不要执行 rm Qwen3-14B-AWQ-4bit-MLX。先用 ls -ld Qwen3-14B-AWQ-4bit-MLX 确认第一个字符是 l,再处理链接。
13.6 mx.metal.device_info 弃用警告
运行时可能看到:
mx.metal.device_info is deprecated and will be removed in a future version.
Use mx.device_info instead.
这是 MLX-LM 内部调用产生的弃用警告,不会阻止当前模型加载和生成。只要后面仍然有正常回答、生成速度和峰值内存日志,就不需要为了这个警告修改业务代码。
13.7 /v1/models 可访问,但聊天请求返回 502
本次实际遇到过 /v1/models 返回 HTTP 200,而聊天请求返回 502 的情况。服务端日志中同时出现:
RuntimeError: There is no Stream(gpu, 0) in current thread.
原因不是模型列表接口,而是客户端使用项目 .venv_langchain,服务端命令却调用了 Miniforge Base 环境中的旧版 MLX-LM。HTTP 路由可以先启动,真正进入模型生成线程后才暴露版本不匹配。
排查时应先查看运行 MLX-LM Server 的终端日志,然后检查解释器和依赖版本:
python -c 'import sys; print(sys.executable)'
python -c \
'from importlib.metadata import version; print(version("mlx-lm"), version("mlx"), version("transformers"))'
确认无误后,使用项目虚拟环境中的解释器重新启动服务:
"$VIRTUAL_ENV/bin/python" -m mlx_lm server \
--model Qwen3-14B-AWQ-4bit-MLX \
--host 127.0.0.1 \
--port 18080 \
--chat-template-args '{"enable_thinking": false}'
14. 小结
这次部署完成了五件事:
- 使用 Python 3.12 创建独立的
.venv_langchain虚拟环境。 - 使用 ModelScope 下载 Qwen3-14B-AWQ-4bit-MLX 的 15 个模型文件。
- 使用 MLX-LM 直接加载本地模型并生成中文回答。
- 使用 MLX-LM Server 启动只监听本机的 HTTP 服务。
- 分别验证模型列表路由和真实聊天生成接口,并记录资源占用与排错过程。
对于 M1 Max 64GB,14B MLX 4bit 模型能够在保留较多系统余量的情况下完成本地单用户推理,并通过 OpenAI 风格的聊天接口为本机程序提供服务。下一篇不再重复部署服务,而是分别使用 OpenAI Python SDK 和 LangChain 调用这个地址。
15. 最终部署结果
完成环境安装、模型下载、直接推理和 HTTP 服务验证后,将本次实际部署数据整理如下。
| 项目 | 实测结果 | 含义 |
|---|---|---|
| 机器 | Apple M1 Max,64GB 统一内存 | 本次部署使用的硬件及可供 CPU、GPU 共同使用的内存容量 |
| 系统 | macOS 15,arm64 | 操作系统版本及 Apple Silicon 使用的处理器架构 |
| Python | 3.12.11 | 运行下载和推理脚本的 Python 版本 |
| 模型 | mlx-community/Qwen3-14B-AWQ-4bit-MLX | ModelScope 上的完整模型仓库 ID,指向本次下载的具体模型版本 |
| 模型 revision | 67a0161a28eb07263a862fa6b3a63fcd84b55c14 | 下载脚本固定的模型仓库提交版本 |
| 模型格式 | MLX,AWQ 4bit 量化 | 权重适用于 MLX 框架,并使用 AWQ 方法压缩为 4bit |
| 模型目录大小 | 7.8G | 模型全部文件在磁盘上的实际占用空间 |
| 模型文件数量 | 15 个顶层文件 | 模型目录第一层包含的权重、配置和分词器等文件数量 |
| 初次下载耗时 | 约 49 分钟 | 首次从 ModelScope 下载完整模型所需时间 |
| MLX-LM | 0.28.3 | 用于加载和运行大语言模型的 MLX-LM 工具版本 |
| MLX | 0.29.3 | Apple Silicon 上执行张量计算的 MLX 框架版本 |
| ModelScope | 1.31.0 | 用于从魔塔社区下载模型的 Python 包版本 |
| transformers | 4.57.1 | 用于读取模型配置和分词器等内容的 Transformers 版本 |
| 本次复测加载耗时 | 1.2 秒 | 从本地磁盘加载模型并完成推理准备所需时间 |
| 本次复测生成速度 | 15.371 tokens/s | 模型平均每秒生成的 token 数量 |
| 本次复测 MLX-LM Peak memory | 8.510 GB | 本次推理过程中 MLX-LM 记录的峰值内存占用 |
| 本地服务地址 | http://127.0.0.1:18080 | 只允许本机访问的 MLX-LM Server 地址 |
| /v1/models | HTTP 200,data 为空数组 | 证明 HTTP 路由可访问,不单独代表推理成功 |
| /v1/chat/completions | HTTP 200,返回中文回答与 token 用量 | 证明服务端已经完成真实模型生成 |
| 服务适用范围 | 本机开发与接口联调 | MLX-LM Server 只提供基础安全检查,不直接作为生产公网服务 |
模型的 bit 数表示权重保存时使用的精度。16bit 精度较高,但模型体积和内存占用最大;8bit 在精度与资源占用之间较均衡;4bit 占用最小,更适合本地部署,但可能带来少量效果损失。以 14B 模型为例,仅权重的理论大小在 16bit、8bit 和 4bit 下分别约为 28GB、14GB 和 7GB。