上一篇已经使用 MLX-LM 直接加载本地 Qwen3-14B-AWQ-4bit-MLX,并完成了一次中文生成测试。
直接加载模型适合验证模型文件和推理环境,但业务代码会和 MLX-LM 绑定在一起。后续学习 LangChain 时,更常见的做法是先把模型启动成一个 HTTP 服务,再由不同的客户端通过统一接口调用。
本文只介绍两种客户端调用方式:
- 使用 OpenAI Python SDK 调用本地模型。
- 使用 LangChain 的 ChatOpenAI 调用同一个本地模型。
两种方式都访问 mlx_lm.server 提供的 OpenAI 兼容接口,不再在客户端代码中直接执行 mlx_lm.load() 和 generate()。
两种客户端与本地模型服务的关系如下:

图中的 OpenAI 兼容端点属于 MLX-LM Server,并不是另一层独立服务。两个客户端发送的 HTTP 请求结构相近,但向上提供的调用方法和返回对象不同。
1. 什么是 OpenAI 兼容接口
1.1 从直接加载变成服务调用
上一篇的调用过程是:
Python 脚本 -> MLX-LM -> 本地模型文件
这种方式下,每个脚本都要自己加载模型。如果多个程序需要调用模型,就会出现重复加载、代码绑定和资源管理困难等问题。
启动 mlx_lm.server 后,调用过程变成:
OpenAI SDK / LangChain
↓ HTTP 请求:http://127.0.0.1:18080/v1/chat/completions
MLX-LM Server(提供 OpenAI 兼容接口)
↓ 加载模型并完成推理
本地 Qwen3 模型
模型只由服务端加载,客户端只负责发送消息和接收回答。
1.2 “兼容”不等于使用 OpenAI 在线模型
OpenAI 兼容接口是指服务端提供与 OpenAI API 相近的路径、请求字段和响应结构。例如本文使用的是:
POST /v1/chat/completions
请求中仍然有 model、messages、temperature、max_tokens 等字段,但真正完成推理的是本机 Qwen3,不会把问题发送到 OpenAI 在线服务。
这种接口的价值在于:只要客户端允许修改 base_url,同一套调用方式就可以连接不同的兼容服务。
2. 准备项目与启动模型服务
2.1 当前代码目录
学习项目位于:
~/Documents/git/blog/source/_posts/llm_learning
第二阶段代码结构如下:
llm_learning/
├── .venv/
├── model -> /Users/bianhn/Documents/git/llm/qwen3/model
└── langchain/
└── p03_openai_compatible_llm/
├── 01_test_openai_client.py
├── 02_test_langchain_chatopenai.py
├── README.md
└── requirements.txt
model 是项目共用的模型入口。本机使用符号链接指向已经下载完成的 Qwen3 模型,因此不需要重新复制 7.8GB 权重。
2.2 激活 Python 3.12 虚拟环境
进入项目并激活虚拟环境:
cd ~/Documents/git/blog/source/_posts/llm_learning
source .venv/bin/activate
检查解释器:
python -V
python -c 'import sys; print(sys.executable)'
本机输出:
Python 3.12.11
/Users/bianhn/Documents/git/blog/source/_posts/llm_learning/.venv/bin/python
这里必须确认解释器位于项目的 .venv 中。终端可能同时显示 Conda 的 (base) 和 venv 名称,不能只根据提示符判断环境是否正确。
2.3 启动 MLX-LM Server
在第一个终端执行:
"$VIRTUAL_ENV/bin/python" -m mlx_lm server \
--model model \
--host 127.0.0.1 \
--port 18080 \
--chat-template-args '{"enable_thinking": false}'
这里特意使用 “$VIRTUAL_ENV/bin/python” -m mlx_lm server,而不是直接执行终端中搜索到的 mlx_lm.server。这样可以保证服务端使用项目虚拟环境中的 Python、MLX-LM 和 MLX,避免误用 Miniforge Base 环境里的旧版本。
参数含义:
| 参数 | 作用 |
|---|---|
| –model model | 使用项目根目录下的模型入口 |
| –host 127.0.0.1 | 只监听本机回环地址 |
| –port 18080 | 服务端口为 18080 |
| enable_thinking: false | 本次基础测试关闭 Qwen3 thinking 输出 |
启动后可以看到以下关键日志,日志前面的动态时间不影响服务状态,因此这里不记录:
/Users/bianhn/Documents/git/blog/source/_posts/llm_learning/.venv/lib/python3.12/site-packages/mlx_lm/server.py:974: UserWarning: mlx_lm.server is not recommended for production as it only implements basic security checks.
warnings.warn(
INFO - Starting httpd at 127.0.0.1 on port 18080...
这个终端需要保持运行。出现命令提示符之前不要关闭它,否则后面的客户端无法连接服务。
2.4 检查服务地址
在第二个终端执行:
curl --noproxy '*' http://127.0.0.1:18080/v1/models | \
python -c 'import json,sys; data=json.load(sys.stdin); print({"object": data["object"], "data": data["data"]})'
本次复测返回:
{'object': 'list', 'data': []}
这说明 HTTP 服务可以访问。需要注意,本文固定的 mlx-lm==0.28.3 在这个接口中返回空的 data,并不会列出启动参数指定的本地模型。浏览器地址必须写成 /v1/models,不是 /v1/modles。
因此,/v1/models 返回 200 只能证明服务端口可用,不能证明模型已经完成一次推理。模型能否真正生成内容,要以接下来的聊天请求为准。
3. 使用 OpenAI Python SDK 调用本地模型
3.1 安装 OpenAI 组件
在第二个终端进入相同虚拟环境:
cd ~/Documents/git/blog/source/_posts/llm_learning
source .venv/bin/activate
python -m pip install 'openai==2.7.1'
检查安装结果:
python -c 'import importlib.metadata as m; print(m.version("openai"))'
本文固定并实测的版本:
2.7.1
OpenAI SDK 会安装并使用 httpx 发送 HTTP 请求。本文测试环境中的 httpx 版本为 0.28.1。
3.2 编写 OpenAI SDK 代码
代码文件为 langchain/p03_openai_compatible_llm/01_test_openai_client.py:
# 这个文件只使用 OpenAI Python SDK 调用已经启动的本地 Qwen3 服务。
# 本地服务通过命令 mlx_lm.server 启动,本文件不负责启动服务。
import httpx
from openai import OpenAI
# 创建 OpenAI 客户端,并把请求地址改成本地 OpenAI 兼容接口。
client = OpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed", # 本地服务不校验 key,但 SDK 要求提供这个参数。
http_client=httpx.Client(trust_env=False), # 本地请求不读取 VPN 或 macOS 系统代理。
)
# 准备一轮最简单的聊天消息。
messages = [
{"role": "system", "content": "你是一个中文老师"},
{"role": "user", "content": "请介绍一下李白"},
]
# 调用本地服务的 Chat Completions 接口。
response = client.chat.completions.create(
model="default_model",
messages=messages,
temperature=0,
max_tokens=128,
)
# 只打印模型返回的文本,便于初学者观察调用结果。
print("OpenAI SDK response:")
print(response.choices[0].message.content)
这段代码可以分成四步:
- 创建 OpenAI 客户端。
- 使用 base_url 把请求地址改为本地服务。
- 准备 system 和 user 两条消息。
- 调用 chat.completions.create() 并读取回答。
3.3 关键参数
| 参数 | 作用 |
|---|---|
| base_url | 指向本地 OpenAI 兼容接口,而不是 OpenAI 在线地址 |
| api_key | OpenAI SDK 要求提供;当前本地服务不校验该值 |
| model | default_model 是 MLX-LM Server 接受的默认模型名称 |
| messages | 按角色保存系统要求和用户问题 |
| temperature=0 | 减少随机性,便于重复测试 |
| max_tokens=128 | 最多生成 128 个 token |
| trust_env=False | 不读取代理环境变量,确保请求直连 127.0.0.1 |
httpx.Client(trust_env=False) 是本机环境中的防御性设置。它可以避免本地请求被代理环境变量影响,但它不能修复服务端自身的模型加载或 MLX stream 错误。
3.4 运行代码
执行:
python langchain/p03_openai_compatible_llm/01_test_openai_client.py
本次复测真实输出:
OpenAI SDK response:
当然可以!李白(701年-762年),字太白,号青莲居士,是唐代最著名的诗人之一,被后人尊称为“诗仙”。他与杜甫并称“李杜”,是中国文学史上最伟大的诗人之一。
### 一、生平简介
李白出生于碎叶(今中亚地区),祖籍陇西成纪(今甘肃天水)。他自幼聪慧,博览群书,尤其喜爱诗歌和剑术。他性格豪放不羁,喜欢饮酒,常与文人墨客交往,游历四方,足迹
最后一句没有生成完整,是因为示例将 max_tokens 限制为 128。这不影响接口连通性验证;需要完整回答时,可以适当增大该值。
到这里已经证明 OpenAI Python SDK 可以通过本地兼容接口调用 Qwen3,并不依赖 OpenAI 在线模型。
4. 使用 LangChain 调用本地模型
4.1 安装 LangChain 组件
本文使用用户指定的 LangChain 1.x 版本:
python -m pip install \
'langchain==1.0.3' \
'langgraph==1.0.2' \
'langchain-core==1.0.2' \
'langchain-openai==1.0.1' \
'langchain-community==0.4.1'
也可以直接安装第二阶段依赖文件:
python -m pip install \
-r langchain/p03_openai_compatible_llm/requirements.txt
本次环境中的实际版本:
langchain==1.0.3
langgraph==1.0.2
langchain-core==1.0.2
langchain-openai==1.0.1
langchain-community==0.4.1
当前代码直接使用的是 langchain-openai 中的 ChatOpenAI。langgraph 和 langchain-community 在本篇还没有用到,先按照当前学习环境统一安装,后续章节再分别介绍。
4.2 编写 LangChain 代码
代码文件为 langchain/p03_openai_compatible_llm/02_test_langchain_chatopenai.py:
# 这个文件使用 LangChain 的 ChatOpenAI 调用已经启动的本地 Qwen3 服务。
# 本地服务通过命令 mlx_lm.server 启动,本文件只负责发送问题并打印回答。
import httpx
from langchain_openai import ChatOpenAI
# 创建 LangChain 聊天模型,并把请求地址改成本地 OpenAI 兼容接口。
llm = ChatOpenAI(
base_url="http://127.0.0.1:18080/v1",
api_key="not-needed", # 本地服务不校验 key,但 ChatOpenAI 要求提供这个参数。
model="default_model",
temperature=0,
max_tokens=128,
http_client=httpx.Client(trust_env=False), # 本地请求不读取 VPN 或系统代理。
)
# invoke 是 LangChain 聊天模型的统一调用方法,返回 AIMessage。
response = llm.invoke("请介绍一下李白")
# AIMessage.content 保存模型生成的文本。
print("LangChain response content:")
print(response.content)
虽然导入语句是:
from langchain_openai import ChatOpenAI
这仍然是在使用 LangChain。LangChain 1.x 把不同模型厂商的集成拆分成独立包,OpenAI 及其兼容接口由 langchain-openai 提供。
4.3 ChatOpenAI 做了什么
ChatOpenAI 在 OpenAI 兼容接口上增加了 LangChain 的聊天模型抽象。
本文最需要理解的是两个地方:
llm = ChatOpenAI(...)
response = llm.invoke("请介绍一下李白")
invoke() 是 LangChain 模型组件的统一调用方法。它返回的不是 OpenAI SDK 原始响应,而是 LangChain 的 AIMessage 对象,因此使用:
response.content
读取模型回答。下一篇介绍消息类型时,会继续说明 AIMessage、HumanMessage 和 SystemMessage。
4.4 运行代码
执行:
python langchain/p03_openai_compatible_llm/02_test_langchain_chatopenai.py
本次复测真实输出:
LangChain response content:
李白(701年-762年),字太白,号青莲居士,唐代著名诗人,被后人尊称为“诗仙”。他是中国文学史上最杰出的浪漫主义诗人之一,与杜甫并称“李杜”,在中国诗歌史上占有极其重要的地位。
### 一、生平简介
李白出生于碎叶(今吉尔吉斯斯坦境内),祖籍陇西成纪(今甘肃天水)。他自幼聪慧,博览群书,尤其喜爱道家思想,向往自由与自然。他一生游历四方,足迹遍布大江南北
Qwen3 推理发生在独立的 MLX-LM Server 中,LangChain 客户端只通过 HTTP 请求服务,不会在客户端重新加载这份模型权重。
5. OpenAI SDK 与 LangChain 的区别
两段代码访问的是同一个地址、同一个模型,区别主要在客户端抽象层。
| 对比项 | OpenAI Python SDK | LangChain ChatOpenAI |
|---|---|---|
| 主要定位 | 直接调用 OpenAI 或兼容接口 | 把接口包装为 LangChain 聊天模型 |
| 核心调用 | client.chat.completions.create() | llm.invoke() |
| 消息格式 | role/content 字典 | 字符串或 LangChain Message |
| 返回结果 | OpenAI SDK 响应对象 | LangChain AIMessage |
| 适合场景 | 接口连通性测试、简单聊天 | Prompt、Message、Chain、Tool、Agent 等后续学习 |
| 代码依赖 | openai | langchain-openai、langchain-core |
如果只是判断接口能不能调用,OpenAI SDK 更接近底层请求,排查问题也更直接。
如果后续准备使用 LangChain 的消息、提示词模板、结构化输出和 Agent,就使用 ChatOpenAI。它的价值不在于这一次调用少写几行代码,而在于后续组件可以使用统一模型接口继续组合。
6. 本次遇到的问题
本地接口调用失败时,可以先按下面的顺序区分网络、服务端生成和客户端参数问题:

