LangChain 系列 3:用 OpenAI 兼容接口连接本地 Qwen3 LLM


上一篇已经使用 MLX-LM 直接加载本地 Qwen3-14B-AWQ-4bit-MLX,并完成了一次中文生成测试。

直接加载模型适合验证模型文件和推理环境,但业务代码会和 MLX-LM 绑定在一起。后续学习 LangChain 时,更常见的做法是先把模型启动成一个 HTTP 服务,再由不同的客户端通过统一接口调用。

本文只介绍两种客户端调用方式:

  1. 使用 OpenAI Python SDK 调用本地模型。
  2. 使用 LangChain 的 ChatOpenAI 调用同一个本地模型。

两种方式都访问 mlx_lm.server 提供的 OpenAI 兼容接口,不再在客户端代码中直接执行 mlx_lm.load() 和 generate()。

两种客户端与本地模型服务的关系如下:

OpenAI SDK 与 LangChain 调用本地接口

图中的 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)

这段代码可以分成四步:

  1. 创建 OpenAI 客户端。
  2. 使用 base_url 把请求地址改为本地服务。
  3. 准备 system 和 user 两条消息。
  4. 调用 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. 本次遇到的问题

本地接口调用失败时,可以先按下面的顺序区分网络、服务端生成和客户端参数问题:

本地 OpenAI 兼容接口调用失败排查

最重要的判断是:/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 服务虽然可以启动,真正进入模型生成线程时仍然失败。

解决方法:

  1. 查看服务端终端或日志,不要只看客户端的 502。
  2. 确认项目环境中的 mlx-lm、mlx 和 transformers 版本。
  3. 使用项目虚拟环境的 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

文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
  目录