最重要的判断是:/v1/models 返回 HTTP 200 只能证明服务和路由可以访问,不能证明模型已经完成一次生成。
6.1 /v1/models 能访问,但聊天请求返回 502
本次实际遇到过:
openai.InternalServerError: Error code: 502
排查过程中,/v1/models 可以返回 200,但服务端处理生成请求时出现:
RuntimeError: There is no Stream(gpu, 0) in current thread.
根因是客户端使用项目 .venv,服务端命令却解析到了 Miniforge Base 环境中的旧版 MLX-LM。HTTP 服务虽然可以启动,真正进入模型生成线程时仍然失败。
解决方法:
- 查看服务端终端或日志,不要只看客户端的 502。
- 确认项目环境中的 mlx-lm、mlx 和 transformers 版本。
- 使用项目虚拟环境的 Python 启动服务。
"$VIRTUAL_ENV/bin/python" -m mlx_lm server \
--model model \
--host 127.0.0.1 \
--port 18080 \
--chat-template-args '{"enable_thinking": false}'
经过验证的组合为:
mlx-lm==0.28.3
mlx==0.29.3
transformers==4.57.1
6.2 VPN 或代理影响本地请求
先用 curl 强制绕过代理:
curl --noproxy '*' http://127.0.0.1:18080/v1/models
两个 Python 示例都使用:
httpx.Client(trust_env=False)
它可以避免 httpx 读取代理环境变量。但如果禁用代理后仍然返回 502,应立即检查服务端日志;服务端模型生成失败同样会向客户端表现为 502。
6.3 浏览器返回 Not Found
正确地址是:
http://127.0.0.1:18080/v1/models
如果写成 /v1/modles,服务端会返回 404。浏览器 JSON 插件可能默认折叠 data 数组,需要展开数组才能看到模型信息,也可以直接使用 curl 查看完整响应。
6.4 default_model 与模型路径不同
本文固定的 MLX-LM 版本没有在 /v1/models 中列出本地模型,但两个客户端实际使用:
model="default_model"
MLX-LM Server 会接受这个默认模型名称,并将请求交给启动时通过 –model model 指定的模型。是否真正成功不能只看模型列表,仍要以聊天请求返回内容为准。
6.5 回答在最后一句被截断
OpenAI SDK 示例的回答停在“足迹”,原因是:
max_tokens=128
max_tokens 限制的是最多生成多少 token,不是字符数。连通性测试使用较小数值可以缩短等待时间;需要完整长回答时可以改为 256 或更大,同时观察内存和响应时间。
7. 小结
这一篇完成了从“脚本直接加载模型”到“客户端通过 HTTP 调用模型服务”的转换。
OpenAI Python SDK 更适合验证兼容接口本身,LangChain ChatOpenAI 则把同一个接口包装成 LangChain 聊天模型。两者不是互相替代:ChatOpenAI 的底层仍然按照 OpenAI 兼容格式发送请求,但向上提供了 LangChain 的 invoke() 和 AIMessage。
从下一篇开始,可以围绕这个 ChatOpenAI 对象继续学习系统消息、用户消息、AI 消息和多轮对话,不需要在每个示例中重新加载 7.8GB 模型权重。
8. 最终验证结果
| 验证项 | 实测结果 |
|---|---|
| 本地服务地址 | http://127.0.0.1:18080/v1 |
| /v1/models | HTTP 200;mlx-lm==0.28.3 返回空 data |
| OpenAI SDK | openai==2.7.1,成功返回真实 Qwen3 回答 |
| LangChain | langchain==1.0.3、langchain-openai==1.0.1 |
| LangChain 返回类型 | AIMessage,通过 response.content 读取文本 |
| 模型服务环境 | mlx-lm==0.28.3、mlx==0.29.3、transformers==4.57.1 |
| 代理处理 | 两个客户端都使用 httpx.Client(trust_env=False) |
| 最终状态 | OpenAI SDK 与 LangChain 均成功调用本地 Qwen3 |